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

mozilla / fx-private-relay / e6596aaf-157d-4581-acf2-f4e4f97a1130

21 Sep 2023 08:17PM CUT coverage: 74.433% (-0.1%) from 74.552%
e6596aaf-157d-4581-acf2-f4e4f97a1130

push

circleci

web-flow
Merge pull request #3907 from mozilla/fix-ga-events-MPP-3422

Fix ga events mpp 3422

1895 of 2761 branches covered (0.0%)

Branch coverage included in aggregate %.

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

5977 of 7815 relevant lines covered (76.48%)

18.39 hits per line

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

72.93
/frontend/src/pages/accounts/profile.page.tsx
1
import type { NextPage } from "next";
2
import Image from "next/image";
1✔
3
import {
4
  forwardRef,
5
  HTMLAttributes,
6
  ReactNode,
7
  RefObject,
8
  useRef,
9
} from "react";
1✔
10
import {
11
  FocusScope,
12
  mergeProps,
13
  useButton,
14
  useOverlay,
15
  useOverlayPosition,
16
  useOverlayTrigger,
17
  useTooltip,
18
  useTooltipTrigger,
19
} from "react-aria";
1✔
20
import { event as gaEvent } from "react-ga";
1✔
21
import { useMenuTriggerState, useTooltipTriggerState } from "react-stately";
1✔
22
import { toast } from "react-toastify";
1✔
23
import styles from "./profile.module.scss";
1✔
24
import BottomBannerIllustration from "../../../public/images/woman-couch-left.svg";
1✔
25
import UpsellBannerUs from "./images/upsell-banner-us.svg";
1✔
26
import UpsellBannerNonUs from "./images/upsell-banner-nonus.svg";
1✔
27
import { CheckBadgeIcon, LockIcon, PencilIcon } from "../../components/Icons";
1✔
28
import { Layout } from "../../components/layout/Layout";
1✔
29
import { useProfiles } from "../../hooks/api/profile";
1✔
30
import {
31
  AliasData,
32
  getAllAliases,
33
  getFullAddress,
34
  useAliases,
35
} from "../../hooks/api/aliases";
1✔
36
import { useUsers } from "../../hooks/api/user";
1✔
37
import { AliasList } from "../../components/dashboard/aliases/AliasList";
1✔
38
import { ProfileBanners } from "../../components/dashboard/ProfileBanners";
1✔
39
import { LinkButton } from "../../components/Button";
1✔
40
import { useRuntimeData } from "../../hooks/api/runtimeData";
1✔
41
import {
42
  isPeriodicalPremiumAvailableInCountry,
43
  isPhonesAvailableInCountry,
44
} from "../../functions/getPlan";
1✔
45
import { useGaViewPing } from "../../hooks/gaViewPing";
1✔
46
import { PremiumOnboarding } from "../../components/dashboard/PremiumOnboarding";
1✔
47
import { Onboarding } from "../../components/dashboard/Onboarding";
1✔
48
import { getRuntimeConfig } from "../../config";
1✔
49
import { Tips } from "../../components/dashboard/tips/Tips";
1✔
50
import { getLocale } from "../../functions/getLocale";
1✔
51
import { AddonData } from "../../components/dashboard/AddonData";
1✔
52
import { useAddonData } from "../../hooks/addon";
1✔
53
import { CloseIcon } from "../../components/Icons";
54
import { isFlagActive } from "../../functions/waffle";
1✔
55
import { DashboardSwitcher } from "../../components/layout/navigation/DashboardSwitcher";
1✔
56
import { usePurchaseTracker } from "../../hooks/purchaseTracker";
1✔
57
import { PremiumPromoBanners } from "../../components/dashboard/PremiumPromoBanners";
1✔
58
import { useL10n } from "../../hooks/l10n";
1✔
59
import { Localized } from "../../components/Localized";
1✔
60
import { clearCookie, getCookie, setCookie } from "../../functions/cookies";
1✔
61
import { SubdomainInfoTooltip } from "../../components/dashboard/subdomain/SubdomainInfoTooltip";
1✔
62
import Link from "next/link";
1✔
63

64
const Profile: NextPage = () => {
1✔
65
  const runtimeData = useRuntimeData();
48✔
66
  const profileData = useProfiles();
48✔
67
  const userData = useUsers();
48✔
68
  const aliasData = useAliases();
48✔
69
  const addonData = useAddonData();
48✔
70
  const l10n = useL10n();
48✔
71
  const bottomBannerSubscriptionLinkRef = useGaViewPing({
48✔
72
    category: "Purchase Button",
73
    label: "profile-bottom-promo",
74
  });
75
  const setCustomDomainLinkRef = useGaViewPing({
48✔
76
    category: "Purchase Button",
77
    label: "profile-set-custom-domain",
78
  });
79
  const hash = getCookie("profile-location-hash");
48✔
80
  if (hash) {
48!
81
    document.location.hash = hash;
×
82
    clearCookie("profile-location-hash");
×
83
  }
84
  usePurchaseTracker(profileData.data?.[0]);
48✔
85

86
  if (!userData.isValidating && userData.error) {
47!
87
    if (document.location.hash) {
×
88
      setCookie("profile-location-hash", document.location.hash);
×
89
    }
90
    // Add url params to auth_params so django-allauth will send them to FXA
91
    const originalUrlParams = document.location.search.replace("?", "");
×
92
    const fxaLoginWithAuthParams =
93
      getRuntimeConfig().fxaLoginUrl +
×
94
      "&auth_params=" +
95
      encodeURIComponent(originalUrlParams);
96
    document.location.assign(fxaLoginWithAuthParams);
×
97
  }
98

99
  const profile = profileData.data?.[0];
47✔
100
  const user = userData.data?.[0];
47✔
101
  if (
47!
102
    !runtimeData.data ||
235✔
103
    !profile ||
104
    !user ||
105
    !aliasData.randomAliasData.data ||
106
    !aliasData.customAliasData.data
107
  ) {
108
    // TODO: Show a loading spinner?
109
    return null;
×
110
  }
111

112
  const setCustomSubdomain = async (customSubdomain: string) => {
47✔
113
    const response = await profileData.setSubdomain(customSubdomain);
×
114
    if (!response.ok) {
×
115
      toast(
×
116
        l10n.getString("error-subdomain-not-available-2", {
117
          unavailable_subdomain: customSubdomain,
118
        }),
119
        { type: "error" },
120
      );
121
    }
122
    addonData.sendEvent("subdomainClaimed", { subdomain: customSubdomain });
×
123
  };
124

125
  const allAliases = getAllAliases(
47✔
126
    aliasData.randomAliasData.data,
127
    aliasData.customAliasData.data,
128
  );
129

130
  if (
47✔
131
    profile.has_premium &&
72✔
132
    profile.onboarding_state < getRuntimeConfig().maxOnboardingAvailable
133
  ) {
134
    const onNextStep = (step: number) => {
7✔
135
      profileData.update(profile.id, {
2✔
136
        onboarding_state: step,
137
      });
138
    };
139

140
    return (
141
      <>
142
        <AddonData
143
          aliases={allAliases}
144
          profile={profile}
145
          runtimeData={runtimeData.data}
146
          totalBlockedEmails={profile.emails_blocked}
147
          totalForwardedEmails={profile.emails_forwarded}
148
          totalEmailTrackersRemoved={profile.level_one_trackers_blocked}
149
        />
150
        <Layout runtimeData={runtimeData.data}>
151
          {isPhonesAvailableInCountry(runtimeData.data) ? (
7!
152
            <DashboardSwitcher />
153
          ) : null}
154
          <PremiumOnboarding
155
            profile={profile}
156
            onNextStep={onNextStep}
157
            onPickSubdomain={setCustomSubdomain}
158
          />
159
        </Layout>
160
      </>
161
    );
162
  }
163

164
  const createAlias = async (
40✔
165
    options:
166
      | { mask_type: "random" }
167
      | { mask_type: "custom"; address: string; blockPromotionals: boolean },
168
  ) => {
169
    try {
1✔
170
      const response = await aliasData.create(options);
1✔
171
      if (!response.ok) {
1!
172
        throw new Error(
×
173
          "Immediately caught to land in the same code path as failed requests.",
174
        );
175
      }
176
      addonData.sendEvent("aliasListUpdate");
1✔
177
    } catch (error) {
178
      toast(l10n.getString("error-mask-create-failed"), { type: "error" });
×
179
    }
180
  };
181

182
  const updateAlias = async (
40✔
183
    alias: AliasData,
184
    updatedFields: Partial<AliasData>,
185
  ) => {
186
    try {
1✔
187
      const response = await aliasData.update(alias, updatedFields);
1✔
188
      if (!response.ok) {
1!
189
        throw new Error(
×
190
          "Immediately caught to land in the same code path as failed requests.",
191
        );
192
      }
193
    } catch (error) {
194
      toast(
1✔
195
        l10n.getString("error-mask-update-failed", {
196
          alias: getFullAddress(alias),
197
        }),
198
        { type: "error" },
199
      );
200
    }
201
  };
202

203
  const deleteAlias = async (alias: AliasData) => {
40✔
204
    try {
1✔
205
      const response = await aliasData.delete(alias);
1✔
206
      if (!response.ok) {
1!
207
        throw new Error(
×
208
          "Immediately caught to land in the same code path as failed requests.",
209
        );
210
      }
211
      addonData.sendEvent("aliasListUpdate");
1✔
212
    } catch (error: unknown) {
213
      toast(
×
214
        l10n.getString("error-mask-delete-failed", {
215
          alias: getFullAddress(alias),
216
        }),
217
        { type: "error" },
218
      );
219
    }
220
  };
221

222
  const freeMaskLimit = getRuntimeConfig().maxFreeAliases;
40✔
223
  const freeMaskLimitReached =
224
    allAliases.length >= freeMaskLimit && !profile.has_premium;
40✔
225

226
  const subdomainMessage =
227
    typeof profile.subdomain === "string" ? (
228
      <>
2✔
229
        <span>{l10n.getString("profile-label-custom-domain")}</span>
230
        <span className={styles["profile-registered-domain-value"]}>
231
          @{profile.subdomain}.{getRuntimeConfig().mozmailDomain}
232
        </span>
233
      </>
234
    ) : profile.has_premium ? (
235
      <a className={styles["open-button"]} href="#mpp-choose-subdomain">
16✔
236
        {l10n.getString("profile-label-set-your-custom-domain-free-user")}
237
      </a>
238
    ) : (
239
      <Link
240
        className={styles["open-button"]}
241
        href={"/premium#pricing"}
242
        ref={setCustomDomainLinkRef}
243
        onClick={() => {
244
          gaEvent({
×
245
            category: "Purchase Button",
246
            action: "Engage",
247
            label: "profile-set-custom-domain",
248
          });
249
        }}
250
      >
251
        {l10n.getString("profile-label-set-your-custom-domain-free-user")}
252
      </Link>
253
    );
254

255
  const numberFormatter = new Intl.NumberFormat(getLocale(l10n), {
40✔
256
    notation: "compact",
257
    compactDisplay: "short",
258
  });
259

260
  type TooltipProps = {
261
    children: ReactNode;
262
  };
263

264
  const MaxedMasksTooltip = (props: TooltipProps) => {
40✔
265
    const l10n = useL10n();
2✔
266
    const triggerState = useTooltipTriggerState({ delay: 0 });
2✔
267
    const triggerRef = useRef<HTMLSpanElement>(null);
2✔
268
    const tooltipTrigger = useTooltipTrigger({}, triggerState, triggerRef);
2✔
269
    const { tooltipProps } = useTooltip({}, triggerState);
2✔
270

271
    return (
272
      <div className={styles["stat-wrapper"]}>
273
        <span
274
          ref={triggerRef}
275
          {...tooltipTrigger.triggerProps}
276
          className={`${styles.stat} ${styles["forwarded-stat"]}`}
277
        >
278
          {props.children}
279
        </span>
280
        {triggerState.isOpen && (
2✔
281
          <div
282
            {...mergeProps(tooltipTrigger.tooltipProps, tooltipProps)}
283
            className={styles.tooltip}
284
          >
285
            <p>
286
              {l10n.getString("profile-maxed-aliases-tooltip", {
287
                limit: freeMaskLimit,
288
              })}
289
            </p>
290
          </div>
291
        )}
292
      </div>
293
    );
294
  };
295

296
  // Show stats for free users and premium users
297
  const stats = (
298
    <section className={styles.header}>
299
      <div className={styles["header-wrapper"]}>
300
        <div className={styles["user-details"]}>
301
          <Localized
302
            id="profile-label-welcome-html"
303
            vars={{
304
              email: user.email,
305
            }}
306
            elems={{
307
              span: <span className={styles.lead} />,
308
            }}
309
          >
310
            <span className={styles.greeting} />
311
          </Localized>
312
          <strong className={styles.subdomain}>
313
            {/* render check badge if subdomain is set and user has premium
314
            render pencil icon if subdomain is not set but user has premium
315
            render lock icon by default */}
316
            {typeof profile.subdomain === "string" && profile.has_premium ? (
42✔
317
              <CheckBadgeIcon alt="" />
2✔
318
            ) : typeof profile.subdomain !== "string" && profile.has_premium ? (
76✔
319
              <PencilIcon alt="" className={styles["pencil-icon"]} />
16✔
320
            ) : (
321
              <LockIcon alt="" className={styles["lock-icon"]} />
322
            )}
323
            {subdomainMessage}
324
            <SubdomainInfoTooltip hasPremium={profile.has_premium} />
325
          </strong>
326
        </div>
327
        <dl className={styles["account-stats"]}>
328
          <div className={styles.stat}>
329
            <dt className={styles.label}>
330
              {l10n.getString("profile-stat-label-aliases-used-2")}
331
            </dt>
332
            {/* If premium is available in the user's country and 
333
            the user has reached their free mask limit and 
334
            they are a free user, show the maxed masks tooltip */}
335
            {isPeriodicalPremiumAvailableInCountry(runtimeData.data) &&
78✔
336
            freeMaskLimitReached ? (
337
              <dd className={`${styles.value} ${styles.maxed}`}>
2✔
338
                <MaxedMasksTooltip>
339
                  {numberFormatter.format(allAliases.length)}
340
                </MaxedMasksTooltip>
341
              </dd>
342
            ) : (
343
              <dd className={`${styles.value}`}>
344
                {numberFormatter.format(allAliases.length)}
345
              </dd>
346
            )}
347
          </div>
348
          <div className={styles.stat}>
349
            <dt className={styles.label}>
350
              {l10n.getString("profile-stat-label-blocked")}
351
            </dt>
352
            <dd className={styles.value}>
353
              {numberFormatter.format(profile.emails_blocked)}
354
            </dd>
355
          </div>
356
          <div className={styles.stat}>
357
            <dt className={styles.label}>
358
              {l10n.getString("profile-stat-label-forwarded")}
359
            </dt>
360
            <dd className={styles.value}>
361
              {numberFormatter.format(profile.emails_forwarded)}
362
            </dd>
363
          </div>
364
          {/*
365
            Only show tracker blocking stats if the back-end provides them:
366
          */}
367
          {isFlagActive(runtimeData.data, "tracker_removal") &&
40!
368
            typeof profile.level_one_trackers_blocked === "number" && (
369
              <div className={styles.stat}>
370
                <dt className={styles.label}>
371
                  {l10n.getString("profile-stat-label-trackers-removed")}
372
                </dt>
373
                <dd className={styles.value}>
374
                  {numberFormatter.format(profile.level_one_trackers_blocked)}
375
                  <StatExplainer>
376
                    <p>
377
                      {l10n.getString(
378
                        "profile-stat-label-trackers-learn-more-part1",
379
                      )}
380
                    </p>
381
                    <p>
382
                      {l10n.getString(
383
                        "profile-stat-label-trackers-learn-more-part2-2",
384
                      )}
385
                    </p>
386
                  </StatExplainer>
387
                </dd>
388
              </div>
389
            )}
390
        </dl>
391
      </div>
392
    </section>
393
  );
394

395
  const bottomPremiumSection =
396
    profile.has_premium ||
62✔
397
    !isPeriodicalPremiumAvailableInCountry(runtimeData.data) ? null : (
20✔
398
      <section className={styles["bottom-banner"]}>
399
        <div className={styles["bottom-banner-wrapper"]}>
400
          <div className={styles["bottom-banner-content"]}>
401
            {isPhonesAvailableInCountry(runtimeData.data) ? (
402
              <>
×
403
                <Localized
404
                  id="footer-banner-premium-promo-headine"
405
                  elems={{ strong: <strong />, i: <i /> }}
406
                >
407
                  <h3 />
408
                </Localized>
409
                <p>{l10n.getString("footer-banner-premium-promo-body")}</p>
410
              </>
411
            ) : (
412
              <>
413
                <Localized
414
                  id="banner-pack-upgrade-headline-2-html"
415
                  elems={{ strong: <strong /> }}
416
                >
417
                  <h3 />
418
                </Localized>
419
                <p>{l10n.getString("banner-pack-upgrade-copy-2")}</p>
420
              </>
421
            )}
422

423
            <LinkButton
424
              href="/premium#pricing"
425
              ref={bottomBannerSubscriptionLinkRef}
426
              onClick={() => {
427
                gaEvent({
×
428
                  category: "Purchase Button",
429
                  action: "Engage",
430
                  label: "profile-bottom-promo",
431
                });
432
              }}
433
            >
434
              {l10n.getString("banner-pack-upgrade-cta")}
435
            </LinkButton>
436
          </div>
437
          <Image src={BottomBannerIllustration} alt="" />
438
        </div>
439
      </section>
440
    );
441

442
  const banners = (
443
    <section className={styles["banners-wrapper"]}>
444
      {!profile.has_premium &&
122!
445
      isPeriodicalPremiumAvailableInCountry(runtimeData.data) &&
446
      isFlagActive(runtimeData.data, "premium_promo_banners") ? (
447
        <PremiumPromoBanners />
448
      ) : null}
449
      <ProfileBanners
450
        profile={profile}
451
        user={user}
452
        onCreateSubdomain={setCustomSubdomain}
453
        runtimeData={runtimeData.data}
454
        aliases={allAliases}
455
      />
456
    </section>
457
  );
458
  const topBanners = allAliases.length > 0 ? banners : null;
40✔
459
  const bottomBanners = allAliases.length === 0 ? banners : null;
40✔
460

461
  // Render the upsell banner when a user has reached the free mask limit
462
  const UpsellBanner = () => (
463
    <div className={styles["upsell-banner"]}>
464
      <div className={styles["upsell-banner-wrapper"]}>
465
        <div className={styles["upsell-banner-content"]}>
466
          <p className={styles["upsell-banner-header"]}>
467
            {isPhonesAvailableInCountry(runtimeData.data)
2!
468
              ? l10n.getString("profile-maxed-aliases-with-phone-header")
469
              : l10n.getString("profile-maxed-aliases-without-phone-header")}
470
          </p>
471
          <p className={styles["upsell-banner-description"]}>
472
            {l10n.getString(
473
              isPhonesAvailableInCountry(runtimeData.data)
2!
474
                ? "profile-maxed-aliases-with-phone-description"
475
                : "profile-maxed-aliases-without-phone-description",
476
              {
477
                limit: freeMaskLimit,
478
              },
479
            )}
480
          </p>
481
          <LinkButton
482
            href="/premium#pricing"
483
            ref={useGaViewPing({
484
              category: "Purchase Button",
485
              label: "upgrade-premium-header-mask-limit",
486
            })}
487
            onClick={() => {
488
              gaEvent({
×
489
                category: "Purchase Button",
490
                action: "Engage",
491
                label: "upgrade-premium-header-mask-limit",
492
              });
493
            }}
494
          >
495
            {l10n.getString("profile-maxed-aliases-cta")}
496
          </LinkButton>
497
        </div>
498
        <Image
499
          className={styles["upsell-banner-image"]}
500
          src={
501
            isPhonesAvailableInCountry(runtimeData.data)
2!
502
              ? UpsellBannerUs
503
              : UpsellBannerNonUs
504
          }
505
          alt=""
506
        />
507
      </div>
508
    </div>
509
  );
510

511
  return (
512
    <>
513
      <AddonData
514
        aliases={allAliases}
515
        profile={profile}
516
        runtimeData={runtimeData.data}
517
        totalBlockedEmails={profile.emails_blocked}
518
        totalForwardedEmails={profile.emails_forwarded}
519
        totalEmailTrackersRemoved={profile.level_one_trackers_blocked}
520
      />
521
      <Layout runtimeData={runtimeData.data}>
522
        {/* If free user has reached their free mask limit and 
523
        premium is available in their country, show upsell banner */}
524
        {freeMaskLimitReached &&
45✔
525
          isPeriodicalPremiumAvailableInCountry(runtimeData.data) && (
526
            <UpsellBanner />
527
          )}
528
        {isPhonesAvailableInCountry(runtimeData.data) ? (
40!
529
          <DashboardSwitcher />
530
        ) : null}
531
        <main className={styles["profile-wrapper"]}>
532
          {stats}
533
          {topBanners}
534
          <section className={styles["main-wrapper"]}>
535
            <Onboarding
536
              aliases={allAliases}
537
              onCreate={() => createAlias({ mask_type: "random" })}
1✔
538
            />
539
            <AliasList
540
              aliases={allAliases}
541
              onCreate={createAlias}
542
              onUpdate={updateAlias}
543
              onDelete={deleteAlias}
544
              profile={profile}
545
              user={user}
546
              runtimeData={runtimeData.data}
547
            />
548
            <p className={styles["size-information"]}>
549
              {l10n.getString("profile-supports-email-forwarding", {
550
                size: getRuntimeConfig().emailSizeLimitNumber,
551
                unit: getRuntimeConfig().emailSizeLimitUnit,
552
              })}
553
            </p>
554
          </section>
555
          {bottomBanners}
556
        </main>
557
        <aside>{bottomPremiumSection}</aside>
558
        <Tips profile={profile} runtimeData={runtimeData.data} />
559
      </Layout>
560
    </>
561
  );
562
};
563

564
const StatExplainer = (props: { children: React.ReactNode }) => {
1✔
565
  const l10n = useL10n();
×
566
  const explainerState = useMenuTriggerState({});
×
567
  const overlayRef = useRef<HTMLDivElement>(null);
×
568
  const openButtonRef = useRef<HTMLButtonElement>(null);
×
569
  const closeButtonRef = useRef<HTMLButtonElement>(null);
×
570
  const { triggerProps } = useOverlayTrigger(
×
571
    { type: "dialog" },
572
    explainerState,
573
    openButtonRef,
574
  );
575

576
  const openButtonProps = useButton(triggerProps, openButtonRef).buttonProps;
×
577
  const closeButtonProps = useButton(
×
578
    { onPress: explainerState.close },
579
    closeButtonRef,
580
  ).buttonProps;
581

582
  const positionProps = useOverlayPosition({
×
583
    targetRef: openButtonRef,
584
    overlayRef: overlayRef,
585
    placement: "bottom",
586
    // $spacing-sm is 8px:
587
    offset: 8,
588
    isOpen: explainerState.isOpen,
589
  }).overlayProps;
590

591
  return (
592
    <div
593
      className={`${styles["learn-more-wrapper"]} ${
594
        explainerState.isOpen ? styles["is-open"] : styles["is-closed"]
×
595
      }`}
596
    >
597
      <button
598
        {...openButtonProps}
599
        ref={openButtonRef}
600
        className={styles["open-button"]}
601
      >
602
        {l10n.getString("profile-stat-learn-more")}
603
      </button>
604
      {explainerState.isOpen && (
×
605
        <StatExplainerTooltip
606
          ref={overlayRef}
607
          overlayProps={{
608
            isOpen: explainerState.isOpen,
609
            isDismissable: true,
610
            onClose: explainerState.close,
611
          }}
612
          positionProps={positionProps}
613
        >
614
          <button
615
            ref={closeButtonRef}
616
            {...closeButtonProps}
617
            className={styles["close-button"]}
618
          >
619
            <CloseIcon alt={l10n.getString("profile-stat-learn-more-close")} />
620
          </button>
621
          {props.children}
622
        </StatExplainerTooltip>
623
      )}
624
    </div>
625
  );
626
};
627

628
type StatExplainerTooltipProps = {
629
  children: ReactNode;
630
  overlayProps: Parameters<typeof useOverlay>[0];
631
  positionProps: HTMLAttributes<HTMLDivElement>;
632
};
633
const StatExplainerTooltip = forwardRef<
1✔
634
  HTMLDivElement,
635
  StatExplainerTooltipProps
636
>(function StatExplainerTooltipWithForwardedRef(props, overlayRef) {
637
  const { overlayProps } = useOverlay(
×
638
    props.overlayProps,
639
    overlayRef as RefObject<HTMLDivElement>,
640
  );
641

642
  return (
643
    <FocusScope restoreFocus>
644
      <div
645
        {...overlayProps}
646
        {...props.positionProps}
647
        style={{
648
          ...props.positionProps.style,
649
          // Don't let `useOverlayPosition` handle the horizontal positioning,
650
          // as it will align the tooltip with the `.stat` element, whereas we
651
          // want it to span almost the full width on mobile:
652
          left: undefined,
653
          right: undefined,
654
        }}
655
        ref={overlayRef}
656
        className={styles["learn-more-tooltip"]}
657
      >
658
        {props.children}
659
      </div>
660
    </FocusScope>
661
  );
662
});
663

664
export default Profile;
47✔
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