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

mozilla / fx-private-relay / 9fb3bc6c-78d7-4a1e-8156-57326783f61b

05 Dec 2023 03:12PM CUT coverage: 73.533% (-0.02%) from 73.55%
9fb3bc6c-78d7-4a1e-8156-57326783f61b

push

circleci

web-flow
Merge pull request #4198 from mozilla/free-user-onboarding-update

Free user onboarding changes from readiness meeting

1970 of 2920 branches covered (0.0%)

Branch coverage included in aggregate %.

2 of 3 new or added lines in 2 files covered. (66.67%)

1 existing line in 1 file now uncovered.

6262 of 8275 relevant lines covered (75.67%)

19.69 hits per line

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

29.35
/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 Image from "next/image";
1✔
17
import { Button, LinkButton } from "../Button";
1✔
18
import { event as gaEvent } from "react-ga";
1✔
19
import { useGaViewPing } from "../../hooks/gaViewPing";
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 { supportsChromeExtension } 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

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

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

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

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

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

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

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

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

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

113
    step = <StepOne />;
×
114

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

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

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

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

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

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

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

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

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

212
  if (props.profile.onboarding_free_state === 2) {
×
213
    const linkForBrowser = supportsChromeExtension()
×
214
      ? "https://chrome.google.com/webstore/detail/firefox-relay/lknpoadjjkjcmjhbjpcljdednccbldeb?utm_source=fx-relay&utm_medium=onboarding&utm_campaign=install-addon"
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
NEW
229
      props.onNextStep(4);
×
UNCOV
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

240
    next = (
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
    );
250

251
    button = (
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

269
    skipButton = (
270
      <button
271
        ref={skipStepThreeButtonRef}
272
        className={styles["skip-link"]}
273
        onClick={skipAddonStep}
274
      >
275
        {l10n.getString("profile-free-onboarding-skip-step")}
276
      </button>
277
    );
278
  }
279

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

334
const StepOne = () => {
1✔
335
  const l10n = useL10n();
×
336

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

377
type StepTwoProps = {
378
  aliases: AliasData[];
379
  profile: ProfileData;
380
  user: UserData;
381
  runtimeData?: RuntimeData;
382
  isModalOpen: boolean;
383
  setIsModalOpen: (isOpen: boolean) => void;
384
  continue: () => void;
385
  onUpdate: (alias: AliasData, updatedFields: Partial<AliasData>) => void;
386
};
387

388
const StepTwo = (props: StepTwoProps) => {
1✔
389
  const l10n = useL10n();
×
390
  const [isSet, setIsSet] = useState(false);
×
391

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

465
const StepThree = () => {
1✔
466
  const l10n = useL10n();
×
467

468
  return (
469
    <div
470
      className={`${styles.step} ${styles["step-copy-mask"]} ${styles["mask-use"]}`}
471
    >
472
      <div className={styles["copy-mask-header"]}>
473
        <h1>{l10n.getString("profile-free-onboarding-addon-headline")}</h1>
474
        <p>{l10n.getString("profile-free-onboarding-addon-subheadline")}</p>
475
      </div>
476
      <div className={styles["addon-content-wrapper"]}>
477
        <div className={styles["addon-content-items"]}>
478
          <div className={styles["addon-content-text"]}>
479
            <p className={styles.headline}>
480
              {l10n.getString("profile-free-onboarding-addon-item-headline-1")}
481
            </p>
482
            <p className={styles.description}>
483
              {l10n.getString(
484
                "profile-free-onboarding-addon-item-description-1",
485
              )}
486
            </p>
487
            <Image className={styles["large-arrow"]} src={LargeArrow} alt="" />
488
          </div>
489
          <Image src={WorkingMan} alt="" />
490
        </div>
491

492
        <div className={styles["addon-content-items"]}>
493
          <Image src={Extension} alt="" />
494
          <div className={styles["addon-content-text"]}>
495
            <p className={styles.headline}>
496
              {l10n.getString("profile-free-onboarding-addon-item-headline-2")}
497
            </p>
498
            <p className={styles.description}>
499
              {l10n.getString(
500
                "profile-free-onboarding-addon-item-description-2",
501
              )}
502
            </p>
503
            <Image className={styles["small-arrow"]} src={SmallArrow} alt="" />
504
          </div>
505
        </div>
506
      </div>
507
    </div>
508
  );
509
};
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