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

mozilla / fx-private-relay / 3f6af296-e1d8-49e8-84e8-4464d7ab6333

06 May 2025 07:50PM CUT coverage: 85.251% (+0.008%) from 85.243%
3f6af296-e1d8-49e8-84e8-4464d7ab6333

Pull #5549

circleci

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

2461 of 3597 branches covered (68.42%)

Branch coverage included in aggregate %.

1 of 3 new or added lines in 1 file covered. (33.33%)

2 existing lines in 2 files now uncovered.

17313 of 19598 relevant lines covered (88.34%)

9.69 hits per line

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

28.24
/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 LargeArrow from "./images/free-onboarding-arrow-large.svg";
1✔
14
import { Button, LinkButton } from "../Button";
1✔
15
import Image from "../Image";
1✔
16
import { useGaViewPing } from "../../hooks/gaViewPing";
1✔
17
import { useGaEvent } from "../../hooks/gaEvent";
1✔
18
import { AliasData } from "../../hooks/api/aliases";
19
import { UserData } from "../../hooks/api/user";
20
import { RuntimeData } from "../../hooks/api/runtimeData";
21
import { EmailForwardingModal } from "./EmailForwardingModal";
1✔
22
import { useState } from "react";
1✔
23
import { ChevronRightIcon } from "../Icons";
1✔
24
import { AliasList } from "./aliases/AliasList";
1✔
25

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

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

47
  let step = null;
×
48
  let button = null;
×
49
  let skipButton = null;
×
50
  let next = null;
×
51

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

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

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

71
  if (props.profile.onboarding_free_state === 0) {
×
UNCOV
72
    const skipMaskCreation = () => {
×
73
      props.onNextStep(3);
×
74
      gaEvent({
×
75
        category: "Free Onboarding",
76
        action: "Engage",
77
        label: "onboarding-step-1-skip",
78
        value: 1,
79
      });
80
    };
81

82
    const createNewMask = async () => {
×
83
      let moveToNextStep = true;
×
84

85
      try {
×
86
        await props.generateNewMask({ mask_type: "random" });
×
87
      } catch (_e) {
88
        // On error, we can only move to the next step if the user has atleast 1 mask.
89
        if (!props.hasAtleastOneMask) {
×
90
          moveToNextStep = false;
×
91
        }
92
      }
93

94
      if (moveToNextStep) {
×
95
        props.onNextStep(1);
×
96
        gaEvent({
×
97
          category: "Free Onboarding",
98
          action: "Engage",
99
          label: "onboarding-step-1-create-random-mask",
100
          value: 1,
101
        });
102
      }
103
    };
104

105
    step = <StepOne />;
×
106

107
    skipButton = (
108
      <button
109
        ref={skipStepOneButtonRef}
110
        className={styles["skip-link"]}
111
        onClick={skipMaskCreation}
112
      >
113
        {l10n.getString("profile-free-onboarding-skip-step")}
114
      </button>
115
    );
116

117
    button = (
118
      <Button className={styles["generate-new-mask"]} onClick={createNewMask}>
119
        {l10n.getString("profile-free-onboarding-welcome-generate-new-mask")}
120
        <Image src={Plus} alt="" />
121
      </Button>
122
    );
123
  }
124

125
  if (props.profile.onboarding_free_state === 1) {
×
126
    const skipMaskTesting = () => {
×
127
      props.onNextStep(3);
×
128
      gaEvent({
×
129
        category: "Free Onboarding",
130
        action: "Engage",
131
        label: "onboarding-step-2-skip",
132
        value: 1,
133
      });
134
    };
135

136
    const nextStep = () => {
×
137
      props.onNextStep(2);
×
138
      gaEvent({
×
139
        category: "Free Onboarding",
140
        action: "Engage",
141
        label: "onboarding-step-2-next",
142
        value: 1,
143
      });
144
    };
145

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

156
    step = (
157
      <StepTwo
158
        aliases={props.aliases}
159
        profile={props.profile}
160
        user={props.user}
161
        runtimeData={props.runtimeData}
162
        continue={forwardedEmail}
163
        isModalOpen={isModalOpen}
164
        setIsModalOpen={setIsModalOpen}
165
        onUpdate={props.onUpdate}
166
      />
167
    );
168

169
    next = (
170
      <button
171
        ref={nextStepTwoButtonRef}
172
        className={styles["next-link"]}
173
        onClick={nextStep}
174
      >
175
        {l10n.getString("profile-free-onboarding-next-step")}
176
        <ChevronRightIcon className={styles.chevron} width={16} alt="" />
177
      </button>
178
    );
179

180
    button = (
181
      <Button
182
        className={styles["generate-new-mask"]}
183
        onClick={() => {
184
          setIsModalOpen(true);
×
185
        }}
186
      >
187
        {l10n.getString(
188
          "profile-free-onboarding-copy-mask-how-forwarding-works",
189
        )}
190
      </Button>
191
    );
192

193
    skipButton = (
194
      <button
195
        ref={skipStepTwoButtonRef}
196
        className={styles["skip-link"]}
197
        onClick={skipMaskTesting}
198
      >
199
        {l10n.getString("profile-free-onboarding-skip-step")}
200
      </button>
201
    );
202
  }
203

204
  if (props.profile.onboarding_free_state === 2) {
×
205
    const finish = () => {
×
206
      // this will trigger confetti in the dashboard
207
      props.onNextStep(4);
×
208
      gaEvent({
×
209
        category: "Free Onboarding",
210
        action: "Engage",
211
        label: "onboarding-step-3-complete",
212
        value: 1,
213
      });
214
    };
215

216
    step = <StepThree />;
×
217

NEW
218
    next = null;
×
219

220
    button = (
221
      <LinkButton
222
        ref={nextStepTwoButtonRef}
223
        target="_blank"
224
        className={`is-hidden-with-addon ${styles["get-addon-button"]}`}
225
        onClick={finish}
226
      >
227
        {l10n.getString("profile-free-onboarding-addon-finish")}
228
      </LinkButton>
229
    );
230

NEW
231
    skipButton = null;
×
232
  }
233

234
  return (
235
    <section className={styles.onboarding}>
236
      {step}
237
      <div className={styles.controls}>
238
        {button}
239
        <div className={styles["progress-container"]}>
240
          <VisuallyHidden>
241
            <progress
242
              max={getRuntimeConfig().maxOnboardingAvailable}
243
              value={props.profile.onboarding_free_state + 1}
244
            >
245
              {l10n.getString("multi-part-onboarding-step-counter", {
246
                step: props.profile.onboarding_free_state,
247
                max: getRuntimeConfig().maxOnboardingAvailable,
248
              })}
249
            </progress>
250
          </VisuallyHidden>
251
          {next}
252
          <ol className={styles["styled-progress-bar"]} aria-hidden={true}>
253
            <li
254
              className={
255
                props.profile.onboarding_free_state >= 0
×
256
                  ? styles["is-completed"]
257
                  : undefined
258
              }
259
            >
260
              <span></span>1
261
            </li>
262
            <li
263
              className={
264
                props.profile.onboarding_free_state >= 1
×
265
                  ? styles["is-completed"]
266
                  : undefined
267
              }
268
            >
269
              <span></span>2
270
            </li>
271
            <li
272
              className={
273
                props.profile.onboarding_free_state >= 2
×
274
                  ? styles["is-completed"]
275
                  : undefined
276
              }
277
            >
278
              <span></span>3
279
            </li>
280
          </ol>
281
        </div>
282
        {skipButton}
283
      </div>
284
    </section>
285
  );
286
};
287

288
const StepOne = () => {
1✔
289
  const l10n = useL10n();
×
290

291
  return (
292
    <div className={`${styles.step} ${styles["step-welcome"]}`}>
293
      <div className={styles["welcome-header"]}>
294
        <h1>{l10n.getString("profile-free-onboarding-welcome-headline")}</h1>
295
        <p>{l10n.getString("profile-free-onboarding-welcome-description")}</p>
296
      </div>
297
      <div className={styles["content-wrapper"]}>
298
        <Image src={WomanEmail} alt="" width={475} />
299
        <div className={styles["content-text"]}>
300
          <div>
301
            <Image className={styles["hidden-mobile"]} src={CheckMark} alt="" />
302
            <p className={styles["headline"]}>
303
              {l10n.getString(
304
                "profile-free-onboarding-welcome-item-headline-1",
305
              )}
306
            </p>
307
            <p className={styles["description"]}>
308
              {l10n.getString(
309
                "profile-free-onboarding-welcome-item-description-1",
310
              )}
311
            </p>
312
          </div>
313
          <div>
314
            <p className={styles["headline"]}>
315
              {l10n.getString(
316
                "profile-free-onboarding-welcome-item-headline-2",
317
              )}
318
            </p>
319
            <p className={styles["description"]}>
320
              {l10n.getString(
321
                "profile-free-onboarding-welcome-item-description-2",
322
              )}
323
            </p>
324
          </div>
325
        </div>
326
      </div>
327
    </div>
328
  );
329
};
330

331
type StepTwoProps = {
332
  aliases: AliasData[];
333
  profile: ProfileData;
334
  user: UserData;
335
  runtimeData?: RuntimeData;
336
  isModalOpen: boolean;
337
  setIsModalOpen: (isOpen: boolean) => void;
338
  continue: () => void;
339
  onUpdate: (alias: AliasData, updatedFields: Partial<AliasData>) => void;
340
};
341

342
const StepTwo = (props: StepTwoProps) => {
1✔
343
  const l10n = useL10n();
×
344
  const [isSet, setIsSet] = useState(false);
×
345

346
  return (
347
    <div className={`${styles.step} ${styles["step-copy-mask"]}`}>
348
      <EmailForwardingModal
349
        isOpen={props.isModalOpen}
350
        onClose={() => {
351
          props.setIsModalOpen(false);
×
352
        }}
353
        onComplete={() => {
354
          props.setIsModalOpen(false);
×
355
          props.continue();
×
356
        }}
357
        onConfirm={() => {
358
          setIsSet(true);
×
359
        }}
360
        isSet={isSet}
361
      />
362
      <div className={styles["copy-mask-header"]}>
363
        <h1>{l10n.getString("profile-free-onboarding-copy-mask-headline")}</h1>
364
        <p>{l10n.getString("profile-free-onboarding-copy-mask-description")}</p>
365
      </div>
366
      <div className={styles["content-wrapper-copy-mask"]}>
367
        <div className={styles["copy-mask-arrow-element"]}>
368
          <Image src={VerticalArrow} alt="" />
369
        </div>
370
        <AliasList
371
          aliases={props.aliases}
372
          onCreate={() => {}}
373
          onUpdate={props.onUpdate}
374
          onDelete={() => {}}
375
          profile={props.profile}
376
          user={props.user}
377
          runtimeData={props.runtimeData}
378
          onboarding={true}
379
        >
380
          <div className={styles["content-wrapper-copy-mask-items"]}>
381
            <div className={styles["content-item"]}>
382
              <Image src={Emails} alt="" />
383
              <div className={styles["content-text"]}>
384
                <p className={styles["headline"]}>
385
                  {l10n.getString(
386
                    "profile-free-onboarding-copy-mask-item-headline-1",
387
                  )}
388
                </p>
389
                <p className={styles["description"]}>
390
                  {l10n.getString(
391
                    "profile-free-onboarding-copy-mask-item-description-1",
392
                  )}
393
                </p>
394
              </div>
395
            </div>
396
            <hr />
397
            <div className={styles["content-item"]}>
398
              <Image src={Congratulations} alt="" />
399
              <div className={styles["content-text"]}>
400
                <p className={styles["headline"]}>
401
                  {l10n.getString(
402
                    "profile-free-onboarding-copy-mask-item-headline-2",
403
                  )}
404
                </p>
405
                <p className={styles["description"]}>
406
                  {l10n.getString(
407
                    "profile-free-onboarding-copy-mask-item-description-2",
408
                  )}
409
                </p>
410
              </div>
411
            </div>
412
          </div>
413
        </AliasList>
414
      </div>
415
    </div>
416
  );
417
};
418

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

422
  return (
423
    <div
424
      className={`${styles.step} ${styles["step-copy-mask"]} ${styles["mask-use"]}`}
425
    >
426
      <div className={styles["copy-mask-header"]}>
427
        <h1>{l10n.getString("profile-free-onboarding-addon-headline")}</h1>
428
        <p>{l10n.getString("profile-free-onboarding-addon-subheadline")}</p>
429
      </div>
430
      <div className={styles["addon-content-wrapper"]}>
431
        <div className={styles["addon-content-items"]}>
432
          <div className={styles["addon-content-text"]}>
433
            <p className={styles.headline}>
434
              {l10n.getString("profile-free-onboarding-addon-item-headline-1")}
435
            </p>
436
            <p className={styles.description}>
437
              {l10n.getString(
438
                "profile-free-onboarding-addon-item-description-1",
439
              )}
440
            </p>
441
            <Image className={styles["large-arrow"]} src={LargeArrow} alt="" />
442
          </div>
443
          <Image src={WorkingMan} alt="" />
444
        </div>
445
      </div>
446
    </div>
447
  );
448
};
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