UNPKG

@frak-labs/components

Version:

Frak Wallet components, helping any person to interact with the Frak wallet.

497 lines (496 loc) 14.9 kB
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 };