@frak-labs/components
Version:
Frak Wallet components, helping any person to interact with the Frak wallet.
497 lines (496 loc) • 14.9 kB
JavaScript
import { a as registerWebComponent, i as useClientReady, t as usePlacement } from "./usePlacement-5kbU3BKj.js";
import { t as useGlobalComponents } from "./useGlobalComponents-mSs9unyN.js";
import { t as useLightDomStyles } from "./useLightDomStyles-C8giLInY.js";
import { t as useReward } from "./useReward-ClVShg45.js";
import { a as ExternalLinkIcon, i as LogoFrakWithName, n as cssSource$3, o as CloseCircleIcon, r as WarningIcon, t as GiftIcon } from "./GiftIcon-BIp9FTJs.js";
import { isInAppBrowser, redirectToExternalBrowser, trackEvent } from "@frak-labs/core-sdk";
import { REFERRAL_SUCCESS_EVENT, getMergeToken } from "@frak-labs/core-sdk/actions";
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { jsx, jsxs } from "preact/jsx-runtime";
//#region \0ve-inline:../../packages/design-system/src/styles/inAppBanner.css.ts.vanilla.js
const cssSource$2 = `@keyframes inAppBanner_fadeIn__1ibpiy70 {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.inAppBanner_container__1ibpiy71 {
position: fixed;
top: max(8px, env(safe-area-inset-top));
left: 16px;
right: 16px;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 4px;
padding: 12px 16px;
padding-right: 32px;
border-radius: 12px;
background-color: #000000CC;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
color: #ffffff;
animation: inAppBanner_fadeIn__1ibpiy70 300ms ease-out;
}
.inAppBanner_header__1ibpiy72 {
display: flex;
align-items: center;
gap: 8px;
}
.inAppBanner_iconWrapper__1ibpiy73 {
flex-shrink: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
}
.inAppBanner_title__1ibpiy74 {
margin: 0;
padding: 0;
font-size: 16px;
font-weight: 500;
line-height: 26px;
color: var(--text-onAction__pbq4ak6);
}
.inAppBanner_body__1ibpiy75 {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0 4px;
}
.inAppBanner_description__1ibpiy76 {
margin: 0;
padding: 0;
font-size: 14px;
color: var(--text-onAction__pbq4ak6);
line-height: 22px;
opacity: 0.96;
}
.inAppBanner_cta__1ibpiy77 {
all: unset;
display: inline-flex;
align-items: center;
gap: 4px;
color: #2BB2FF;
font-size: 14px;
font-weight: 600;
text-decoration: underline;
text-underline-offset: 2px;
cursor: pointer;
}
.inAppBanner_cta__1ibpiy77:focus-visible {
outline: 2px solid #2BB2FF;
outline-offset: 2px;
border-radius: 4px;
}
.inAppBanner_closeButton__1ibpiy78 {
all: unset;
position: absolute;
top: 8px;
right: 8px;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 9999px;
color: rgba(255, 255, 255, 0.6);
cursor: pointer;
}
.inAppBanner_closeButton__1ibpiy78:focus-visible {
outline: 2px solid #ffffff;
outline-offset: 2px;
}`;
//#endregion
//#region ../../packages/design-system/src/styles/inAppBanner.css.ts
var body = "inAppBanner_body__1ibpiy75";
var closeButton = "inAppBanner_closeButton__1ibpiy78";
var container = "inAppBanner_container__1ibpiy71";
var cta = "inAppBanner_cta__1ibpiy77";
var description = "inAppBanner_description__1ibpiy76";
var header = "inAppBanner_header__1ibpiy72";
var iconWrapper = "inAppBanner_iconWrapper__1ibpiy73";
var title = "inAppBanner_title__1ibpiy74";
//#endregion
//#region ../../packages/design-system/src/components/InAppBanner/index.tsx
function InAppBanner({ title: title$1, description: description$1, cta: cta$1, dismissLabel, onAction, onDismiss, className, classNames }) {
return /* @__PURE__ */ jsxs("div", {
className: `${container}${className ? ` ${className}` : ""}`,
role: "alert",
children: [
/* @__PURE__ */ jsxs("div", {
className: header,
children: [/* @__PURE__ */ jsx("span", {
className: `${iconWrapper}${classNames?.icon ? ` ${classNames.icon}` : ""}`,
children: /* @__PURE__ */ jsx(WarningIcon, {
width: 20,
height: 20
})
}), /* @__PURE__ */ jsx("p", {
className: `${title}${classNames?.title ? ` ${classNames.title}` : ""}`,
children: title$1
})]
}),
/* @__PURE__ */ jsxs("div", {
className: body,
children: [/* @__PURE__ */ jsx("p", {
className: `${description}${classNames?.description ? ` ${classNames.description}` : ""}`,
children: description$1
}), /* @__PURE__ */ jsxs("button", {
type: "button",
className: `${cta}${classNames?.cta ? ` ${classNames.cta}` : ""}`,
onClick: onAction,
children: [cta$1, /* @__PURE__ */ jsx(ExternalLinkIcon, {
width: 14,
height: 14
})]
})]
}),
/* @__PURE__ */ jsx("button", {
type: "button",
className: `${closeButton}${classNames?.close ? ` ${classNames.close}` : ""}`,
onClick: onDismiss,
"aria-label": dismissLabel,
children: /* @__PURE__ */ jsx(CloseCircleIcon, {
width: 16,
height: 16
})
})
]
});
}
//#endregion
//#region \0ve-inline:src/components/Banner/Banner.css.ts.vanilla.js
const cssSource$1 = `@keyframes Banner_fadeIn__1gnumzi0 {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.Banner_rootBase__1gnumzi1 {
position: relative;
display: flex;
animation: Banner_fadeIn__1gnumzi0 300ms ease-out;
}
.Banner_iconSvg__1gnumzi2 {
width: 100%;
height: 100%;
}
.Banner_referral__1gnumzi3 {
flex-direction: row;
align-items: center;
gap: 16px;
padding: 16px;
background-color: #ffffff;
color: var(--text-primary__pbq4ak0);
border: 1px solid var(--border-default__pbq4akv);
border-radius: 12px;
}
.Banner_referralIconWrapper__1gnumzi4 {
flex-shrink: 0;
align-self: flex-start;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
overflow: hidden;
}
.Banner_referralImage__1gnumzi5 {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
object-fit: contain;
display: block;
}
.Banner_referralBody__1gnumzi6 {
flex: 1;
min-width: 0;
}
.Banner_referralTitle__1gnumzi7 {
font-size: 16px;
font-weight: 600;
color: var(--text-primary__pbq4ak0);
line-height: 22px;
}
.Banner_referralDescription__1gnumzi8 {
margin-bottom: 8px;
font-size: 14px;
color: #979797;
line-height: 22px;
}
.Banner_referralCta__1gnumzi9 {
display: inline-block;
padding: 8px 16px;
border: 1px solid #000000;
border-radius: 9999px;
color: var(--text-primary__pbq4ak0);
font-size: 10px;
font-weight: 700;
line-height: 12px;
text-transform: uppercase;
}
.Banner_frakLogo__1gnumzia {
position: absolute;
right: 16px;
bottom: 12px;
pointer-events: none;
}`;
//#endregion
//#region src/components/Banner/Banner.css.ts
var frakLogo = "Banner_frakLogo__1gnumzia";
var iconSvg = "Banner_iconSvg__1gnumzi2";
var referral = "Banner_referral__1gnumzi3 reset_base__1831jhd0 Banner_rootBase__1gnumzi1";
var referralBody = "Banner_referralBody__1gnumzi6";
var referralCta = "Banner_referralCta__1gnumzi9 sharedBaseCss_buttonReset__7cswil0";
var referralDescription = "Banner_referralDescription__1gnumzi8 reset_base__1831jhd0";
var referralIconWrapper = "Banner_referralIconWrapper__1gnumzi4";
var referralImage = "Banner_referralImage__1gnumzi5";
var referralTitle = "Banner_referralTitle__1gnumzi7 reset_base__1831jhd0";
const cssSource = cssSource$2 + cssSource$1;
//#endregion
//#region src/components/Banner/Banner.tsx
/**
* Auto-detecting notification banner component.
*
* Renders an inline banner on the merchant page with one of two distinct
* visual styles depending on the detected mode:
*
* - **Referral mode** (white): Shown after a successful referral link
* processing. Displays a gift icon, reward copy, and a "Got it" CTA.
* - **In-app browser mode** (dark transparent): Shown when the page is
* opened inside a social media in-app browser (Instagram, Facebook).
* Offers an inline link to redirect to the default browser plus a
* close button to dismiss.
*
* In-app browser mode takes priority over referral mode.
* Uses Light DOM + vanilla-extract styles from `@frak-labs/design-system`.
*
* @group components
*
* @example
* Basic usage (auto-detects mode):
* ```html
* <frak-banner></frak-banner>
* ```
*
* @example
* With a custom class:
* ```html
* <frak-banner classname="my-custom-banner"></frak-banner>
* ```
*/
function Banner({ placement: placementId, classname = "", interaction, referralTitle: propReferralTitle, referralDescription: propReferralDescription, referralCta: propReferralCta, inappTitle: propInappTitle, inappDescription: propInappDescription, inappCta: propInappCta, imageUrl, preview, previewMode, allowInappRedirect }) {
const isPreview = !!preview;
const resolvedPreviewMode = previewMode === "inapp" ? "inapp" : "referral";
const isInappRedirectAllowed = allowInappRedirect === true || allowInappRedirect === "true";
const placement = usePlacement(placementId);
const { shouldRender, isHidden, isClientReady } = useClientReady();
useLightDomStyles("frak-banner", placementId, placement?.components?.banner?.css, cssSource, cssSource$3);
const [dismissed, setDismissed] = useState(false);
const [mode, setMode] = useState(() => {
if (isPreview) return resolvedPreviewMode;
return isInappRedirectAllowed && isInAppBrowser ? "inapp" : null;
});
const trackedImpressionModeRef = useRef(null);
useEffect(() => {
if (isPreview) setMode(resolvedPreviewMode);
}, [isPreview, resolvedPreviewMode]);
const { reward } = useReward(mode === "referral" && isClientReady, interaction);
const [prefetchedMergeToken, setPrefetchedMergeToken] = useState(null);
useEffect(() => {
const client = window.FrakSetup?.client;
if (mode !== "inapp" || isPreview || !isClientReady || !client) return;
getMergeToken(client).then((token) => setPrefetchedMergeToken(token)).catch(() => {});
}, [
mode,
isPreview,
isClientReady
]);
useEffect(() => {
if (isPreview || !mode || dismissed) return;
if (trackedImpressionModeRef.current === mode) return;
if (!isClientReady) return;
trackEvent(window.FrakSetup?.client, "banner_impression", {
placement: placementId,
variant: mode,
has_reward: mode === "referral" ? Boolean(reward) : void 0
});
trackedImpressionModeRef.current = mode;
}, [
mode,
dismissed,
isClientReady,
isPreview,
placementId
]);
useEffect(() => {
if (isPreview || mode === "inapp") return;
const handler = () => setMode("referral");
window.addEventListener(REFERRAL_SUCCESS_EVENT, handler);
return () => window.removeEventListener(REFERRAL_SUCCESS_EVENT, handler);
}, [isPreview, mode]);
const handleAction = useCallback(async () => {
if (isPreview) return;
trackEvent(window.FrakSetup?.client, "banner_resolved", {
placement: placementId,
variant: mode ?? "referral",
outcome: "clicked"
});
if (mode === "referral") {
setDismissed(true);
return;
}
let mergeToken = prefetchedMergeToken;
if (!mergeToken && window.FrakSetup?.client) try {
mergeToken = await getMergeToken(window.FrakSetup?.client);
} catch {}
let targetUrl = window.location.href;
if (mergeToken) {
const url = new URL(targetUrl);
url.searchParams.set("fmt", mergeToken);
targetUrl = url.toString();
}
redirectToExternalBrowser(targetUrl);
}, [
isPreview,
mode,
prefetchedMergeToken,
placementId
]);
const handleDismiss = useCallback(() => {
if (isPreview) return;
trackEvent(window.FrakSetup?.client, "banner_resolved", {
placement: placementId,
variant: mode ?? "referral",
outcome: "dismissed"
});
setDismissed(true);
}, [
isPreview,
mode,
placementId
]);
const globalComponents = useGlobalComponents();
const bannerConfig = placement?.components?.banner ?? globalComponents?.banner;
const texts = useMemo(() => {
if (mode === "referral") {
const defaultTitle = reward ? `Earn ${reward} on purchases on this site` : "You've been referred!";
return {
title: propReferralTitle ?? bannerConfig?.referralTitle ?? defaultTitle,
description: propReferralDescription ?? bannerConfig?.referralDescription ?? "Earn rewards after your purchase via the Frak partner app.",
cta: propReferralCta ?? bannerConfig?.referralCta ?? "Got it"
};
}
return {
title: propInappTitle ?? bannerConfig?.inappTitle ?? "Open in your browser",
description: propInappDescription ?? bannerConfig?.inappDescription ?? "For a better experience and to earn your rewards, open this page in your default browser.",
cta: propInappCta ?? bannerConfig?.inappCta ?? "Open browser"
};
}, [
mode,
reward,
bannerConfig,
propReferralTitle,
propReferralDescription,
propReferralCta,
propInappTitle,
propInappDescription,
propInappCta
]);
if (!mode || !isPreview && (!shouldRender || isHidden || dismissed)) return null;
const bannerClass = [
referral,
"frak-banner",
`frak-banner--${mode}`,
classname
].filter(Boolean).join(" ");
if (mode === "inapp") return /* @__PURE__ */ jsx(InAppBanner, {
title: texts.title,
description: texts.description,
cta: texts.cta,
dismissLabel: "Dismiss",
onAction: handleAction,
onDismiss: handleDismiss,
className: [
"frak-banner",
"frak-banner--inapp",
classname
].filter(Boolean).join(" "),
classNames: {
icon: "frak-banner__icon",
title: "frak-banner__title",
description: "frak-banner__description",
cta: "frak-banner__cta",
close: "frak-banner__close"
}
});
return /* @__PURE__ */ jsxs("div", {
class: bannerClass,
role: "alert",
children: [
/* @__PURE__ */ jsx("div", {
class: `${referralIconWrapper} frak-banner__icon`,
children: imageUrl ? /* @__PURE__ */ jsx("img", {
src: imageUrl,
alt: "",
class: referralImage
}) : /* @__PURE__ */ jsx(GiftIcon, { class: iconSvg })
}),
/* @__PURE__ */ jsxs("div", {
class: `${referralBody} frak-banner__text`,
children: [
/* @__PURE__ */ jsx("p", {
class: `${referralTitle} frak-banner__title`,
children: texts.title
}),
/* @__PURE__ */ jsx("p", {
class: `${referralDescription} frak-banner__description`,
children: texts.description
}),
/* @__PURE__ */ jsx("button", {
type: "button",
class: `${referralCta} frak-banner__cta`,
onClick: handleAction,
children: texts.cta
})
]
}),
/* @__PURE__ */ jsx(LogoFrakWithName, {
class: `${frakLogo} frak-banner__logo`,
width: 42,
height: 24
})
]
});
}
//#endregion
//#region src/components/Banner/index.ts
registerWebComponent(Banner, "frak-banner", [
"placement",
"classname",
"interaction",
"referralTitle",
"referralDescription",
"referralCta",
"inappTitle",
"inappDescription",
"inappCta",
"preview",
"previewMode",
"imageUrl",
"allowInappRedirect"
], { shadow: false });
//#endregion
export { Banner };