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

mozilla / fx-private-relay / 8b39734c-31ce-4dbf-9b05-171797c18cae

02 Oct 2025 04:03PM UTC coverage: 88.815% (-0.05%) from 88.863%
8b39734c-31ce-4dbf-9b05-171797c18cae

Pull #5924

circleci

vpremamozilla
MPP-4348: propagate UTM params from landing page to SubPlat and Accounts URLs
Pull Request #5924: MPP-4348: propagate UTM params from landing page to SubPlat and Accounts URLs

2932 of 3953 branches covered (74.17%)

Branch coverage included in aggregate %.

13 of 21 new or added lines in 5 files covered. (61.9%)

4 existing lines in 3 files now uncovered.

18086 of 19712 relevant lines covered (91.75%)

11.52 hits per line

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

87.93
/frontend/src/components/landing/PlanGrid.tsx
1
import { useTab, useTabList, useTabPanel } from "react-aria";
3✔
2
import { ReactNode, useRef } from "react";
3✔
3
import Link from "next/link";
3✔
4
import {
5
  Item,
6
  TabListProps,
7
  TabListState,
8
  useTabListState,
9
} from "react-stately";
3✔
10
import styles from "./PlanGrid.module.scss";
3✔
11
import {
12
  getPeriodicalPremiumPrice,
13
  getPeriodicalPremiumYearlyPrice,
14
  getPeriodicalPremiumSubscribeLink,
15
  getPhonesPrice,
16
  getPhonesYearlyPrice,
17
  getPhoneSubscribeLink,
18
  isPeriodicalPremiumAvailableInCountry,
19
  isPhonesAvailableInCountry,
20
  getMegabundlePrice,
21
  getMegabundleYearlyPrice,
22
  isMegabundleAvailableInCountry,
23
  getMegabundleSubscribeLink,
24
  getIndividualBundlePrice,
25
  getBundleDiscountPercentage,
26
} from "../../functions/getPlan";
3✔
27
import { RuntimeData } from "../../hooks/api/types";
28
import {
29
  CheckIcon2,
30
  VpnIcon,
31
  PlusIcon2,
32
  MonitorIcon,
33
  RelayIcon,
34
} from "../Icons";
3✔
35
import { getRuntimeConfig } from "../../config";
3✔
36
import { useGaEvent } from "../../hooks/gaEvent";
3✔
37
import { useGaViewPing } from "../../hooks/gaViewPing";
3✔
38
import { Plan, trackPlanPurchaseStart } from "../../functions/trackPurchase";
3✔
39
import { setCookie } from "../../functions/cookies";
3✔
40
import { useL10n } from "../../hooks/l10n";
3✔
41
import { LinkButton } from "../Button";
3✔
42
import { useIsLoggedIn } from "../../hooks/session";
3✔
43
import { getLocale } from "../../functions/getLocale";
3✔
44

45
export type Props = {
46
  runtimeData: RuntimeData;
47
};
48

49
/**
50
 * Grid cards to compare and choose between the different plans available to the user.
51
 */
52
export const PlanGrid = (props: Props) => {
15✔
53
  const l10n = useL10n();
15✔
54

55
  const freeButtonRef = useGaViewPing({
15✔
56
    category: "Sign In",
57
    label: "plan-grid-free-cta",
58
  });
59

60
  const bundleButtonRef = useGaViewPing({
15✔
61
    category: "Purchase Megabundle button",
62
    label: "plan-grid-megabundle-cta",
63
  });
64

65
  const gaEvent = useGaEvent();
15✔
66

67
  const countSignIn = (label: string) => {
15✔
68
    gaEvent({
×
69
      category: "Sign In",
70
      action: "Engage",
71
      label: label,
72
    });
73
    setCookie("user-sign-in", "true", { maxAgeInSeconds: 60 * 60 });
×
74
  };
75

76
  const isLoggedIn = useIsLoggedIn();
15✔
77

78
  const formatter = new Intl.NumberFormat(getLocale(l10n), {
15✔
79
    style: "currency",
80
    currency: "USD",
81
  });
82

83
  return (
84
    <div
85
      id="pricing"
86
      className={styles.content}
87
      data-testid="plan-grid-megabundle"
88
    >
89
      <div className={styles.header}>
90
        <h2>
91
          <b>{l10n.getString("plan-grid-title")}</b>
92
        </h2>
93
        <p>{l10n.getString("plan-grid-body")}</p>
94
      </div>
95
      <section id="pricing-grid" className={styles.pricingPlans}>
96
        {isMegabundleAvailableInCountry(props.runtimeData) ? (
15✔
97
          <dl
98
            key={"megabundle"}
99
            className={styles.pricingCard}
100
            aria-label={l10n.getString("plan-grid-megabundle-title")}
101
          >
102
            <dt>
103
              <b>{l10n.getString("plan-grid-megabundle-title")}</b>
104
              <span className={styles.pricingCardLabel}>
105
                {l10n.getFragment("plan-grid-megabundle-label", {
106
                  vars: {
107
                    discountPercentage: getBundleDiscountPercentage(
108
                      props.runtimeData,
109
                    ),
110
                  },
111
                })}
112
              </span>
113
              <p>{l10n.getString("plan-grid-megabundle-subtitle")}</p>
114
            </dt>
115
            <dd key={`megabundle-feature-1`}>
116
              <a
117
                key="bundle-vpn"
118
                className={styles.bundleItemLink}
119
                href={"https://www.mozilla.org/products/vpn/"}
120
              >
121
                <div className={styles.bundleTitle}>
122
                  <VpnIcon alt="" />
123
                  <b>{l10n.getString("plan-grid-megabundle-vpn-title")}</b>
124
                </div>
125
                {l10n.getString("plan-grid-megabundle-vpn-description")}
126
              </a>
127
            </dd>
128
            <dd key={"megabundle-feature-2"}>
129
              <Link
130
                key="megabundle-monitor"
131
                className={styles.bundleItemLink}
132
                href="https://monitor.mozilla.org/"
133
              >
134
                <div className={styles.bundleTitle}>
135
                  <MonitorIcon alt="" />
136
                  <b>{l10n.getString("plan-grid-megabundle-monitor-title")}</b>
137
                </div>
138
                {l10n.getString("plan-grid-megabundle-monitor-description")}
139
              </Link>
140
            </dd>
141
            <dd key={"megabundle-feature-3"}>
142
              <a
143
                key="megabundle-relay"
144
                className={styles.bundleItemLink}
145
                href="#"
146
                onClick={(e) => {
147
                  e.preventDefault();
×
148
                }}
149
              >
150
                <div className={styles.bundleTitle}>
151
                  <RelayIcon alt="" />
152
                  <b>{l10n.getString("plan-grid-megabundle-relay-title")}</b>
153
                </div>
154
                {l10n.getString("plan-grid-megabundle-relay-description")}
155
              </a>
156
            </dd>
157
            <dd className={styles.pricingCardCta}>
158
              <p id="pricingPlanBundle">
159
                <span className={styles.pricingCardSavings}>
160
                  {l10n.getString("plan-grid-megabundle-yearly", {
161
                    yearly_price: getMegabundleYearlyPrice(
162
                      props.runtimeData,
163
                      l10n,
164
                    ),
165
                  })}
166
                </span>
167
                <strong>
168
                  <s>{formatter.format(getIndividualBundlePrice("monthly"))}</s>
169
                  {l10n.getString("plan-grid-megabundle-monthly", {
170
                    price: getMegabundlePrice(props.runtimeData, l10n),
171
                  })}
172
                </strong>
173
              </p>
174
              <LinkButton
175
                href={getMegabundleSubscribeLink(props.runtimeData)}
176
                className={styles["megabundle-pick-button"]}
177
                ref={bundleButtonRef}
178
                data-testid="plan-cta-megabundle"
179
                onClick={() =>
180
                  trackPlanPurchaseStart(
1✔
181
                    gaEvent,
182
                    { plan: "megabundle" },
183
                    {
184
                      label: "plan-grid-megabundle-cta",
185
                    },
186
                  )
187
                }
188
              >
189
                {l10n.getString("plan-grid-card-btn")}
190
              </LinkButton>
191
            </dd>
192
          </dl>
193
        ) : null}
194
        <dl
195
          key={"phone"}
196
          className={styles.pricingCard}
197
          aria-label={l10n.getString("plan-grid-premium-title")}
198
        >
199
          <dt>
200
            <b>{l10n.getString("plan-grid-premium-title")}</b>
201
            <p>{l10n.getString("plan-grid-phone-subtitle")}</p>
202
          </dt>
203
          <dd key={"phone-feature-plus"}>
204
            <span className={styles.plusNote}>
205
              <PlusIcon2 alt={l10n.getString("plan-grid-card-phone-plus")} />
206
              <b>{l10n.getString("plan-grid-card-phone-plus")}</b>
207
            </span>
208
          </dd>
209
          <dd key={`phone-feature-1`}>
210
            <CheckIcon2 alt={""} />
211
            <span>
212
              {l10n.getFragment("plan-grid-card-phone-item-one", {
213
                elems: { b: <b /> },
214
              })}
215
            </span>
216
          </dd>
217
          <dd className={styles.pricingCardCta}>
218
            {isPhonesAvailableInCountry(props.runtimeData) ? (
15✔
219
              <PricingToggle
220
                monthlyBilled={{
221
                  monthly_price: getPhonesPrice(
222
                    props.runtimeData,
223
                    "monthly",
224
                    l10n,
225
                  ),
226
                  subscribeLink: getPhoneSubscribeLink(
227
                    props.runtimeData,
228
                    "monthly",
229
                  ),
230
                  gaViewPing: {
231
                    category: "Purchase monthly Premium+phones button",
232
                    label: "plan-grid-phone-monthly-cta",
233
                  },
234
                  plan: {
235
                    plan: "phones",
236
                    billing_period: "monthly",
237
                  },
238
                }}
239
                yearlyBilled={{
240
                  monthly_price: getPhonesPrice(
241
                    props.runtimeData,
242
                    "yearly",
243
                    l10n,
244
                  ),
245
                  yearly_price: getPhonesYearlyPrice(
246
                    props.runtimeData,
247
                    "yearly",
248
                    l10n,
249
                  ),
250
                  subscribeLink: getPhoneSubscribeLink(
251
                    props.runtimeData,
252
                    "yearly",
253
                  ),
254
                  gaViewPing: {
255
                    category: "Purchase yearly Premium+phones button",
256
                    label: "plan-grid-phone-yearly-cta",
257
                  },
258
                  plan: {
259
                    plan: "phones",
260
                    billing_period: "yearly",
261
                  },
262
                }}
263
              />
264
            ) : null}
265
          </dd>
266
        </dl>
267

268
        <dl
269
          key={"premium"}
270
          className={styles.pricingCard}
271
          aria-label={l10n.getString("plan-grid-premium-title")}
272
        >
273
          <dt>
274
            <b>{l10n.getString("plan-grid-premium-title")}</b>
275
            <p>{l10n.getString("plan-grid-premium-subtitle")}</p>
276
          </dt>
277
          <dd key={"premium-feature-plus"}>
278
            <span className={styles.plusNote}>
279
              <PlusIcon2 alt={l10n.getString("plan-grid-card-premium-plus")} />
280
              <b>{l10n.getString("plan-grid-card-premium-plus")}</b>
281
            </span>
282
          </dd>
283
          <dd key={`premium-feature-1`}>
284
            <CheckIcon2 alt={""} />
285
            <span>
286
              {l10n.getFragment("plan-grid-card-premium-item-one", {
287
                elems: { b: <b /> },
288
              })}
289
            </span>
290
          </dd>
291
          <dd key={`premium-feature-2`}>
292
            <CheckIcon2 alt={""} />
293
            <span>
294
              {l10n.getFragment("plan-grid-card-premium-item-two", {
295
                elems: { b: <b /> },
296
              })}
297
            </span>
298
          </dd>
299
          <dd key={`premium-feature-3`}>
300
            <CheckIcon2 alt={""} />
301
            <span>
302
              {l10n.getFragment("plan-grid-card-premium-item-three", {
303
                elems: { b: <b /> },
304
              })}
305
            </span>
306
          </dd>
307
          <dd key={`premium-feature-4`}>
308
            <CheckIcon2 alt={""} />
309
            <span>
310
              {l10n.getFragment("plan-grid-card-premium-item-four", {
311
                elems: { b: <b /> },
312
              })}
313
            </span>
314
          </dd>
315
          <dd className={styles.pricingCardCta}>
316
            {isPeriodicalPremiumAvailableInCountry(props.runtimeData) ? (
15✔
317
              <PricingToggle
318
                monthlyBilled={{
319
                  monthly_price: getPeriodicalPremiumPrice(
320
                    props.runtimeData,
321
                    "monthly",
322
                    l10n,
323
                  ),
324
                  subscribeLink: getPeriodicalPremiumSubscribeLink(
325
                    props.runtimeData,
326
                    "monthly",
327
                  ),
328
                  gaViewPing: {
329
                    category: "Purchase monthly Premium button",
330
                    label: "plan-grid-premium-monthly-cta",
331
                  },
332
                  plan: {
333
                    plan: "premium",
334
                    billing_period: "monthly",
335
                  },
336
                }}
337
                yearlyBilled={{
338
                  monthly_price: getPeriodicalPremiumPrice(
339
                    props.runtimeData,
340
                    "yearly",
341
                    l10n,
342
                  ),
343
                  yearly_price: getPeriodicalPremiumYearlyPrice(
344
                    props.runtimeData,
345
                    "yearly",
346
                    l10n,
347
                  ),
348
                  subscribeLink: getPeriodicalPremiumSubscribeLink(
349
                    props.runtimeData,
350
                    "yearly",
351
                  ),
352
                  gaViewPing: {
353
                    category: "Purchase yearly Premium button",
354
                    label: "plan-grid-premium-yearly-cta",
355
                  },
356
                  plan: {
357
                    plan: "premium",
358
                    billing_period: "yearly",
359
                  },
360
                }}
361
              />
362
            ) : null}
363
          </dd>
364
        </dl>
365

366
        <dl
367
          key={"free"}
368
          className={styles.pricingCard}
369
          aria-label={l10n.getString("plan-grid-free-title")}
370
        >
371
          <dt>
372
            <b>{l10n.getString("plan-grid-free-title")}</b>
373
            <p>{l10n.getString("plan-matrix-heading-plan-free")}</p>
374
          </dt>
375
          <dd key={`free-feature-1`}>
376
            <CheckIcon2 alt={""} />
377
            <span>
378
              {l10n.getFragment("plan-grid-card-free-item-one", {
379
                vars: {
380
                  mask_limit: 5,
381
                },
382
                elems: { b: <b /> },
383
              })}
384
            </span>
385
          </dd>
386
          <dd key={`free-feature-2`}>
387
            <CheckIcon2 alt={""} />
388
            <span>
389
              {l10n.getFragment("plan-grid-card-free-item-two", {
390
                elems: { b: <b /> },
391
              })}
392
            </span>
393
          </dd>
394
          <dd key={`free-feature-3`}>
395
            <CheckIcon2 alt={""} />
396
            <span>
397
              {l10n.getFragment("plan-grid-card-free-item-three", {
398
                elems: { b: <b /> },
399
              })}
400
            </span>
401
          </dd>
402
          <dd className={styles.pricingCardCta}>
403
            <p>
404
              <strong>{l10n.getString("plan-matrix-price-free")}</strong>
405
            </p>
406
            <LinkButton
407
              ref={freeButtonRef}
408
              href={`${getRuntimeConfig().fxaLoginUrl}&auth_params=${
409
                typeof window !== "undefined"
15!
410
                  ? encodeURIComponent(window.location.search.slice(1))
411
                  : ""
412
              }`}
UNCOV
413
              onClick={() => countSignIn("plan-grid-free-cta")}
×
414
              className={styles["pick-button"]}
415
              disabled={isLoggedIn === "logged-in"}
416
            >
417
              {isLoggedIn === "logged-in"
15✔
418
                ? l10n.getString("plan-matrix-your-plan")
419
                : l10n.getString("plan-grid-card-btn")}
420
            </LinkButton>
421
          </dd>
422
        </dl>
423
      </section>
424
    </div>
425
  );
426
};
427

428
type PricingToggleProps = {
429
  yearlyBilled: {
430
    monthly_price: string;
431
    yearly_price: string;
432
    subscribeLink: string;
433
    gaViewPing: Parameters<typeof useGaViewPing>[0];
434
    plan: Plan;
435
  };
436
  monthlyBilled: {
437
    monthly_price: string;
438
    subscribeLink: string;
439
    gaViewPing: Parameters<typeof useGaViewPing>[0];
440
    plan: Plan;
441
  };
442
};
443

444
const PricingToggle = (props: PricingToggleProps) => {
3✔
445
  const l10n = useL10n();
27✔
446
  const gaEvent = useGaEvent();
27✔
447
  const yearlyButtonRef = useGaViewPing(props.yearlyBilled.gaViewPing);
27✔
448
  const monthlyButtonRef = useGaViewPing(props.monthlyBilled.gaViewPing);
27✔
449

450
  return (
451
    <PricingTabs defaultSelectedKey="yearly">
452
      <Item
453
        key="yearly"
454
        title={l10n.getString("plan-matrix-price-period-yearly")}
455
      >
456
        <div className={styles["price-text"]}>
457
          <small>
458
            {l10n.getString("plan-matrix-price-yearly-calculated", {
459
              yearly_price: props.yearlyBilled.yearly_price,
460
            })}
461
          </small>
462
          <span className={styles.price}>
463
            {l10n.getString("plan-matrix-price-monthly-calculated", {
464
              monthly_price: props.yearlyBilled.monthly_price,
465
            })}
466
          </span>
467
        </div>
468
        <a
469
          ref={yearlyButtonRef}
470
          href={props.yearlyBilled.subscribeLink}
471
          onClick={() =>
472
            trackPlanPurchaseStart(gaEvent, props.yearlyBilled.plan, {
×
473
              label: props.yearlyBilled.gaViewPing?.label,
474
            })
475
          }
476
          tabIndex={0}
477
          className={styles["pick-button"]}
478
          data-testid={`plan-cta-${props.yearlyBilled.plan.plan}-yearly`}
479
        >
480
          {l10n.getString("plan-grid-card-btn")}
481
        </a>
482
      </Item>
483
      <Item
484
        key="monthly"
485
        title={l10n.getString("plan-matrix-price-period-monthly")}
486
      >
487
        <div className={styles["price-text"]}>
488
          <small>{l10n.getString("plan-grid-billed-monthly")}</small>
489
          <span className={styles.price}>
490
            {l10n.getString("plan-matrix-price-monthly-calculated", {
491
              monthly_price: props.monthlyBilled.monthly_price,
492
            })}
493
          </span>
494
        </div>
495
        <a
496
          ref={monthlyButtonRef}
497
          href={props.monthlyBilled.subscribeLink}
498
          onClick={() =>
499
            trackPlanPurchaseStart(
×
500
              gaEvent,
501
              { plan: "megabundle" },
502
              {
503
                label: props.monthlyBilled.gaViewPing?.label,
504
              },
505
            )
506
          }
507
          tabIndex={0}
508
          className={styles["pick-button"]}
509
          data-testid={`plan-cta-${props.monthlyBilled.plan.plan}-monthly`}
510
        >
511
          {l10n.getString("plan-grid-card-btn")}
512
        </a>
513
      </Item>
514
    </PricingTabs>
515
  );
516
};
517

518
const PricingTabs = (props: TabListProps<object>) => {
3✔
519
  const tabListState = useTabListState(props);
81✔
520
  const tabListRef = useRef(null);
81✔
521
  const { tabListProps } = useTabList(props, tabListState, tabListRef);
81✔
522
  const tabPanelRef = useRef(null);
81✔
523
  const { tabPanelProps } = useTabPanel({}, tabListState, tabPanelRef);
81✔
524

525
  return (
526
    <div className={styles.pricing}>
527
      <div className={styles["pricing-toggle-wrapper"]}>
528
        <div
529
          {...tabListProps}
530
          ref={tabListRef}
531
          className={styles["pricing-toggle"]}
532
        >
533
          {Array.from(tabListState.collection).map((item) => (
534
            <PricingTab key={item.key} item={item} state={tabListState} />
162✔
535
          ))}
536
        </div>
537
      </div>
538
      <div
539
        {...tabPanelProps}
540
        ref={tabPanelRef}
541
        className={styles["pricing-overview"]}
542
      >
543
        {tabListState.selectedItem?.props.children}
544
      </div>
545
    </div>
546
  );
547
};
548

549
const PricingTab = (props: {
3✔
550
  state: TabListState<object>;
551
  item: { key: Parameters<typeof useTab>[0]["key"]; rendered: ReactNode };
552
}) => {
553
  const tabRef = useRef(null);
108✔
554
  const { tabProps } = useTab({ key: props.item.key }, props.state, tabRef);
108✔
555
  return (
556
    <div
557
      {...tabProps}
558
      ref={tabRef}
559
      className={
560
        props.state.selectedKey === props.item.key ? styles["is-selected"] : ""
108✔
561
      }
562
    >
563
      {props.item.rendered}
564
    </div>
565
  );
566
};
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