@frak-labs/components
Version:
Frak Wallet components, helping any person to interact with the Frak wallet.
514 lines (513 loc) • 15.6 kB
JavaScript
import { i as useLang, o as useClientReady, s as registerWebComponent, t as usePlacement } from "./usePlacement-Bpo25qjA.js";
import { t as useGlobalComponents } from "./useGlobalComponents-CeTO2xYs.js";
import { t as useLightDomStyles } from "./useLightDomStyles-B2U96tzU.js";
import { i as x } from "./rewards-CDS1CYlm.js";
import { t as useReward } from "./useReward-Cg2wk8XI.js";
import { componentDefaults } from "./i18n/defaults.js";
import { a as ExternalLinkIcon, i as LogoFrakWithName, n as cssSource$5, o as CloseCircleIcon, r as WarningIcon, t as GiftIcon } from "./GiftIcon-BhL0bhVu.js";
import { isInAppBrowser, redirectToExternalBrowser, sanitizeProductDetailsList, 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/keyframes.css.ts.vanilla.js
const cssSource$4 = `@keyframes keyframes_fadeIn__mnqmlt0 {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes keyframes_fadeOut__mnqmlt1 {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes keyframes_fadeInDown__mnqmlt2 {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}`;
//#endregion
//#region \0ve-inline:../../packages/design-system/src/styles/inAppBanner.css.ts.vanilla.js
const cssSource$3 = `.inAppBanner_container__1ibpiy70 {
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: var(--text-onAction__pbq4ak6);
animation: keyframes_fadeInDown__mnqmlt2 300ms ease-out;
}
.inAppBanner_header__1ibpiy71 {
display: flex;
align-items: center;
gap: 8px;
}
.inAppBanner_iconWrapper__1ibpiy72 {
flex-shrink: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-onAction__pbq4ak6);
}
.inAppBanner_title__1ibpiy73 {
margin: 0;
padding: 0;
font-size: 16px;
font-weight: 500;
line-height: 26px;
color: var(--text-onAction__pbq4ak6);
}
.inAppBanner_body__1ibpiy74 {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0 4px;
}
.inAppBanner_description__1ibpiy75 {
margin: 0;
padding: 0;
font-size: 14px;
color: var(--text-onAction__pbq4ak6);
line-height: 22px;
opacity: 0.96;
}
.inAppBanner_cta__1ibpiy76 {
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__1ibpiy76:focus-visible {
outline: 2px solid #2bb2ff;
outline-offset: 2px;
border-radius: 4px;
}
.inAppBanner_closeButton__1ibpiy77 {
all: unset;
position: absolute;
top: 8px;
right: 8px;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 9999px;
color: #ffffff99;
cursor: pointer;
}
.inAppBanner_closeButton__1ibpiy77:focus-visible {
outline: 2px solid var(--text-onAction__pbq4ak6);
outline-offset: 2px;
}`;
//#endregion
//#region ../../packages/design-system/src/styles/inAppBanner.css.ts
var body = "inAppBanner_body__1ibpiy74";
var closeButton = "inAppBanner_closeButton__1ibpiy77";
var container = "inAppBanner_container__1ibpiy70";
var cta = "inAppBanner_cta__1ibpiy76";
var description = "inAppBanner_description__1ibpiy75";
var header = "inAppBanner_header__1ibpiy71";
var iconWrapper = "inAppBanner_iconWrapper__1ibpiy72";
var title = "inAppBanner_title__1ibpiy73";
cssSource$4 + cssSource$3;
//#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 = `.Banner_rootBase__1gnumzi0 {
position: relative;
display: flex;
animation: keyframes_fadeInDown__mnqmlt2 300ms ease-out;
}
.Banner_iconSvg__1gnumzi1 {
width: 100%;
height: 100%;
}
.Banner_referral__1gnumzi2 {
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__1gnumzi3 {
flex-shrink: 0;
align-self: flex-start;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
overflow: hidden;
}
.Banner_referralImage__1gnumzi4 {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
object-fit: contain;
display: block;
}
.Banner_referralBody__1gnumzi5 {
flex: 1;
min-width: 0;
}
.Banner_referralTitle__1gnumzi6 {
font-size: 16px;
font-weight: 600;
color: var(--text-primary__pbq4ak0);
line-height: 22px;
}
.Banner_referralDescription__1gnumzi7 {
margin-bottom: 8px;
font-size: 14px;
color: #979797;
line-height: 22px;
}
.Banner_referralCta__1gnumzi8 {
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__1gnumzi9 {
position: absolute;
right: 16px;
bottom: 12px;
pointer-events: none;
}`;
//#endregion
//#region src/components/Banner/Banner.css.ts
var frakLogo = "Banner_frakLogo__1gnumzi9";
var iconSvg = "Banner_iconSvg__1gnumzi1";
var referral = "Banner_referral__1gnumzi2 reset_base__1831jhd0 Banner_rootBase__1gnumzi0";
var referralBody = "Banner_referralBody__1gnumzi5";
var referralCta = "Banner_referralCta__1gnumzi8 sharedBaseCss_buttonReset__7cswil0";
var referralDescription = "Banner_referralDescription__1gnumzi7 reset_base__1831jhd0";
var referralIconWrapper = "Banner_referralIconWrapper__1gnumzi3";
var referralImage = "Banner_referralImage__1gnumzi4";
var referralTitle = "Banner_referralTitle__1gnumzi6 reset_base__1831jhd0";
const cssSource = cssSource$4 + cssSource$3 + 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, products, referralTitle: propReferralTitle, referralDescription: propReferralDescription, referralCta: propReferralCta, inappTitle: propInappTitle, inappDescription: propInappDescription, inappCta: propInappCta, imageUrl: propImageUrl, preview, previewMode, allowInappRedirect }) {
const isPreview = !!preview;
const lang = useLang();
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$5);
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 parsedProducts = useMemo(() => sanitizeProductDetailsList(products), [products]);
const { reward } = useReward(mode === "referral" && isClientReady, interaction, "referee", parsedProducts);
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 imageUrl = propImageUrl ?? bannerConfig?.imageUrl;
const texts = useMemo(() => {
const defaults = componentDefaults[lang].banner;
if (mode === "referral") {
const defaultTitle = reward ? defaults.referralTitleReward : defaults.referralTitle;
return {
title: x(propReferralTitle ?? bannerConfig?.referralTitle ?? defaultTitle, reward),
description: x(propReferralDescription ?? bannerConfig?.referralDescription ?? defaults.referralDescription, reward),
cta: propReferralCta ?? bannerConfig?.referralCta ?? defaults.referralCta
};
}
return {
title: x(propInappTitle ?? bannerConfig?.inappTitle ?? defaults.inappTitle, void 0),
description: x(propInappDescription ?? bannerConfig?.inappDescription ?? defaults.inappDescription, void 0),
cta: propInappCta ?? bannerConfig?.inappCta ?? defaults.inappCta
};
}, [
lang,
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: componentDefaults[lang].banner.dismissLabel,
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",
"products",
"referralTitle",
"referralDescription",
"referralCta",
"inappTitle",
"inappDescription",
"inappCta",
"preview",
"previewMode",
"imageUrl",
"allowInappRedirect"
], { shadow: false });
//#endregion
export { Banner };