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

mozilla / fx-private-relay / 3bc340e2-329f-4ed2-8700-adaaac8d78c8

15 Dec 2023 06:50PM CUT coverage: 73.514% (-0.1%) from 73.614%
3bc340e2-329f-4ed2-8700-adaaac8d78c8

push

circleci

jwhitlock
Use branch database with production tests

Previously, migrations tests were run with production code, branch
requirements, and branch migrations. Now they run with production
requirements, so that third-party migrations are tested as well.

This uses pytest --reuse-db to create a test database with the branch's
migrations, and then a pip install with the production code. This more
closely emulates the mixed environment during a deploy.

1962 of 2913 branches covered (0.0%)

Branch coverage included in aggregate %.

6273 of 8289 relevant lines covered (75.68%)

19.91 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
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

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