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

mozilla / fx-private-relay / f69b9b96-11e8-4734-af81-880c37a3b16c

29 May 2025 05:59PM UTC coverage: 85.638% (+0.06%) from 85.583%
f69b9b96-11e8-4734-af81-880c37a3b16c

Pull #5603

circleci

vpremamozilla
fixed linting errors
Pull Request #5603: MPP-4165 Megabundle Pricing Grid

2516 of 3650 branches covered (68.93%)

Branch coverage included in aggregate %.

65 of 69 new or added lines in 5 files covered. (94.2%)

1 existing line in 1 file now uncovered.

17662 of 19912 relevant lines covered (88.7%)

9.65 hits per line

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

92.45
/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
  getIndividualBundlePrice,
24
  getBundleDiscountPercentage,
25
} from "../../functions/getPlan";
3✔
26
import { RuntimeData } from "../../hooks/api/runtimeData";
27
import {
28
  CheckIcon2,
29
  VpnIcon,
30
  PlusIcon2,
31
  MonitorIcon,
32
  RelayIcon,
33
} from "../Icons";
3✔
34
import { getRuntimeConfig } from "../../config";
3✔
35
import { useGaEvent } from "../../hooks/gaEvent";
3✔
36
import { useGaViewPing } from "../../hooks/gaViewPing";
3✔
37
import { Plan, trackPlanPurchaseStart } from "../../functions/trackPurchase";
3✔
38
import { setCookie } from "../../functions/cookies";
3✔
39
import { useL10n } from "../../hooks/l10n";
3✔
40
import { LinkButton } from "../Button";
3✔
41
import { useIsLoggedIn } from "../../hooks/session";
3✔
42
import { getLocale } from "../../functions/getLocale";
3✔
43

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

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

54
  const freeButtonDesktopRef = useGaViewPing({
15✔
55
    category: "Sign In",
56
    label: "plan-matrix-free-cta-desktop",
57
  });
58

59
  const gaEvent = useGaEvent();
15✔
60

61
  const countSignIn = (label: string) => {
15✔
NEW
62
    gaEvent({
×
63
      category: "Sign In",
64
      action: "Engage",
65
      label: label,
66
    });
NEW
67
    setCookie("user-sign-in", "true", { maxAgeInSeconds: 60 * 60 });
×
68
  };
69

70
  const isLoggedIn = useIsLoggedIn();
15✔
71

72
  const formatter = new Intl.NumberFormat(getLocale(l10n), {
15✔
73
    style: "currency",
74
    currency: "USD",
75
  });
76

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

243
        <dl
244
          key={"premium"}
245
          className={styles.pricingCard}
246
          aria-label={l10n.getString("plan-grid-premium-title")}
247
        >
248
          <dt>
249
            <b>{l10n.getString("plan-grid-premium-title")}</b>
250
            <p>{l10n.getString("plan-grid-premium-subtitle")}</p>
251
          </dt>
252
          <dd key={"premium-feature-plus"}>
253
            <span className={styles.plusNote}>
254
              <PlusIcon2 alt={l10n.getString("plan-grid-card-premium-plus")} />
255
              <b>{l10n.getString("plan-grid-card-premium-plus")}</b>
256
            </span>
257
          </dd>
258
          <dd key={`premium-feature-1`}>
259
            <CheckIcon2 alt={""} />
260
            <span>
261
              {l10n.getFragment("plan-grid-card-premium-item-one", {
262
                elems: { b: <b /> },
263
              })}
264
            </span>
265
          </dd>
266
          <dd key={`premium-feature-2`}>
267
            <CheckIcon2 alt={""} />
268
            <span>
269
              {l10n.getFragment("plan-grid-card-premium-item-two", {
270
                elems: { b: <b /> },
271
              })}
272
            </span>
273
          </dd>
274
          <dd key={`premium-feature-3`}>
275
            <CheckIcon2 alt={""} />
276
            <span>
277
              {l10n.getFragment("plan-grid-card-premium-item-three", {
278
                elems: { b: <b /> },
279
              })}
280
            </span>
281
          </dd>
282
          <dd key={`premium-feature-4`}>
283
            <CheckIcon2 alt={""} />
284
            <span>
285
              {l10n.getFragment("plan-grid-card-premium-item-four", {
286
                elems: { b: <b /> },
287
              })}
288
            </span>
289
          </dd>
290
          <dd className={styles.pricingCardCta}>
291
            {isPeriodicalPremiumAvailableInCountry(props.runtimeData) ? (
15✔
292
              <PricingToggle
293
                monthlyBilled={{
294
                  monthly_price: getPeriodicalPremiumPrice(
295
                    props.runtimeData,
296
                    "monthly",
297
                    l10n,
298
                  ),
299
                  subscribeLink: getPeriodicalPremiumSubscribeLink(
300
                    props.runtimeData,
301
                    "monthly",
302
                  ),
303
                  gaViewPing: {
304
                    category: "Purchase monthly Premium button",
305
                    label: "plan-matrix-premium-monthly-cta-desktop",
306
                  },
307
                  plan: {
308
                    plan: "premium",
309
                    billing_period: "monthly",
310
                  },
311
                }}
312
                yearlyBilled={{
313
                  monthly_price: getPeriodicalPremiumPrice(
314
                    props.runtimeData,
315
                    "yearly",
316
                    l10n,
317
                  ),
318
                  yearly_price: getPeriodicalPremiumYearlyPrice(
319
                    props.runtimeData,
320
                    "yearly",
321
                    l10n,
322
                  ),
323
                  subscribeLink: getPeriodicalPremiumSubscribeLink(
324
                    props.runtimeData,
325
                    "yearly",
326
                  ),
327
                  gaViewPing: {
328
                    category: "Purchase yearly Premium button",
329
                    label: "plan-matrix-premium-yearly-cta-desktop",
330
                  },
331
                  plan: {
332
                    plan: "premium",
333
                    billing_period: "yearly",
334
                  },
335
                }}
336
              />
337
            ) : null}
338
          </dd>
339
        </dl>
340

341
        <dl
342
          key={"free"}
343
          className={styles.pricingCard}
344
          aria-label={l10n.getString("plan-grid-free-title")}
345
        >
346
          <dt>
347
            <b>{l10n.getString("plan-grid-free-title")}</b>
348
            <p>{l10n.getString("plan-matrix-heading-plan-free")}</p>
349
          </dt>
350
          <dd key={`free-feature-1`}>
351
            <CheckIcon2 alt={""} />
352
            <span>
353
              {l10n.getFragment("plan-grid-card-free-item-one", {
354
                vars: {
355
                  mask_limit: 5,
356
                },
357
                elems: { b: <b /> },
358
              })}
359
            </span>
360
          </dd>
361
          <dd key={`free-feature-2`}>
362
            <CheckIcon2 alt={""} />
363
            <span>
364
              {l10n.getFragment("plan-grid-card-free-item-two", {
365
                elems: { b: <b /> },
366
              })}
367
            </span>
368
          </dd>
369
          <dd key={`free-feature-3`}>
370
            <CheckIcon2 alt={""} />
371
            <span>
372
              {l10n.getFragment("plan-grid-card-free-item-three", {
373
                elems: { b: <b /> },
374
              })}
375
            </span>
376
          </dd>
377
          <dd className={styles.pricingCardCta}>
378
            <p>
379
              <strong>{l10n.getString("plan-matrix-price-free")}</strong>
380
            </p>
381
            <LinkButton
382
              ref={freeButtonDesktopRef}
383
              href={getRuntimeConfig().fxaLoginUrl}
NEW
384
              onClick={() => countSignIn("plan-matrix-free-cta-desktop")}
×
385
              className={styles["pick-button"]}
386
              disabled={isLoggedIn === "logged-in"}
387
            >
388
              {isLoggedIn === "logged-in"
15✔
389
                ? l10n.getString("plan-matrix-your-plan")
390
                : l10n.getString("plan-grid-card-btn")}
391
            </LinkButton>
392
          </dd>
393
        </dl>
394
      </section>
395
    </div>
396
  );
397
};
398

399
type PricingToggleProps = {
400
  yearlyBilled: {
401
    monthly_price: string;
402
    yearly_price: string;
403
    subscribeLink: string;
404
    gaViewPing: Parameters<typeof useGaViewPing>[0];
405
    plan: Plan;
406
  };
407
  monthlyBilled: {
408
    monthly_price: string;
409
    subscribeLink: string;
410
    gaViewPing: Parameters<typeof useGaViewPing>[0];
411
    plan: Plan;
412
  };
413
};
414
const PricingToggle = (props: PricingToggleProps) => {
3✔
415
  const l10n = useL10n();
27✔
416
  const gaEvent = useGaEvent();
27✔
417
  const yearlyButtonRef = useGaViewPing(props.yearlyBilled.gaViewPing);
27✔
418
  const monthlyButtonRef = useGaViewPing(props.monthlyBilled.gaViewPing);
27✔
419

420
  return (
421
    <PricingTabs defaultSelectedKey="yearly">
422
      <Item
423
        key="yearly"
424
        title={l10n.getString("plan-matrix-price-period-yearly")}
425
      >
426
        <div className={styles["price-text"]}>
427
          <small>
428
            {l10n.getString("plan-matrix-price-yearly-calculated", {
429
              yearly_price: props.yearlyBilled.yearly_price,
430
            })}
431
          </small>
432
          <span className={styles.price}>
433
            {l10n.getString("plan-matrix-price-monthly-calculated", {
434
              monthly_price: props.yearlyBilled.monthly_price,
435
            })}
436
          </span>
437
        </div>
438
        <a
439
          ref={yearlyButtonRef}
440
          href={props.yearlyBilled.subscribeLink}
441
          onClick={() =>
442
            trackPlanPurchaseStart(gaEvent, props.yearlyBilled.plan, {
1✔
443
              label: props.yearlyBilled.gaViewPing?.label,
444
            })
445
          }
446
          tabIndex={0}
447
          className={styles["pick-button"]}
448
        >
449
          {l10n.getString("plan-grid-card-btn")}
450
        </a>
451
      </Item>
452
      <Item
453
        key="monthly"
454
        title={l10n.getString("plan-matrix-price-period-monthly")}
455
      >
456
        <div className={styles["price-text"]}>
457
          <small>{l10n.getString("plan-grid-billed-monthly")}</small>
458
          <span className={styles.price}>
459
            {l10n.getString("plan-matrix-price-monthly-calculated", {
460
              monthly_price: props.monthlyBilled.monthly_price,
461
            })}
462
          </span>
463
        </div>
464
        <a
465
          ref={monthlyButtonRef}
466
          href={props.monthlyBilled.subscribeLink}
467
          onClick={() =>
NEW
468
            trackPlanPurchaseStart(gaEvent, props.monthlyBilled.plan, {
×
469
              label: props.monthlyBilled.gaViewPing?.label,
470
            })
471
          }
472
          tabIndex={0}
473
          className={styles["pick-button"]}
474
        >
475
          {l10n.getString("plan-grid-card-btn")}
476
        </a>
477
      </Item>
478
    </PricingTabs>
479
  );
480
};
481

482
const PricingTabs = (props: TabListProps<object>) => {
3✔
483
  const tabListState = useTabListState(props);
81✔
484
  const tabListRef = useRef(null);
81✔
485
  const { tabListProps } = useTabList(props, tabListState, tabListRef);
81✔
486
  const tabPanelRef = useRef(null);
81✔
487
  const { tabPanelProps } = useTabPanel({}, tabListState, tabPanelRef);
81✔
488

489
  return (
490
    <div className={styles.pricing}>
491
      <div className={styles["pricing-toggle-wrapper"]}>
492
        <div
493
          {...tabListProps}
494
          ref={tabListRef}
495
          className={styles["pricing-toggle"]}
496
        >
497
          {Array.from(tabListState.collection).map((item) => (
498
            <PricingTab key={item.key} item={item} state={tabListState} />
162✔
499
          ))}
500
        </div>
501
      </div>
502
      <div
503
        {...tabPanelProps}
504
        ref={tabPanelRef}
505
        className={styles["pricing-overview"]}
506
      >
507
        {tabListState.selectedItem?.props.children}
508
      </div>
509
    </div>
510
  );
511
};
512

513
const PricingTab = (props: {
3✔
514
  state: TabListState<object>;
515
  item: { key: Parameters<typeof useTab>[0]["key"]; rendered: ReactNode };
516
}) => {
517
  const tabRef = useRef(null);
108✔
518
  const { tabProps } = useTab({ key: props.item.key }, props.state, tabRef);
108✔
519
  return (
520
    <div
521
      {...tabProps}
522
      ref={tabRef}
523
      className={
524
        props.state.selectedKey === props.item.key ? styles["is-selected"] : ""
108✔
525
      }
526
    >
527
      {props.item.rendered}
528
    </div>
529
  );
530
};
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