@tiendanube/live-state 1.0.0-beta.14 → 1.0.0-beta.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +60 -29
- package/dist/index.js.map +1 -1
- package/dist/src/components/LiveStateAlert.d.ts.map +1 -1
- package/dist/src/components/LiveStateInfo.d.ts.map +1 -1
- package/dist/src/components/RichText.d.ts +19 -0
- package/dist/src/components/RichText.d.ts.map +1 -0
- package/dist/src/utils/sanitize.d.ts +25 -0
- package/dist/src/utils/sanitize.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./LiveStateProvider-vcADappI.cjs`);let t=require(`react`),n=require(`swr`);n=e.s(n,1);let r=require(`react/jsx-runtime`),i=require(`@nimbus-ds/components`),a=require(`@nimbus-ds/icons`);function o(){let{fetcher:t,log:r}=e.n(),{data:i,error:a,isLoading:o,mutate:s}=(0,n.default)(`live-state`,t,{revalidateOnFocus:!1,revalidateOnReconnect:!0,dedupingInterval:6e4,shouldRetryOnError:!1,onError:e=>{r(`error`,`Failed to fetch live state`,{error:e})}});return{liveState:i??null,isLoading:o,isError:a!=null,error:a??null,refresh:async()=>{try{await s()}catch(e){r(`error`,`Failed to refresh live state`,{error:e})}}}}function s(){let{onEvent:n,disabled:r,log:i}=e.n();return(0,t.useCallback)((t,a)=>{n?.(t,a),r||e.a(t,a,i)},[n,r,i])}var c=`@tiendanube/live-state:closable`,l=c;function u(){try{let e=localStorage.getItem(l);if(!e)return{};let t=JSON.parse(e);if(typeof t!=`object`||!t)return{};let n={};for(let[e,r]of Object.entries(t))if(!(typeof r!=`object`||!r)){n[e]={};for(let[t,i]of Object.entries(r))if(typeof i==`number`)n[e][t]={count:i,closedAt:0};else if(typeof i==`object`&&i){let r=i;n[e][t]={count:typeof r.count==`number`?r.count:0,closedAt:typeof r.closedAt==`number`?r.closedAt:0}}}return n}catch{return{}}}function d(e){try{localStorage.setItem(l,JSON.stringify(e))}catch{}}function f({context:e,id:n,maxCloseTimes:r=3,expiresIn:i}){let[a,o]=(0,t.useState)(null);return(0,t.useEffect)(()=>{if(!n){o(!0);return}let t=u(),a=t[e]?.[n];if(!a){o(!0);return}let{count:s,closedAt:c}=a;if(i!=null&&c>0&&Date.now()-c>i){let r={...t};r[e]&&(delete r[e][n],d(r)),o(!0);return}o(s<r)},[e,n,r,i]),{isVisible:a,close:(0,t.useCallback)(()=>{if(o(!1),!n)return;let t=u(),r=t[e]?.[n]?.count??0;d({...t,[e]:{...t[e]??{},[n]:{count:r+1,closedAt:Date.now()}}})},[e,n])}}function p(t,n){let r=e.o(n);try{switch(t.type){case`redirect`:window.location.href=t.url;break;case`whatsapp`:{let e=t.whatsappMessage?`${t.url}?text=${encodeURIComponent(t.whatsappMessage)}`:t.url;window.open(e,`_blank`);break}case`external`:window.open(t.url,`_blank`);break;default:r(`warn`,`Unknown CTA type: ${t.type}`,{cta:t})}}catch(e){r(`error`,`Failed to handle CTA click`,{cta:t,error:e})}}function m({data:n,trackingConfig:a,onClose:o,onCtaClick:c}){let l=s(),{log:u}=e.n(),d=n.type===`alert`?`danger`:`warning`,f=(0,t.useCallback)(()=>e.i({context:n.context,campaignId:n.campaignId,group:n.group,...n.metadata,...a.properties}),[n,a.properties]),m=(0,t.useCallback)(()=>{l(e.r(a.prefix,a.page,n.context,`click`),f()),c?c(n.cta):p(n.cta,u)},[n,a,f,l,c,u]),h=(0,t.useCallback)(()=>{l(e.r(a.prefix,a.page,n.context,`close`),f()),o?.()},[o,n,a,f,l]);return(0,t.useEffect)(()=>{l(e.r(a.prefix,a.page,n.context,`view`),f())},[]),(0,r.jsx)(i.Alert,{appearance:d,title:n.title,onRemove:o?h:void 0,children:(0,r.jsxs)(i.Box,{display:`flex`,alignItems:`flex-start`,gap:`3`,flexWrap:`wrap`,flexDirection:`column`,children:[(0,r.jsx)(i.Box,{flex:`1 1 auto`,minWidth:`220px`,children:(0,r.jsx)(
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./LiveStateProvider-vcADappI.cjs`);let t=require(`react`),n=require(`swr`);n=e.s(n,1);let r=require(`react/jsx-runtime`),i=require(`@nimbus-ds/components`),a=require(`@nimbus-ds/icons`);function o(){let{fetcher:t,log:r}=e.n(),{data:i,error:a,isLoading:o,mutate:s}=(0,n.default)(`live-state`,t,{revalidateOnFocus:!1,revalidateOnReconnect:!0,dedupingInterval:6e4,shouldRetryOnError:!1,onError:e=>{r(`error`,`Failed to fetch live state`,{error:e})}});return{liveState:i??null,isLoading:o,isError:a!=null,error:a??null,refresh:async()=>{try{await s()}catch(e){r(`error`,`Failed to refresh live state`,{error:e})}}}}function s(){let{onEvent:n,disabled:r,log:i}=e.n();return(0,t.useCallback)((t,a)=>{n?.(t,a),r||e.a(t,a,i)},[n,r,i])}var c=`@tiendanube/live-state:closable`,l=c;function u(){try{let e=localStorage.getItem(l);if(!e)return{};let t=JSON.parse(e);if(typeof t!=`object`||!t)return{};let n={};for(let[e,r]of Object.entries(t))if(!(typeof r!=`object`||!r)){n[e]={};for(let[t,i]of Object.entries(r))if(typeof i==`number`)n[e][t]={count:i,closedAt:0};else if(typeof i==`object`&&i){let r=i;n[e][t]={count:typeof r.count==`number`?r.count:0,closedAt:typeof r.closedAt==`number`?r.closedAt:0}}}return n}catch{return{}}}function d(e){try{localStorage.setItem(l,JSON.stringify(e))}catch{}}function f({context:e,id:n,maxCloseTimes:r=3,expiresIn:i}){let[a,o]=(0,t.useState)(null);return(0,t.useEffect)(()=>{if(!n){o(!0);return}let t=u(),a=t[e]?.[n];if(!a){o(!0);return}let{count:s,closedAt:c}=a;if(i!=null&&c>0&&Date.now()-c>i){let r={...t};r[e]&&(delete r[e][n],d(r)),o(!0);return}o(s<r)},[e,n,r,i]),{isVisible:a,close:(0,t.useCallback)(()=>{if(o(!1),!n)return;let t=u(),r=t[e]?.[n]?.count??0;d({...t,[e]:{...t[e]??{},[n]:{count:r+1,closedAt:Date.now()}}})},[e,n])}}function p(t,n){let r=e.o(n);try{switch(t.type){case`redirect`:window.location.href=t.url;break;case`whatsapp`:{let e=t.whatsappMessage?`${t.url}?text=${encodeURIComponent(t.whatsappMessage)}`:t.url;window.open(e,`_blank`);break}case`external`:window.open(t.url,`_blank`);break;default:r(`warn`,`Unknown CTA type: ${t.type}`,{cta:t})}}catch(e){r(`error`,`Failed to handle CTA click`,{cta:t,error:e})}}var m=new Set([`strong`,`em`,`b`,`i`,`br`,`span`]),h=/<(script|style|iframe|object|embed|form|input|button|textarea|select|link|meta|head|body|html|svg|math)[\s\S]*?<\/\1\s*>|<(script|style|iframe|object|embed|form|input|button|textarea|select|link|meta|head|body|html|svg|math)[^>]*\/?>/gi;function g(e){return e.replace(h,``).replace(/<([a-zA-Z][a-zA-Z0-9]*)\s[^>]*>/g,`<$1>`).replace(/<\/?([a-zA-Z][a-zA-Z0-9]*)[^>]*>/g,(e,t)=>m.has(t.toLowerCase())?e:``)}function _(e){return/<[a-zA-Z][^>]*>/.test(e)}function v({content:e,...t}){if(_(e)){let n=g(e);return(0,r.jsx)(i.Text,{...t,children:(0,r.jsx)(`span`,{dangerouslySetInnerHTML:{__html:n}})})}return(0,r.jsx)(i.Text,{...t,children:e})}function y({data:n,trackingConfig:a,onClose:o,onCtaClick:c}){let l=s(),{log:u}=e.n(),d=n.type===`alert`?`danger`:`warning`,f=(0,t.useCallback)(()=>e.i({context:n.context,campaignId:n.campaignId,group:n.group,...n.metadata,...a.properties}),[n,a.properties]),m=(0,t.useCallback)(()=>{l(e.r(a.prefix,a.page,n.context,`click`),f()),c?c(n.cta):p(n.cta,u)},[n,a,f,l,c,u]),h=(0,t.useCallback)(()=>{l(e.r(a.prefix,a.page,n.context,`close`),f()),o?.()},[o,n,a,f,l]);return(0,t.useEffect)(()=>{l(e.r(a.prefix,a.page,n.context,`view`),f())},[]),(0,r.jsx)(i.Alert,{appearance:d,title:n.title,onRemove:o?h:void 0,children:(0,r.jsxs)(i.Box,{display:`flex`,alignItems:`flex-start`,gap:`3`,flexWrap:`wrap`,flexDirection:`column`,children:[(0,r.jsx)(i.Box,{flex:`1 1 auto`,minWidth:`220px`,children:(0,r.jsx)(v,{content:n.message})}),(0,r.jsx)(i.Box,{display:`flex`,justifyContent:`flex-end`,marginTop:`2`,children:(0,r.jsx)(i.Button,{appearance:`primary`,onClick:m,children:n.cta.label})})]})})}var b={blue:{background:`#0050C3`,icon:`#FFFFFF`},white:{background:`#FFFFFF`,icon:`#0059D5`}};function x({data:n,trackingConfig:o,defaultVariant:c=`blue`,onClose:l,isMobile:u,onCtaClick:d}){let f=s(),{log:m}=e.n(),h=n.variant||c,g=b[h],[_,y]=(0,t.useState)(()=>window.innerWidth<750);(0,t.useEffect)(()=>{let e=()=>y(window.innerWidth<750);return window.addEventListener(`resize`,e),()=>window.removeEventListener(`resize`,e)},[]);let x=u??_,S=(0,t.useCallback)(()=>e.i({context:n.context,campaignId:n.campaignId,group:n.group,...n.metadata,...o.properties}),[n,o.properties]),C=(0,t.useCallback)(()=>{f(e.r(o.prefix,o.page,n.context,`click`),S()),d?d(n.cta):p(n.cta,m)},[n,o,S,f,d,m]),w=(0,t.useCallback)(()=>{f(e.r(o.prefix,o.page,n.context,`close`),S()),l?.()},[l,n,o,S,f]);(0,t.useEffect)(()=>{f(e.r(o.prefix,o.page,n.context,`view`),S())},[]);let T=(0,r.jsx)(i.Link,{as:`a`,textDecoration:`none`,onClick:C,"data-testid":`live-state-cta-link`,children:(0,r.jsx)(i.Text,{color:`primary-interactive`,fontSize:`base`,children:n.cta.label})});return(0,r.jsx)(i.Card,{children:(0,r.jsxs)(i.Box,{display:`flex`,flexDirection:`row`,justifyContent:`space-between`,children:[(0,r.jsxs)(i.Box,{display:`flex`,gap:`4`,alignItems:x?`flex-start`:`center`,paddingRight:`1-5`,children:[(0,r.jsx)(i.Box,{minWidth:`32px`,children:(0,r.jsx)(`div`,{style:{width:32,height:32,borderRadius:`35%`,borderColor:h===`white`?`#E7E7E7`:`transparent`,borderWidth:1,borderStyle:`solid`,background:g.background,display:`flex`,alignItems:`center`,justifyContent:`center`},children:(0,r.jsx)(a.MoneyIcon,{style:{color:g.icon}})})}),(0,r.jsxs)(i.Box,{display:`grid`,gap:`2`,children:[(0,r.jsxs)(i.Box,{children:[(0,r.jsx)(v,{content:n.title,fontSize:`base`,color:`neutral-textHigh`}),(0,r.jsx)(v,{content:n.message,fontSize:`base`,color:`neutral-textLow`})]}),x&&T]})]}),(!x||l)&&(0,r.jsxs)(i.Box,{display:`flex`,flexDirection:x?`column`:`row`,alignItems:x?`flex-end`:`center`,gap:`2`,children:[!x&&T,l&&(0,r.jsx)(`button`,{type:`button`,"data-testid":`live-state-close-button`,"aria-label":`Fechar notificação`,onClick:w,style:{background:`none`,border:`none`,padding:0,cursor:`pointer`,display:`flex`,alignItems:`center`,justifyContent:`center`},children:(0,r.jsx)(i.Icon,{source:(0,r.jsx)(a.CloseIcon,{width:`18px`,height:`18px`}),color:`neutral-interactive`})})]})]})})}function S({data:t,loading:n,trackingConfig:i,allowedContexts:a,defaultVariant:o,isMobile:s,onCtaClick:c}){let{log:l}=e.n(),{isVisible:u,close:d}=f({context:t?.context??``,id:t?.campaignId,maxCloseTimes:t?.metadata?.maxCloseTimes,expiresIn:t?.metadata?.expiresIn});if(n||!t)return null;if(!t.context||!t.type||!t.title||!t.message||!t.cta?.label||!t.cta?.url||!t.cta?.type)return l(`warn`,`Invalid payload, missing required fields`,{data:t}),null;if(a&&!a.includes(t.context)||u===null||!u)return null;let p=t.closable?d:void 0;return t.type===`alert`||t.type===`warning`?(0,r.jsx)(y,{data:t,onClose:p,trackingConfig:i,onCtaClick:c}):t.type===`info`?(0,r.jsx)(x,{data:t,onClose:p,trackingConfig:i,defaultVariant:o,isMobile:s,onCtaClick:c}):(l(`warn`,`Unknown type: ${t.type}`,{data:t}),null)}exports.LIVE_STATE_STORAGE_KEY=c,exports.LiveStateAlert=y,exports.LiveStateInfo=x,exports.LiveStateProvider=e.t,exports.LiveStateRenderer=S,exports.handleCtaClick=p,exports.trackEvent=e.a,exports.useLiveState=o,exports.useTrackEvent=s;
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../src/hooks/useLiveState.ts","../src/hooks/useTrackEvent.ts","../src/utils/closable-storage.ts","../src/hooks/useClosable.ts","../src/utils/cta.ts","../src/components/LiveStateAlert.tsx","../src/components/LiveStateInfo.tsx","../src/components/LiveStateRenderer.tsx"],"sourcesContent":["import useSWR from 'swr';\nimport { useLiveStateContext } from '../providers/LiveStateProvider';\nimport type { LiveStateResponse } from '../types';\n\ninterface UseLiveStateReturn {\n /** Live state data */\n liveState: LiveStateResponse | null;\n\n /** True while the first fetch is in progress */\n isLoading: boolean;\n\n /**\n * True when the last fetch attempt resulted in an error.\n * Use this to implement fallbacks or custom error logging.\n * Note: the lib already logs a warning to the console on every error.\n */\n isError: boolean;\n\n /**\n * The raw error from the last failed fetch, or null.\n * Pair with `isError` for more specific error handling.\n */\n error: Error | null;\n\n /** Manually trigger a re-fetch */\n refresh: () => Promise<void>;\n}\n\n/**\n * useLiveState hook\n *\n * Fetches and manages live state data using SWR with automatic caching,\n * deduplication, and revalidation.\n *\n * Must be used within LiveStateProvider. See examples/ directory for usage.\n */\nexport function useLiveState(): UseLiveStateReturn {\n const { fetcher, log } = useLiveStateContext();\n\n const { data, error, isLoading, mutate } = useSWR<LiveStateResponse | null>(\n 'live-state',\n fetcher,\n {\n revalidateOnFocus: false,\n revalidateOnReconnect: true,\n dedupingInterval: 60000, // 1 minute\n shouldRetryOnError: false, // Fail silently\n onError: err => {\n log('error', 'Failed to fetch live state', { error: err });\n },\n },\n );\n\n const refresh = async () => {\n try {\n await mutate();\n } catch (err) {\n log('error', 'Failed to refresh live state', { error: err });\n }\n };\n\n return {\n liveState: data ?? null,\n isLoading,\n isError: error != null,\n error: error ?? null,\n refresh,\n };\n}\n","import { useCallback } from 'react';\nimport { useLiveStateContext } from '../providers/LiveStateProvider';\nimport { trackEvent } from '../utils/analytics';\nimport type { TrackingProperties } from '../types';\n\n/**\n * useTrackEvent\n *\n * Returns a `track` function that fires both the internal analytics\n * (Amplitude + Clarity) and the optional `onEvent` callback provided\n * to LiveStateProvider — useful for simulators and debugging tools.\n */\nexport function useTrackEvent() {\n const { onEvent, disabled, log } = useLiveStateContext();\n\n const track = useCallback(\n (eventName: string, properties?: TrackingProperties) => {\n // onEvent callback fires even when disabled — it is an observability\n // hook, not an analytics SDK, so the caller opts in deliberately.\n onEvent?.(eventName, properties);\n\n // Skip SDK calls when analytics are disabled (e.g. dev/test environments)\n if (!disabled) {\n trackEvent(eventName, properties, log);\n }\n },\n [onEvent, disabled, log],\n );\n\n return track;\n}\n","export const LIVE_STATE_STORAGE_KEY = '@tiendanube/live-state:closable';\n\n/** @internal */\nconst STORAGE_KEY = LIVE_STATE_STORAGE_KEY;\n\nexport interface ClosableState {\n count: number;\n closedAt: number;\n}\n\nexport type ClosableStates = Record<string, Record<string, ClosableState>>;\n\n/**\n * Reads closable states from localStorage.\n * Normalises legacy format (plain number) to the current shape ({ count, closedAt }).\n * Returns an empty object on any error (fail-silent).\n */\nexport function getClosableStates(): ClosableStates {\n try {\n const raw = localStorage.getItem(STORAGE_KEY);\n if (!raw) return {};\n\n const parsed = JSON.parse(raw) as unknown;\n if (typeof parsed !== 'object' || parsed === null) return {};\n\n const normalized: ClosableStates = {};\n\n for (const [ctx, ctxState] of Object.entries(\n parsed as Record<string, unknown>,\n )) {\n if (typeof ctxState !== 'object' || ctxState === null) continue;\n\n normalized[ctx] = {};\n\n for (const [id, entry] of Object.entries(\n ctxState as Record<string, unknown>,\n )) {\n if (typeof entry === 'number') {\n // legacy format: plain close count\n normalized[ctx][id] = { count: entry, closedAt: 0 };\n } else if (typeof entry === 'object' && entry !== null) {\n const candidate = entry as Partial<ClosableState>;\n normalized[ctx][id] = {\n count: typeof candidate.count === 'number' ? candidate.count : 0,\n closedAt:\n typeof candidate.closedAt === 'number' ? candidate.closedAt : 0,\n };\n }\n }\n }\n\n return normalized;\n } catch {\n return {};\n }\n}\n\n/**\n * Persists closable states to localStorage.\n * Fails silently if localStorage is unavailable or full.\n */\nexport function setClosableStates(states: ClosableStates): void {\n try {\n localStorage.setItem(STORAGE_KEY, JSON.stringify(states));\n } catch {\n // localStorage unavailable or quota exceeded — application continues normally\n }\n}\n","import { useState, useEffect, useCallback } from 'react';\nimport {\n getClosableStates,\n setClosableStates,\n} from '../utils/closable-storage';\n\nexport interface UseClosableOptions {\n /** Notification context (e.g. 'awareness', 'charge') */\n context: string;\n /** Unique notification ID (campaignId). If absent, always visible — no close tracking. */\n id: string | undefined;\n /** Maximum number of times the user can close before it is hidden permanently */\n maxCloseTimes?: number;\n /** Milliseconds after which the close counter resets (optional) */\n expiresIn?: number;\n}\n\nexport interface UseClosableReturn {\n /** Whether the notification should be shown. null = initial check in progress. */\n isVisible: boolean | null;\n /** Call this to record a close action */\n close: () => void;\n}\n\n/**\n * Manages the close state of a notification using localStorage.\n *\n * After the user closes a notification `maxCloseTimes` times, it is hidden\n * permanently. Optionally, the counter resets after `expiresIn` milliseconds.\n *\n * Fails silently — errors in localStorage never break the application.\n *\n * @example\n * ```tsx\n * const { isVisible, close } = useClosable({\n * context: 'awareness',\n * id: 'awareness-2026-q1',\n * maxCloseTimes: 3,\n * expiresIn: 86_400_000, // 1 day\n * });\n *\n * if (!isVisible) return null;\n * return <Notification onClose={close} />;\n * ```\n */\nexport function useClosable({\n context,\n id,\n maxCloseTimes = 3,\n expiresIn,\n}: UseClosableOptions): UseClosableReturn {\n const [isVisible, setIsVisible] = useState<boolean | null>(null);\n\n useEffect(() => {\n // No ID means no close tracking — always show\n if (!id) {\n setIsVisible(true);\n return;\n }\n\n const states = getClosableStates();\n const entry = states[context]?.[id];\n\n if (!entry) {\n setIsVisible(true);\n return;\n }\n\n const { count, closedAt } = entry;\n\n // Check expiry — reset counter and show if expired\n if (expiresIn != null && closedAt > 0) {\n const elapsed = Date.now() - closedAt;\n if (elapsed > expiresIn) {\n const updated = { ...states };\n if (updated[context]) {\n delete updated[context][id];\n setClosableStates(updated);\n }\n setIsVisible(true);\n return;\n }\n }\n\n setIsVisible(count < maxCloseTimes);\n }, [context, id, maxCloseTimes, expiresIn]);\n\n const close = useCallback(() => {\n // Always hide visually\n setIsVisible(false);\n\n // Only persist to localStorage if there is an id to track\n if (!id) return;\n\n const states = getClosableStates();\n const currentCount = states[context]?.[id]?.count ?? 0;\n\n const updated: typeof states = {\n ...states,\n [context]: {\n ...(states[context] ?? {}),\n [id]: {\n count: currentCount + 1,\n closedAt: Date.now(),\n },\n },\n };\n\n setClosableStates(updated);\n }, [context, id]);\n\n return { isVisible, close };\n}\n","import type { CtaConfig } from '../types';\nimport { createLogger, type LoggerFn } from './logger';\n\n/**\n * Handle CTA click based on type\n *\n * @param cta - CTA configuration from LiveStateResponse\n * @param log - Optional logger (defaults to console.warn)\n */\nexport function handleCtaClick(cta: CtaConfig, log?: LoggerFn): void {\n const logger = createLogger(log);\n try {\n switch (cta.type) {\n case 'redirect':\n window.location.href = cta.url;\n break;\n\n case 'whatsapp': {\n const whatsappUrl = cta.whatsappMessage\n ? `${cta.url}?text=${encodeURIComponent(cta.whatsappMessage)}`\n : cta.url;\n window.open(whatsappUrl, '_blank');\n break;\n }\n\n case 'external':\n window.open(cta.url, '_blank');\n break;\n\n default:\n logger('warn', `Unknown CTA type: ${(cta as CtaConfig).type}`, { cta });\n }\n } catch (error) {\n logger('error', 'Failed to handle CTA click', { cta, error });\n }\n}\n","import { useCallback, useEffect } from 'react';\nimport { Alert, Box, Button, Text } from '@nimbus-ds/components';\nimport type { LiveStateResponse, TrackingConfig } from '../types';\nimport { buildTrackingProperties, buildEventName } from '../utils/analytics';\nimport { handleCtaClick } from '../utils/cta';\nimport { useTrackEvent } from '../hooks/useTrackEvent';\nimport { useLiveStateContext } from '../providers/LiveStateProvider';\n\nexport interface LiveStateAlertProps {\n data: LiveStateResponse;\n trackingConfig: TrackingConfig;\n /** When provided, shows a close button and calls this on dismiss */\n onClose?: () => void;\n /**\n * Override CTA click behaviour.\n * When provided, replaces default navigation so the caller controls what\n * happens on click (e.g. prevent navigation in a simulator, use a router).\n */\n onCtaClick?: (cta: import('../types').CtaConfig) => void;\n}\n\n/**\n * LiveStateAlert\n *\n * Renders an Alert component (Nimbus) for alert/warning types.\n * Automatically tracks view, click, and close events.\n * Shows a close button when onClose is provided.\n *\n * See examples/ directory for usage examples.\n */\nexport function LiveStateAlert({\n data,\n trackingConfig,\n onClose,\n onCtaClick,\n}: LiveStateAlertProps) {\n const track = useTrackEvent();\n const { log } = useLiveStateContext();\n const appearance = data.type === 'alert' ? 'danger' : 'warning';\n\n const buildEventProperties = useCallback(\n () =>\n buildTrackingProperties({\n context: data.context,\n campaignId: data.campaignId,\n group: data.group,\n ...data.metadata,\n ...trackingConfig.properties,\n }),\n [data, trackingConfig.properties],\n );\n\n const handleClick = useCallback(() => {\n const eventName = buildEventName(\n trackingConfig.prefix,\n trackingConfig.page,\n data.context,\n 'click',\n );\n track(eventName, buildEventProperties());\n if (onCtaClick) {\n onCtaClick(data.cta);\n } else {\n handleCtaClick(data.cta, log);\n }\n }, [data, trackingConfig, buildEventProperties, track, onCtaClick, log]);\n\n const handleClose = useCallback(() => {\n const eventName = buildEventName(\n trackingConfig.prefix,\n trackingConfig.page,\n data.context,\n 'close',\n );\n track(eventName, buildEventProperties());\n onClose?.();\n }, [onClose, data, trackingConfig, buildEventProperties, track]);\n\n useEffect(() => {\n const eventName = buildEventName(\n trackingConfig.prefix,\n trackingConfig.page,\n data.context,\n 'view',\n );\n track(eventName, buildEventProperties());\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return (\n <Alert\n appearance={appearance}\n title={data.title}\n onRemove={onClose ? handleClose : undefined}>\n <Box\n display=\"flex\"\n alignItems=\"flex-start\"\n gap=\"3\"\n flexWrap=\"wrap\"\n flexDirection=\"column\">\n <Box flex=\"1 1 auto\" minWidth=\"220px\">\n <Text>{data.message}</Text>\n </Box>\n\n <Box display=\"flex\" justifyContent=\"flex-end\" marginTop=\"2\">\n <Button appearance=\"primary\" onClick={handleClick}>\n {data.cta.label}\n </Button>\n </Box>\n </Box>\n </Alert>\n );\n}\n","import { useCallback, useEffect, useState } from 'react';\nimport { Card, Box, Text, Link, Icon } from '@nimbus-ds/components';\nimport { MoneyIcon, CloseIcon } from '@nimbus-ds/icons';\nimport type { LiveStateResponse, TrackingConfig } from '../types';\nimport { buildTrackingProperties, buildEventName } from '../utils/analytics';\nimport { handleCtaClick } from '../utils/cta';\nimport { useTrackEvent } from '../hooks/useTrackEvent';\nimport { useLiveStateContext } from '../providers/LiveStateProvider';\n\nexport interface LiveStateInfoProps {\n data: LiveStateResponse;\n trackingConfig: TrackingConfig;\n defaultVariant?: 'blue' | 'white';\n /** When provided, shows a close button and calls this on dismiss */\n onClose?: () => void;\n /**\n * Override mobile layout detection.\n * When provided, bypasses `window.innerWidth < 750` so simulators and\n * testing tools can force a specific layout without resizing the window.\n */\n isMobile?: boolean;\n /**\n * Override CTA click behaviour.\n * When provided, replaces default navigation so the caller controls what\n * happens on click (e.g. prevent navigation in a simulator, use a router).\n */\n onCtaClick?: (cta: import('../types').CtaConfig) => void;\n}\n\nconst ICON_COLOR_MAP = {\n blue: { background: '#0050C3', icon: '#FFFFFF' },\n white: { background: '#FFFFFF', icon: '#0059D5' },\n} as const;\n\n/**\n * LiveStateInfo\n *\n * Renders a Card component (Nimbus) for info type.\n * Uses MoneyIcon with colored circle background, Text components,\n * and a Link-style CTA — matching the LendingBanner design reference.\n * Automatically tracks view, click, and close events.\n * Shows a close button when onClose is provided.\n *\n * See examples/ directory for usage examples.\n */\nexport function LiveStateInfo({\n data,\n trackingConfig,\n defaultVariant = 'blue',\n onClose,\n isMobile: isMobileProp,\n onCtaClick,\n}: LiveStateInfoProps) {\n const track = useTrackEvent();\n const { log } = useLiveStateContext();\n const variant = data.variant || defaultVariant;\n const iconConfig = ICON_COLOR_MAP[variant];\n\n const [isMobileWindow, setIsMobileWindow] = useState(\n () => window.innerWidth < 750,\n );\n\n useEffect(() => {\n const handler = () => setIsMobileWindow(window.innerWidth < 750);\n window.addEventListener('resize', handler);\n return () => window.removeEventListener('resize', handler);\n }, []);\n\n // Prop overrides window detection — allows simulators/tests to force layout\n const isMobile = isMobileProp ?? isMobileWindow;\n\n const buildEventProperties = useCallback(\n () =>\n buildTrackingProperties({\n context: data.context,\n campaignId: data.campaignId,\n group: data.group,\n ...data.metadata,\n ...trackingConfig.properties,\n }),\n [data, trackingConfig.properties],\n );\n\n const handleClick = useCallback(() => {\n const eventName = buildEventName(\n trackingConfig.prefix,\n trackingConfig.page,\n data.context,\n 'click',\n );\n track(eventName, buildEventProperties());\n if (onCtaClick) {\n onCtaClick(data.cta);\n } else {\n handleCtaClick(data.cta, log);\n }\n }, [data, trackingConfig, buildEventProperties, track, onCtaClick, log]);\n\n const handleClose = useCallback(() => {\n const eventName = buildEventName(\n trackingConfig.prefix,\n trackingConfig.page,\n data.context,\n 'close',\n );\n track(eventName, buildEventProperties());\n onClose?.();\n }, [onClose, data, trackingConfig, buildEventProperties, track]);\n\n useEffect(() => {\n const eventName = buildEventName(\n trackingConfig.prefix,\n trackingConfig.page,\n data.context,\n 'view',\n );\n track(eventName, buildEventProperties());\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const ctaLink = (\n <Link\n as=\"a\"\n textDecoration=\"none\"\n onClick={handleClick}\n data-testid=\"live-state-cta-link\">\n <Text color=\"primary-interactive\" fontSize=\"base\">\n {data.cta.label}\n </Text>\n </Link>\n );\n\n return (\n <Card>\n <Box display=\"flex\" flexDirection=\"row\" justifyContent=\"space-between\">\n {/* Left: icon + text (+ cta on mobile) */}\n <Box\n display=\"flex\"\n gap=\"4\"\n alignItems={isMobile ? 'flex-start' : 'center'}\n paddingRight=\"1-5\">\n <Box minWidth=\"32px\">\n <div\n style={{\n width: 32,\n height: 32,\n borderRadius: '35%',\n borderColor: variant === 'white' ? '#E7E7E7' : 'transparent',\n borderWidth: 1,\n borderStyle: 'solid',\n background: iconConfig.background,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n }}>\n <MoneyIcon style={{ color: iconConfig.icon }} />\n </div>\n </Box>\n\n <Box display=\"grid\" gap=\"2\">\n <Box>\n <Text fontSize=\"base\" color=\"neutral-textHigh\">\n {data.title}\n </Text>\n <Text fontSize=\"base\" color=\"neutral-textLow\">\n {data.message}\n </Text>\n </Box>\n\n {/* CTA shown below text on mobile */}\n {isMobile && ctaLink}\n </Box>\n </Box>\n\n {/* Right: cta (desktop only) + close button */}\n {(!isMobile || onClose) && (\n <Box\n display=\"flex\"\n flexDirection={isMobile ? 'column' : 'row'}\n alignItems={isMobile ? 'flex-end' : 'center'}\n gap=\"2\">\n {/* CTA shown on the right side on desktop */}\n {!isMobile && ctaLink}\n\n {onClose && (\n <button\n type=\"button\"\n data-testid=\"live-state-close-button\"\n aria-label=\"Fechar notificação\"\n onClick={handleClose}\n style={{\n background: 'none',\n border: 'none',\n padding: 0,\n cursor: 'pointer',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n }}>\n <Icon\n source={<CloseIcon width=\"18px\" height=\"18px\" />}\n color=\"neutral-interactive\"\n />\n </button>\n )}\n </Box>\n )}\n </Box>\n </Card>\n );\n}\n","import type { LiveStateRendererProps } from '../types';\nimport { useClosable } from '../hooks/useClosable';\nimport { useLiveStateContext } from '../providers/LiveStateProvider';\nimport { LiveStateAlert } from './LiveStateAlert';\nimport { LiveStateInfo } from './LiveStateInfo';\n\n/**\n * LiveStateRenderer\n *\n * Main component that validates payload, filters by context, and renders\n * the appropriate visual component (Alert or Info) based on type.\n * Handles closable notifications via useClosable when closable is configured.\n *\n * See examples/ directory for usage examples.\n */\nexport function LiveStateRenderer({\n data,\n loading,\n trackingConfig,\n allowedContexts,\n defaultVariant,\n isMobile,\n onCtaClick,\n}: LiveStateRendererProps) {\n const { log } = useLiveStateContext();\n const { isVisible, close } = useClosable({\n context: data?.context ?? '',\n id: data?.campaignId,\n maxCloseTimes: data?.metadata?.maxCloseTimes,\n expiresIn: data?.metadata?.expiresIn,\n });\n\n // Don't render while loading or no data\n if (loading || !data) {\n return null;\n }\n\n // Validate required fields\n if (\n !data.context ||\n !data.type ||\n !data.title ||\n !data.message ||\n !data.cta?.label ||\n !data.cta?.url ||\n !data.cta?.type\n ) {\n log('warn', 'Invalid payload, missing required fields', { data });\n return null;\n }\n\n // Filter by allowed contexts\n if (allowedContexts && !allowedContexts.includes(data.context)) {\n return null;\n }\n\n // Awaiting localStorage check\n if (isVisible === null) {\n return null;\n }\n\n // Hidden — user reached maxCloseTimes\n if (!isVisible) {\n return null;\n }\n\n // Pass onClose whenever closable is true.\n // campaignId is optional — useClosable handles id=undefined gracefully\n // (always visible, no localStorage tracking).\n const onClose = data.closable ? close : undefined;\n\n // Render appropriate component based on type\n if (data.type === 'alert' || data.type === 'warning') {\n return (\n <LiveStateAlert\n data={data}\n onClose={onClose}\n trackingConfig={trackingConfig}\n onCtaClick={onCtaClick}\n />\n );\n }\n\n if (data.type === 'info') {\n return (\n <LiveStateInfo\n data={data}\n onClose={onClose}\n trackingConfig={trackingConfig}\n defaultVariant={defaultVariant}\n isMobile={isMobile}\n onCtaClick={onCtaClick}\n />\n );\n }\n\n log('warn', `Unknown type: ${data.type}`, { data });\n return null;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../src/hooks/useLiveState.ts","../src/hooks/useTrackEvent.ts","../src/utils/closable-storage.ts","../src/hooks/useClosable.ts","../src/utils/cta.ts","../src/utils/sanitize.ts","../src/components/RichText.tsx","../src/components/LiveStateAlert.tsx","../src/components/LiveStateInfo.tsx","../src/components/LiveStateRenderer.tsx"],"sourcesContent":["import useSWR from 'swr';\nimport { useLiveStateContext } from '../providers/LiveStateProvider';\nimport type { LiveStateResponse } from '../types';\n\ninterface UseLiveStateReturn {\n /** Live state data */\n liveState: LiveStateResponse | null;\n\n /** True while the first fetch is in progress */\n isLoading: boolean;\n\n /**\n * True when the last fetch attempt resulted in an error.\n * Use this to implement fallbacks or custom error logging.\n * Note: the lib already logs a warning to the console on every error.\n */\n isError: boolean;\n\n /**\n * The raw error from the last failed fetch, or null.\n * Pair with `isError` for more specific error handling.\n */\n error: Error | null;\n\n /** Manually trigger a re-fetch */\n refresh: () => Promise<void>;\n}\n\n/**\n * useLiveState hook\n *\n * Fetches and manages live state data using SWR with automatic caching,\n * deduplication, and revalidation.\n *\n * Must be used within LiveStateProvider. See examples/ directory for usage.\n */\nexport function useLiveState(): UseLiveStateReturn {\n const { fetcher, log } = useLiveStateContext();\n\n const { data, error, isLoading, mutate } = useSWR<LiveStateResponse | null>(\n 'live-state',\n fetcher,\n {\n revalidateOnFocus: false,\n revalidateOnReconnect: true,\n dedupingInterval: 60000, // 1 minute\n shouldRetryOnError: false, // Fail silently\n onError: err => {\n log('error', 'Failed to fetch live state', { error: err });\n },\n },\n );\n\n const refresh = async () => {\n try {\n await mutate();\n } catch (err) {\n log('error', 'Failed to refresh live state', { error: err });\n }\n };\n\n return {\n liveState: data ?? null,\n isLoading,\n isError: error != null,\n error: error ?? null,\n refresh,\n };\n}\n","import { useCallback } from 'react';\nimport { useLiveStateContext } from '../providers/LiveStateProvider';\nimport { trackEvent } from '../utils/analytics';\nimport type { TrackingProperties } from '../types';\n\n/**\n * useTrackEvent\n *\n * Returns a `track` function that fires both the internal analytics\n * (Amplitude + Clarity) and the optional `onEvent` callback provided\n * to LiveStateProvider — useful for simulators and debugging tools.\n */\nexport function useTrackEvent() {\n const { onEvent, disabled, log } = useLiveStateContext();\n\n const track = useCallback(\n (eventName: string, properties?: TrackingProperties) => {\n // onEvent callback fires even when disabled — it is an observability\n // hook, not an analytics SDK, so the caller opts in deliberately.\n onEvent?.(eventName, properties);\n\n // Skip SDK calls when analytics are disabled (e.g. dev/test environments)\n if (!disabled) {\n trackEvent(eventName, properties, log);\n }\n },\n [onEvent, disabled, log],\n );\n\n return track;\n}\n","export const LIVE_STATE_STORAGE_KEY = '@tiendanube/live-state:closable';\n\n/** @internal */\nconst STORAGE_KEY = LIVE_STATE_STORAGE_KEY;\n\nexport interface ClosableState {\n count: number;\n closedAt: number;\n}\n\nexport type ClosableStates = Record<string, Record<string, ClosableState>>;\n\n/**\n * Reads closable states from localStorage.\n * Normalises legacy format (plain number) to the current shape ({ count, closedAt }).\n * Returns an empty object on any error (fail-silent).\n */\nexport function getClosableStates(): ClosableStates {\n try {\n const raw = localStorage.getItem(STORAGE_KEY);\n if (!raw) return {};\n\n const parsed = JSON.parse(raw) as unknown;\n if (typeof parsed !== 'object' || parsed === null) return {};\n\n const normalized: ClosableStates = {};\n\n for (const [ctx, ctxState] of Object.entries(\n parsed as Record<string, unknown>,\n )) {\n if (typeof ctxState !== 'object' || ctxState === null) continue;\n\n normalized[ctx] = {};\n\n for (const [id, entry] of Object.entries(\n ctxState as Record<string, unknown>,\n )) {\n if (typeof entry === 'number') {\n // legacy format: plain close count\n normalized[ctx][id] = { count: entry, closedAt: 0 };\n } else if (typeof entry === 'object' && entry !== null) {\n const candidate = entry as Partial<ClosableState>;\n normalized[ctx][id] = {\n count: typeof candidate.count === 'number' ? candidate.count : 0,\n closedAt:\n typeof candidate.closedAt === 'number' ? candidate.closedAt : 0,\n };\n }\n }\n }\n\n return normalized;\n } catch {\n return {};\n }\n}\n\n/**\n * Persists closable states to localStorage.\n * Fails silently if localStorage is unavailable or full.\n */\nexport function setClosableStates(states: ClosableStates): void {\n try {\n localStorage.setItem(STORAGE_KEY, JSON.stringify(states));\n } catch {\n // localStorage unavailable or quota exceeded — application continues normally\n }\n}\n","import { useState, useEffect, useCallback } from 'react';\nimport {\n getClosableStates,\n setClosableStates,\n} from '../utils/closable-storage';\n\nexport interface UseClosableOptions {\n /** Notification context (e.g. 'awareness', 'charge') */\n context: string;\n /** Unique notification ID (campaignId). If absent, always visible — no close tracking. */\n id: string | undefined;\n /** Maximum number of times the user can close before it is hidden permanently */\n maxCloseTimes?: number;\n /** Milliseconds after which the close counter resets (optional) */\n expiresIn?: number;\n}\n\nexport interface UseClosableReturn {\n /** Whether the notification should be shown. null = initial check in progress. */\n isVisible: boolean | null;\n /** Call this to record a close action */\n close: () => void;\n}\n\n/**\n * Manages the close state of a notification using localStorage.\n *\n * After the user closes a notification `maxCloseTimes` times, it is hidden\n * permanently. Optionally, the counter resets after `expiresIn` milliseconds.\n *\n * Fails silently — errors in localStorage never break the application.\n *\n * @example\n * ```tsx\n * const { isVisible, close } = useClosable({\n * context: 'awareness',\n * id: 'awareness-2026-q1',\n * maxCloseTimes: 3,\n * expiresIn: 86_400_000, // 1 day\n * });\n *\n * if (!isVisible) return null;\n * return <Notification onClose={close} />;\n * ```\n */\nexport function useClosable({\n context,\n id,\n maxCloseTimes = 3,\n expiresIn,\n}: UseClosableOptions): UseClosableReturn {\n const [isVisible, setIsVisible] = useState<boolean | null>(null);\n\n useEffect(() => {\n // No ID means no close tracking — always show\n if (!id) {\n setIsVisible(true);\n return;\n }\n\n const states = getClosableStates();\n const entry = states[context]?.[id];\n\n if (!entry) {\n setIsVisible(true);\n return;\n }\n\n const { count, closedAt } = entry;\n\n // Check expiry — reset counter and show if expired\n if (expiresIn != null && closedAt > 0) {\n const elapsed = Date.now() - closedAt;\n if (elapsed > expiresIn) {\n const updated = { ...states };\n if (updated[context]) {\n delete updated[context][id];\n setClosableStates(updated);\n }\n setIsVisible(true);\n return;\n }\n }\n\n setIsVisible(count < maxCloseTimes);\n }, [context, id, maxCloseTimes, expiresIn]);\n\n const close = useCallback(() => {\n // Always hide visually\n setIsVisible(false);\n\n // Only persist to localStorage if there is an id to track\n if (!id) return;\n\n const states = getClosableStates();\n const currentCount = states[context]?.[id]?.count ?? 0;\n\n const updated: typeof states = {\n ...states,\n [context]: {\n ...(states[context] ?? {}),\n [id]: {\n count: currentCount + 1,\n closedAt: Date.now(),\n },\n },\n };\n\n setClosableStates(updated);\n }, [context, id]);\n\n return { isVisible, close };\n}\n","import type { CtaConfig } from '../types';\nimport { createLogger, type LoggerFn } from './logger';\n\n/**\n * Handle CTA click based on type\n *\n * @param cta - CTA configuration from LiveStateResponse\n * @param log - Optional logger (defaults to console.warn)\n */\nexport function handleCtaClick(cta: CtaConfig, log?: LoggerFn): void {\n const logger = createLogger(log);\n try {\n switch (cta.type) {\n case 'redirect':\n window.location.href = cta.url;\n break;\n\n case 'whatsapp': {\n const whatsappUrl = cta.whatsappMessage\n ? `${cta.url}?text=${encodeURIComponent(cta.whatsappMessage)}`\n : cta.url;\n window.open(whatsappUrl, '_blank');\n break;\n }\n\n case 'external':\n window.open(cta.url, '_blank');\n break;\n\n default:\n logger('warn', `Unknown CTA type: ${(cta as CtaConfig).type}`, { cta });\n }\n } catch (error) {\n logger('error', 'Failed to handle CTA click', { cta, error });\n }\n}\n","/**\n * Minimal HTML sanitiser for notification content.\n *\n * Allows only a strict allowlist of inline formatting tags that are expected\n * in notification messages (strong, em, b, i, br, span). All other tags and\n * attributes are stripped, preventing XSS even if the backend is compromised.\n *\n * This intentionally avoids a runtime dependency on DOMPurify or similar\n * libraries — the allowlist is narrow enough to be maintained safely here.\n */\n\n/** Tags that are safe to render inside notification text */\nconst ALLOWED_TAGS = new Set(['strong', 'em', 'b', 'i', 'br', 'span']);\n\n/**\n * Tags whose entire content (open tag + inner text + close tag) must be\n * removed, not just the surrounding tags. This prevents script injection\n * even if the backend is compromised.\n */\nconst BLOCK_TAGS_RE =\n /<(script|style|iframe|object|embed|form|input|button|textarea|select|link|meta|head|body|html|svg|math)[\\s\\S]*?<\\/\\1\\s*>|<(script|style|iframe|object|embed|form|input|button|textarea|select|link|meta|head|body|html|svg|math)[^>]*\\/?>/gi;\n\n/**\n * Strips all HTML tags except those in ALLOWED_TAGS.\n * Attributes are removed from all allowed tags to prevent event-handler\n * injection (e.g. `<span onclick=\"...\">`).\n * Dangerous tags (script, style, iframe, …) have their entire content removed.\n *\n * Returns the sanitised HTML string, safe to pass to `dangerouslySetInnerHTML`.\n */\nexport function sanitizeHtml(input: string): string {\n // Step 1: remove dangerous tags and their inner content entirely\n const withoutDangerous = input.replace(BLOCK_TAGS_RE, '');\n\n // Step 2: remove all attributes from every remaining tag\n const withoutAttributes = withoutDangerous.replace(\n /<([a-zA-Z][a-zA-Z0-9]*)\\s[^>]*>/g,\n '<$1>',\n );\n\n // Step 3: strip any remaining tag that is not in the allowlist\n return withoutAttributes.replace(\n /<\\/?([a-zA-Z][a-zA-Z0-9]*)[^>]*>/g,\n (match, tag: string) => (ALLOWED_TAGS.has(tag.toLowerCase()) ? match : ''),\n );\n}\n\n/**\n * Returns true when the string contains at least one HTML tag,\n * indicating it should be rendered as rich text.\n */\nexport function containsHtml(input: string): boolean {\n return /<[a-zA-Z][^>]*>/.test(input);\n}\n","import { Text } from '@nimbus-ds/components';\nimport type { ComponentProps } from 'react';\nimport { sanitizeHtml, containsHtml } from '../utils/sanitize';\n\ntype TextProps = ComponentProps<typeof Text>;\n\ninterface RichTextProps extends Omit<TextProps, 'children'> {\n content: string;\n}\n\n/**\n * RichText\n *\n * Renders a text string that may contain inline HTML formatting (e.g. <strong>,\n * <em>, <br>). When HTML is detected, the content is sanitised through a strict\n * allowlist before being injected via dangerouslySetInnerHTML. Plain strings\n * are rendered via the Nimbus Text component without any DOM injection.\n *\n * Allowed tags: strong, em, b, i, br, span. All attributes are stripped.\n */\nexport function RichText({ content, ...textProps }: RichTextProps) {\n if (containsHtml(content)) {\n const safe = sanitizeHtml(content);\n return (\n <Text {...textProps}>\n {/* eslint-disable-next-line react/no-danger */}\n <span dangerouslySetInnerHTML={{ __html: safe }} />\n </Text>\n );\n }\n\n return <Text {...textProps}>{content}</Text>;\n}\n","import { useCallback, useEffect } from 'react';\nimport { Alert, Box, Button } from '@nimbus-ds/components';\nimport type { LiveStateResponse, TrackingConfig } from '../types';\nimport { buildTrackingProperties, buildEventName } from '../utils/analytics';\nimport { handleCtaClick } from '../utils/cta';\nimport { useTrackEvent } from '../hooks/useTrackEvent';\nimport { useLiveStateContext } from '../providers/LiveStateProvider';\nimport { RichText } from './RichText';\n\nexport interface LiveStateAlertProps {\n data: LiveStateResponse;\n trackingConfig: TrackingConfig;\n /** When provided, shows a close button and calls this on dismiss */\n onClose?: () => void;\n /**\n * Override CTA click behaviour.\n * When provided, replaces default navigation so the caller controls what\n * happens on click (e.g. prevent navigation in a simulator, use a router).\n */\n onCtaClick?: (cta: import('../types').CtaConfig) => void;\n}\n\n/**\n * LiveStateAlert\n *\n * Renders an Alert component (Nimbus) for alert/warning types.\n * Automatically tracks view, click, and close events.\n * Shows a close button when onClose is provided.\n *\n * See examples/ directory for usage examples.\n */\nexport function LiveStateAlert({\n data,\n trackingConfig,\n onClose,\n onCtaClick,\n}: LiveStateAlertProps) {\n const track = useTrackEvent();\n const { log } = useLiveStateContext();\n const appearance = data.type === 'alert' ? 'danger' : 'warning';\n\n const buildEventProperties = useCallback(\n () =>\n buildTrackingProperties({\n context: data.context,\n campaignId: data.campaignId,\n group: data.group,\n ...data.metadata,\n ...trackingConfig.properties,\n }),\n [data, trackingConfig.properties],\n );\n\n const handleClick = useCallback(() => {\n const eventName = buildEventName(\n trackingConfig.prefix,\n trackingConfig.page,\n data.context,\n 'click',\n );\n track(eventName, buildEventProperties());\n if (onCtaClick) {\n onCtaClick(data.cta);\n } else {\n handleCtaClick(data.cta, log);\n }\n }, [data, trackingConfig, buildEventProperties, track, onCtaClick, log]);\n\n const handleClose = useCallback(() => {\n const eventName = buildEventName(\n trackingConfig.prefix,\n trackingConfig.page,\n data.context,\n 'close',\n );\n track(eventName, buildEventProperties());\n onClose?.();\n }, [onClose, data, trackingConfig, buildEventProperties, track]);\n\n useEffect(() => {\n const eventName = buildEventName(\n trackingConfig.prefix,\n trackingConfig.page,\n data.context,\n 'view',\n );\n track(eventName, buildEventProperties());\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return (\n <Alert\n appearance={appearance}\n title={data.title}\n onRemove={onClose ? handleClose : undefined}>\n <Box\n display=\"flex\"\n alignItems=\"flex-start\"\n gap=\"3\"\n flexWrap=\"wrap\"\n flexDirection=\"column\">\n <Box flex=\"1 1 auto\" minWidth=\"220px\">\n <RichText content={data.message} />\n </Box>\n\n <Box display=\"flex\" justifyContent=\"flex-end\" marginTop=\"2\">\n <Button appearance=\"primary\" onClick={handleClick}>\n {data.cta.label}\n </Button>\n </Box>\n </Box>\n </Alert>\n );\n}\n","import { useCallback, useEffect, useState } from 'react';\nimport { Card, Box, Text, Link, Icon } from '@nimbus-ds/components';\nimport { MoneyIcon, CloseIcon } from '@nimbus-ds/icons';\nimport type { LiveStateResponse, TrackingConfig } from '../types';\nimport { buildTrackingProperties, buildEventName } from '../utils/analytics';\nimport { handleCtaClick } from '../utils/cta';\nimport { useTrackEvent } from '../hooks/useTrackEvent';\nimport { useLiveStateContext } from '../providers/LiveStateProvider';\nimport { RichText } from './RichText';\n\nexport interface LiveStateInfoProps {\n data: LiveStateResponse;\n trackingConfig: TrackingConfig;\n defaultVariant?: 'blue' | 'white';\n /** When provided, shows a close button and calls this on dismiss */\n onClose?: () => void;\n /**\n * Override mobile layout detection.\n * When provided, bypasses `window.innerWidth < 750` so simulators and\n * testing tools can force a specific layout without resizing the window.\n */\n isMobile?: boolean;\n /**\n * Override CTA click behaviour.\n * When provided, replaces default navigation so the caller controls what\n * happens on click (e.g. prevent navigation in a simulator, use a router).\n */\n onCtaClick?: (cta: import('../types').CtaConfig) => void;\n}\n\nconst ICON_COLOR_MAP = {\n blue: { background: '#0050C3', icon: '#FFFFFF' },\n white: { background: '#FFFFFF', icon: '#0059D5' },\n} as const;\n\n/**\n * LiveStateInfo\n *\n * Renders a Card component (Nimbus) for info type.\n * Uses MoneyIcon with colored circle background, Text components,\n * and a Link-style CTA — matching the LendingBanner design reference.\n * Automatically tracks view, click, and close events.\n * Shows a close button when onClose is provided.\n *\n * See examples/ directory for usage examples.\n */\nexport function LiveStateInfo({\n data,\n trackingConfig,\n defaultVariant = 'blue',\n onClose,\n isMobile: isMobileProp,\n onCtaClick,\n}: LiveStateInfoProps) {\n const track = useTrackEvent();\n const { log } = useLiveStateContext();\n const variant = data.variant || defaultVariant;\n const iconConfig = ICON_COLOR_MAP[variant];\n\n const [isMobileWindow, setIsMobileWindow] = useState(\n () => window.innerWidth < 750,\n );\n\n useEffect(() => {\n const handler = () => setIsMobileWindow(window.innerWidth < 750);\n window.addEventListener('resize', handler);\n return () => window.removeEventListener('resize', handler);\n }, []);\n\n // Prop overrides window detection — allows simulators/tests to force layout\n const isMobile = isMobileProp ?? isMobileWindow;\n\n const buildEventProperties = useCallback(\n () =>\n buildTrackingProperties({\n context: data.context,\n campaignId: data.campaignId,\n group: data.group,\n ...data.metadata,\n ...trackingConfig.properties,\n }),\n [data, trackingConfig.properties],\n );\n\n const handleClick = useCallback(() => {\n const eventName = buildEventName(\n trackingConfig.prefix,\n trackingConfig.page,\n data.context,\n 'click',\n );\n track(eventName, buildEventProperties());\n if (onCtaClick) {\n onCtaClick(data.cta);\n } else {\n handleCtaClick(data.cta, log);\n }\n }, [data, trackingConfig, buildEventProperties, track, onCtaClick, log]);\n\n const handleClose = useCallback(() => {\n const eventName = buildEventName(\n trackingConfig.prefix,\n trackingConfig.page,\n data.context,\n 'close',\n );\n track(eventName, buildEventProperties());\n onClose?.();\n }, [onClose, data, trackingConfig, buildEventProperties, track]);\n\n useEffect(() => {\n const eventName = buildEventName(\n trackingConfig.prefix,\n trackingConfig.page,\n data.context,\n 'view',\n );\n track(eventName, buildEventProperties());\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const ctaLink = (\n <Link\n as=\"a\"\n textDecoration=\"none\"\n onClick={handleClick}\n data-testid=\"live-state-cta-link\">\n <Text color=\"primary-interactive\" fontSize=\"base\">\n {data.cta.label}\n </Text>\n </Link>\n );\n\n return (\n <Card>\n <Box display=\"flex\" flexDirection=\"row\" justifyContent=\"space-between\">\n {/* Left: icon + text (+ cta on mobile) */}\n <Box\n display=\"flex\"\n gap=\"4\"\n alignItems={isMobile ? 'flex-start' : 'center'}\n paddingRight=\"1-5\">\n <Box minWidth=\"32px\">\n <div\n style={{\n width: 32,\n height: 32,\n borderRadius: '35%',\n borderColor: variant === 'white' ? '#E7E7E7' : 'transparent',\n borderWidth: 1,\n borderStyle: 'solid',\n background: iconConfig.background,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n }}>\n <MoneyIcon style={{ color: iconConfig.icon }} />\n </div>\n </Box>\n\n <Box display=\"grid\" gap=\"2\">\n <Box>\n <RichText content={data.title} fontSize=\"base\" color=\"neutral-textHigh\" />\n <RichText content={data.message} fontSize=\"base\" color=\"neutral-textLow\" />\n </Box>\n\n {/* CTA shown below text on mobile */}\n {isMobile && ctaLink}\n </Box>\n </Box>\n\n {/* Right: cta (desktop only) + close button */}\n {(!isMobile || onClose) && (\n <Box\n display=\"flex\"\n flexDirection={isMobile ? 'column' : 'row'}\n alignItems={isMobile ? 'flex-end' : 'center'}\n gap=\"2\">\n {/* CTA shown on the right side on desktop */}\n {!isMobile && ctaLink}\n\n {onClose && (\n <button\n type=\"button\"\n data-testid=\"live-state-close-button\"\n aria-label=\"Fechar notificação\"\n onClick={handleClose}\n style={{\n background: 'none',\n border: 'none',\n padding: 0,\n cursor: 'pointer',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n }}>\n <Icon\n source={<CloseIcon width=\"18px\" height=\"18px\" />}\n color=\"neutral-interactive\"\n />\n </button>\n )}\n </Box>\n )}\n </Box>\n </Card>\n );\n}\n","import type { LiveStateRendererProps } from '../types';\nimport { useClosable } from '../hooks/useClosable';\nimport { useLiveStateContext } from '../providers/LiveStateProvider';\nimport { LiveStateAlert } from './LiveStateAlert';\nimport { LiveStateInfo } from './LiveStateInfo';\n\n/**\n * LiveStateRenderer\n *\n * Main component that validates payload, filters by context, and renders\n * the appropriate visual component (Alert or Info) based on type.\n * Handles closable notifications via useClosable when closable is configured.\n *\n * See examples/ directory for usage examples.\n */\nexport function LiveStateRenderer({\n data,\n loading,\n trackingConfig,\n allowedContexts,\n defaultVariant,\n isMobile,\n onCtaClick,\n}: LiveStateRendererProps) {\n const { log } = useLiveStateContext();\n const { isVisible, close } = useClosable({\n context: data?.context ?? '',\n id: data?.campaignId,\n maxCloseTimes: data?.metadata?.maxCloseTimes,\n expiresIn: data?.metadata?.expiresIn,\n });\n\n // Don't render while loading or no data\n if (loading || !data) {\n return null;\n }\n\n // Validate required fields\n if (\n !data.context ||\n !data.type ||\n !data.title ||\n !data.message ||\n !data.cta?.label ||\n !data.cta?.url ||\n !data.cta?.type\n ) {\n log('warn', 'Invalid payload, missing required fields', { data });\n return null;\n }\n\n // Filter by allowed contexts\n if (allowedContexts && !allowedContexts.includes(data.context)) {\n return null;\n }\n\n // Awaiting localStorage check\n if (isVisible === null) {\n return null;\n }\n\n // Hidden — user reached maxCloseTimes\n if (!isVisible) {\n return null;\n }\n\n // Pass onClose whenever closable is true.\n // campaignId is optional — useClosable handles id=undefined gracefully\n // (always visible, no localStorage tracking).\n const onClose = data.closable ? close : undefined;\n\n // Render appropriate component based on type\n if (data.type === 'alert' || data.type === 'warning') {\n return (\n <LiveStateAlert\n data={data}\n onClose={onClose}\n trackingConfig={trackingConfig}\n onCtaClick={onCtaClick}\n />\n );\n }\n\n if (data.type === 'info') {\n return (\n <LiveStateInfo\n data={data}\n onClose={onClose}\n trackingConfig={trackingConfig}\n defaultVariant={defaultVariant}\n isMobile={isMobile}\n onCtaClick={onCtaClick}\n />\n );\n }\n\n log('warn', `Unknown type: ${data.type}`, { data });\n return null;\n}\n"],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -146,15 +146,46 @@ function O(e, t) {
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
//#endregion
|
|
149
|
+
//#region src/utils/sanitize.ts
|
|
150
|
+
var k = new Set([
|
|
151
|
+
"strong",
|
|
152
|
+
"em",
|
|
153
|
+
"b",
|
|
154
|
+
"i",
|
|
155
|
+
"br",
|
|
156
|
+
"span"
|
|
157
|
+
]), A = /<(script|style|iframe|object|embed|form|input|button|textarea|select|link|meta|head|body|html|svg|math)[\s\S]*?<\/\1\s*>|<(script|style|iframe|object|embed|form|input|button|textarea|select|link|meta|head|body|html|svg|math)[^>]*\/?>/gi;
|
|
158
|
+
function j(e) {
|
|
159
|
+
return e.replace(A, "").replace(/<([a-zA-Z][a-zA-Z0-9]*)\s[^>]*>/g, "<$1>").replace(/<\/?([a-zA-Z][a-zA-Z0-9]*)[^>]*>/g, (e, t) => k.has(t.toLowerCase()) ? e : "");
|
|
160
|
+
}
|
|
161
|
+
function M(e) {
|
|
162
|
+
return /<[a-zA-Z][^>]*>/.test(e);
|
|
163
|
+
}
|
|
164
|
+
//#endregion
|
|
165
|
+
//#region src/components/RichText.tsx
|
|
166
|
+
function N({ content: e, ...t }) {
|
|
167
|
+
if (M(e)) {
|
|
168
|
+
let n = j(e);
|
|
169
|
+
return /* @__PURE__ */ u(v, {
|
|
170
|
+
...t,
|
|
171
|
+
children: /* @__PURE__ */ u("span", { dangerouslySetInnerHTML: { __html: n } })
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return /* @__PURE__ */ u(v, {
|
|
175
|
+
...t,
|
|
176
|
+
children: e
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
//#endregion
|
|
149
180
|
//#region src/components/LiveStateAlert.tsx
|
|
150
|
-
function
|
|
181
|
+
function P({ data: e, trackingConfig: r, onClose: a, onCtaClick: c }) {
|
|
151
182
|
let l = S(), { log: h } = n(), g = e.type === "alert" ? "danger" : "warning", _ = o(() => t({
|
|
152
183
|
context: e.context,
|
|
153
184
|
campaignId: e.campaignId,
|
|
154
185
|
group: e.group,
|
|
155
186
|
...e.metadata,
|
|
156
187
|
...r.properties
|
|
157
|
-
}), [e, r.properties]),
|
|
188
|
+
}), [e, r.properties]), v = o(() => {
|
|
158
189
|
l(i(r.prefix, r.page, e.context, "click"), _()), c ? c(e.cta) : O(e.cta, h);
|
|
159
190
|
}, [
|
|
160
191
|
e,
|
|
@@ -163,7 +194,7 @@ function k({ data: e, trackingConfig: r, onClose: a, onCtaClick: c }) {
|
|
|
163
194
|
l,
|
|
164
195
|
c,
|
|
165
196
|
h
|
|
166
|
-
]),
|
|
197
|
+
]), y = o(() => {
|
|
167
198
|
l(i(r.prefix, r.page, e.context, "close"), _()), a?.();
|
|
168
199
|
}, [
|
|
169
200
|
a,
|
|
@@ -177,7 +208,7 @@ function k({ data: e, trackingConfig: r, onClose: a, onCtaClick: c }) {
|
|
|
177
208
|
}, []), /* @__PURE__ */ u(f, {
|
|
178
209
|
appearance: g,
|
|
179
210
|
title: e.title,
|
|
180
|
-
onRemove: a ?
|
|
211
|
+
onRemove: a ? y : void 0,
|
|
181
212
|
children: /* @__PURE__ */ d(p, {
|
|
182
213
|
display: "flex",
|
|
183
214
|
alignItems: "flex-start",
|
|
@@ -187,14 +218,14 @@ function k({ data: e, trackingConfig: r, onClose: a, onCtaClick: c }) {
|
|
|
187
218
|
children: [/* @__PURE__ */ u(p, {
|
|
188
219
|
flex: "1 1 auto",
|
|
189
220
|
minWidth: "220px",
|
|
190
|
-
children: /* @__PURE__ */ u(
|
|
221
|
+
children: /* @__PURE__ */ u(N, { content: e.message })
|
|
191
222
|
}), /* @__PURE__ */ u(p, {
|
|
192
223
|
display: "flex",
|
|
193
224
|
justifyContent: "flex-end",
|
|
194
225
|
marginTop: "2",
|
|
195
226
|
children: /* @__PURE__ */ u(m, {
|
|
196
227
|
appearance: "primary",
|
|
197
|
-
onClick:
|
|
228
|
+
onClick: v,
|
|
198
229
|
children: e.cta.label
|
|
199
230
|
})
|
|
200
231
|
})]
|
|
@@ -203,7 +234,7 @@ function k({ data: e, trackingConfig: r, onClose: a, onCtaClick: c }) {
|
|
|
203
234
|
}
|
|
204
235
|
//#endregion
|
|
205
236
|
//#region src/components/LiveStateInfo.tsx
|
|
206
|
-
var
|
|
237
|
+
var F = {
|
|
207
238
|
blue: {
|
|
208
239
|
background: "#0050C3",
|
|
209
240
|
icon: "#FFFFFF"
|
|
@@ -213,43 +244,43 @@ var A = {
|
|
|
213
244
|
icon: "#0059D5"
|
|
214
245
|
}
|
|
215
246
|
};
|
|
216
|
-
function
|
|
217
|
-
let x = S(), { log: C } = n(), w = e.variant || a, T =
|
|
247
|
+
function I({ data: e, trackingConfig: r, defaultVariant: a = "blue", onClose: l, isMobile: f, onCtaClick: m }) {
|
|
248
|
+
let x = S(), { log: C } = n(), w = e.variant || a, T = F[w], [E, D] = c(() => window.innerWidth < 750);
|
|
218
249
|
s(() => {
|
|
219
250
|
let e = () => D(window.innerWidth < 750);
|
|
220
251
|
return window.addEventListener("resize", e), () => window.removeEventListener("resize", e);
|
|
221
252
|
}, []);
|
|
222
|
-
let k = f ?? E,
|
|
253
|
+
let k = f ?? E, A = o(() => t({
|
|
223
254
|
context: e.context,
|
|
224
255
|
campaignId: e.campaignId,
|
|
225
256
|
group: e.group,
|
|
226
257
|
...e.metadata,
|
|
227
258
|
...r.properties
|
|
228
|
-
}), [e, r.properties]),
|
|
229
|
-
x(i(r.prefix, r.page, e.context, "click"),
|
|
259
|
+
}), [e, r.properties]), j = o(() => {
|
|
260
|
+
x(i(r.prefix, r.page, e.context, "click"), A()), m ? m(e.cta) : O(e.cta, C);
|
|
230
261
|
}, [
|
|
231
262
|
e,
|
|
232
263
|
r,
|
|
233
|
-
|
|
264
|
+
A,
|
|
234
265
|
x,
|
|
235
266
|
m,
|
|
236
267
|
C
|
|
237
|
-
]),
|
|
238
|
-
x(i(r.prefix, r.page, e.context, "close"),
|
|
268
|
+
]), M = o(() => {
|
|
269
|
+
x(i(r.prefix, r.page, e.context, "close"), A()), l?.();
|
|
239
270
|
}, [
|
|
240
271
|
l,
|
|
241
272
|
e,
|
|
242
273
|
r,
|
|
243
|
-
|
|
274
|
+
A,
|
|
244
275
|
x
|
|
245
276
|
]);
|
|
246
277
|
s(() => {
|
|
247
|
-
x(i(r.prefix, r.page, e.context, "view"),
|
|
278
|
+
x(i(r.prefix, r.page, e.context, "view"), A());
|
|
248
279
|
}, []);
|
|
249
280
|
let P = /* @__PURE__ */ u(_, {
|
|
250
281
|
as: "a",
|
|
251
282
|
textDecoration: "none",
|
|
252
|
-
onClick:
|
|
283
|
+
onClick: j,
|
|
253
284
|
"data-testid": "live-state-cta-link",
|
|
254
285
|
children: /* @__PURE__ */ u(v, {
|
|
255
286
|
color: "primary-interactive",
|
|
@@ -286,14 +317,14 @@ function j({ data: e, trackingConfig: r, defaultVariant: a = "blue", onClose: l,
|
|
|
286
317
|
}), /* @__PURE__ */ d(p, {
|
|
287
318
|
display: "grid",
|
|
288
319
|
gap: "2",
|
|
289
|
-
children: [/* @__PURE__ */ d(p, { children: [/* @__PURE__ */ u(
|
|
320
|
+
children: [/* @__PURE__ */ d(p, { children: [/* @__PURE__ */ u(N, {
|
|
321
|
+
content: e.title,
|
|
290
322
|
fontSize: "base",
|
|
291
|
-
color: "neutral-textHigh"
|
|
292
|
-
|
|
293
|
-
|
|
323
|
+
color: "neutral-textHigh"
|
|
324
|
+
}), /* @__PURE__ */ u(N, {
|
|
325
|
+
content: e.message,
|
|
294
326
|
fontSize: "base",
|
|
295
|
-
color: "neutral-textLow"
|
|
296
|
-
children: e.message
|
|
327
|
+
color: "neutral-textLow"
|
|
297
328
|
})] }), k && P]
|
|
298
329
|
})]
|
|
299
330
|
}), (!k || l) && /* @__PURE__ */ d(p, {
|
|
@@ -305,7 +336,7 @@ function j({ data: e, trackingConfig: r, defaultVariant: a = "blue", onClose: l,
|
|
|
305
336
|
type: "button",
|
|
306
337
|
"data-testid": "live-state-close-button",
|
|
307
338
|
"aria-label": "Fechar notificação",
|
|
308
|
-
onClick:
|
|
339
|
+
onClick: M,
|
|
309
340
|
style: {
|
|
310
341
|
background: "none",
|
|
311
342
|
border: "none",
|
|
@@ -328,7 +359,7 @@ function j({ data: e, trackingConfig: r, defaultVariant: a = "blue", onClose: l,
|
|
|
328
359
|
}
|
|
329
360
|
//#endregion
|
|
330
361
|
//#region src/components/LiveStateRenderer.tsx
|
|
331
|
-
function
|
|
362
|
+
function L({ data: e, loading: t, trackingConfig: r, allowedContexts: i, defaultVariant: a, isMobile: o, onCtaClick: s }) {
|
|
332
363
|
let { log: c } = n(), { isVisible: l, close: d } = D({
|
|
333
364
|
context: e?.context ?? "",
|
|
334
365
|
id: e?.campaignId,
|
|
@@ -339,12 +370,12 @@ function M({ data: e, loading: t, trackingConfig: r, allowedContexts: i, default
|
|
|
339
370
|
if (!e.context || !e.type || !e.title || !e.message || !e.cta?.label || !e.cta?.url || !e.cta?.type) return c("warn", "Invalid payload, missing required fields", { data: e }), null;
|
|
340
371
|
if (i && !i.includes(e.context) || l === null || !l) return null;
|
|
341
372
|
let f = e.closable ? d : void 0;
|
|
342
|
-
return e.type === "alert" || e.type === "warning" ? /* @__PURE__ */ u(
|
|
373
|
+
return e.type === "alert" || e.type === "warning" ? /* @__PURE__ */ u(P, {
|
|
343
374
|
data: e,
|
|
344
375
|
onClose: f,
|
|
345
376
|
trackingConfig: r,
|
|
346
377
|
onCtaClick: s
|
|
347
|
-
}) : e.type === "info" ? /* @__PURE__ */ u(
|
|
378
|
+
}) : e.type === "info" ? /* @__PURE__ */ u(I, {
|
|
348
379
|
data: e,
|
|
349
380
|
onClose: f,
|
|
350
381
|
trackingConfig: r,
|
|
@@ -354,6 +385,6 @@ function M({ data: e, loading: t, trackingConfig: r, allowedContexts: i, default
|
|
|
354
385
|
}) : (c("warn", `Unknown type: ${e.type}`, { data: e }), null);
|
|
355
386
|
}
|
|
356
387
|
//#endregion
|
|
357
|
-
export { C as LIVE_STATE_STORAGE_KEY,
|
|
388
|
+
export { C as LIVE_STATE_STORAGE_KEY, P as LiveStateAlert, I as LiveStateInfo, a as LiveStateProvider, L as LiveStateRenderer, O as handleCtaClick, e as trackEvent, x as useLiveState, S as useTrackEvent };
|
|
358
389
|
|
|
359
390
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/hooks/useLiveState.ts","../src/hooks/useTrackEvent.ts","../src/utils/closable-storage.ts","../src/hooks/useClosable.ts","../src/utils/cta.ts","../src/components/LiveStateAlert.tsx","../src/components/LiveStateInfo.tsx","../src/components/LiveStateRenderer.tsx"],"sourcesContent":["import useSWR from 'swr';\nimport { useLiveStateContext } from '../providers/LiveStateProvider';\nimport type { LiveStateResponse } from '../types';\n\ninterface UseLiveStateReturn {\n /** Live state data */\n liveState: LiveStateResponse | null;\n\n /** True while the first fetch is in progress */\n isLoading: boolean;\n\n /**\n * True when the last fetch attempt resulted in an error.\n * Use this to implement fallbacks or custom error logging.\n * Note: the lib already logs a warning to the console on every error.\n */\n isError: boolean;\n\n /**\n * The raw error from the last failed fetch, or null.\n * Pair with `isError` for more specific error handling.\n */\n error: Error | null;\n\n /** Manually trigger a re-fetch */\n refresh: () => Promise<void>;\n}\n\n/**\n * useLiveState hook\n *\n * Fetches and manages live state data using SWR with automatic caching,\n * deduplication, and revalidation.\n *\n * Must be used within LiveStateProvider. See examples/ directory for usage.\n */\nexport function useLiveState(): UseLiveStateReturn {\n const { fetcher, log } = useLiveStateContext();\n\n const { data, error, isLoading, mutate } = useSWR<LiveStateResponse | null>(\n 'live-state',\n fetcher,\n {\n revalidateOnFocus: false,\n revalidateOnReconnect: true,\n dedupingInterval: 60000, // 1 minute\n shouldRetryOnError: false, // Fail silently\n onError: err => {\n log('error', 'Failed to fetch live state', { error: err });\n },\n },\n );\n\n const refresh = async () => {\n try {\n await mutate();\n } catch (err) {\n log('error', 'Failed to refresh live state', { error: err });\n }\n };\n\n return {\n liveState: data ?? null,\n isLoading,\n isError: error != null,\n error: error ?? null,\n refresh,\n };\n}\n","import { useCallback } from 'react';\nimport { useLiveStateContext } from '../providers/LiveStateProvider';\nimport { trackEvent } from '../utils/analytics';\nimport type { TrackingProperties } from '../types';\n\n/**\n * useTrackEvent\n *\n * Returns a `track` function that fires both the internal analytics\n * (Amplitude + Clarity) and the optional `onEvent` callback provided\n * to LiveStateProvider — useful for simulators and debugging tools.\n */\nexport function useTrackEvent() {\n const { onEvent, disabled, log } = useLiveStateContext();\n\n const track = useCallback(\n (eventName: string, properties?: TrackingProperties) => {\n // onEvent callback fires even when disabled — it is an observability\n // hook, not an analytics SDK, so the caller opts in deliberately.\n onEvent?.(eventName, properties);\n\n // Skip SDK calls when analytics are disabled (e.g. dev/test environments)\n if (!disabled) {\n trackEvent(eventName, properties, log);\n }\n },\n [onEvent, disabled, log],\n );\n\n return track;\n}\n","export const LIVE_STATE_STORAGE_KEY = '@tiendanube/live-state:closable';\n\n/** @internal */\nconst STORAGE_KEY = LIVE_STATE_STORAGE_KEY;\n\nexport interface ClosableState {\n count: number;\n closedAt: number;\n}\n\nexport type ClosableStates = Record<string, Record<string, ClosableState>>;\n\n/**\n * Reads closable states from localStorage.\n * Normalises legacy format (plain number) to the current shape ({ count, closedAt }).\n * Returns an empty object on any error (fail-silent).\n */\nexport function getClosableStates(): ClosableStates {\n try {\n const raw = localStorage.getItem(STORAGE_KEY);\n if (!raw) return {};\n\n const parsed = JSON.parse(raw) as unknown;\n if (typeof parsed !== 'object' || parsed === null) return {};\n\n const normalized: ClosableStates = {};\n\n for (const [ctx, ctxState] of Object.entries(\n parsed as Record<string, unknown>,\n )) {\n if (typeof ctxState !== 'object' || ctxState === null) continue;\n\n normalized[ctx] = {};\n\n for (const [id, entry] of Object.entries(\n ctxState as Record<string, unknown>,\n )) {\n if (typeof entry === 'number') {\n // legacy format: plain close count\n normalized[ctx][id] = { count: entry, closedAt: 0 };\n } else if (typeof entry === 'object' && entry !== null) {\n const candidate = entry as Partial<ClosableState>;\n normalized[ctx][id] = {\n count: typeof candidate.count === 'number' ? candidate.count : 0,\n closedAt:\n typeof candidate.closedAt === 'number' ? candidate.closedAt : 0,\n };\n }\n }\n }\n\n return normalized;\n } catch {\n return {};\n }\n}\n\n/**\n * Persists closable states to localStorage.\n * Fails silently if localStorage is unavailable or full.\n */\nexport function setClosableStates(states: ClosableStates): void {\n try {\n localStorage.setItem(STORAGE_KEY, JSON.stringify(states));\n } catch {\n // localStorage unavailable or quota exceeded — application continues normally\n }\n}\n","import { useState, useEffect, useCallback } from 'react';\nimport {\n getClosableStates,\n setClosableStates,\n} from '../utils/closable-storage';\n\nexport interface UseClosableOptions {\n /** Notification context (e.g. 'awareness', 'charge') */\n context: string;\n /** Unique notification ID (campaignId). If absent, always visible — no close tracking. */\n id: string | undefined;\n /** Maximum number of times the user can close before it is hidden permanently */\n maxCloseTimes?: number;\n /** Milliseconds after which the close counter resets (optional) */\n expiresIn?: number;\n}\n\nexport interface UseClosableReturn {\n /** Whether the notification should be shown. null = initial check in progress. */\n isVisible: boolean | null;\n /** Call this to record a close action */\n close: () => void;\n}\n\n/**\n * Manages the close state of a notification using localStorage.\n *\n * After the user closes a notification `maxCloseTimes` times, it is hidden\n * permanently. Optionally, the counter resets after `expiresIn` milliseconds.\n *\n * Fails silently — errors in localStorage never break the application.\n *\n * @example\n * ```tsx\n * const { isVisible, close } = useClosable({\n * context: 'awareness',\n * id: 'awareness-2026-q1',\n * maxCloseTimes: 3,\n * expiresIn: 86_400_000, // 1 day\n * });\n *\n * if (!isVisible) return null;\n * return <Notification onClose={close} />;\n * ```\n */\nexport function useClosable({\n context,\n id,\n maxCloseTimes = 3,\n expiresIn,\n}: UseClosableOptions): UseClosableReturn {\n const [isVisible, setIsVisible] = useState<boolean | null>(null);\n\n useEffect(() => {\n // No ID means no close tracking — always show\n if (!id) {\n setIsVisible(true);\n return;\n }\n\n const states = getClosableStates();\n const entry = states[context]?.[id];\n\n if (!entry) {\n setIsVisible(true);\n return;\n }\n\n const { count, closedAt } = entry;\n\n // Check expiry — reset counter and show if expired\n if (expiresIn != null && closedAt > 0) {\n const elapsed = Date.now() - closedAt;\n if (elapsed > expiresIn) {\n const updated = { ...states };\n if (updated[context]) {\n delete updated[context][id];\n setClosableStates(updated);\n }\n setIsVisible(true);\n return;\n }\n }\n\n setIsVisible(count < maxCloseTimes);\n }, [context, id, maxCloseTimes, expiresIn]);\n\n const close = useCallback(() => {\n // Always hide visually\n setIsVisible(false);\n\n // Only persist to localStorage if there is an id to track\n if (!id) return;\n\n const states = getClosableStates();\n const currentCount = states[context]?.[id]?.count ?? 0;\n\n const updated: typeof states = {\n ...states,\n [context]: {\n ...(states[context] ?? {}),\n [id]: {\n count: currentCount + 1,\n closedAt: Date.now(),\n },\n },\n };\n\n setClosableStates(updated);\n }, [context, id]);\n\n return { isVisible, close };\n}\n","import type { CtaConfig } from '../types';\nimport { createLogger, type LoggerFn } from './logger';\n\n/**\n * Handle CTA click based on type\n *\n * @param cta - CTA configuration from LiveStateResponse\n * @param log - Optional logger (defaults to console.warn)\n */\nexport function handleCtaClick(cta: CtaConfig, log?: LoggerFn): void {\n const logger = createLogger(log);\n try {\n switch (cta.type) {\n case 'redirect':\n window.location.href = cta.url;\n break;\n\n case 'whatsapp': {\n const whatsappUrl = cta.whatsappMessage\n ? `${cta.url}?text=${encodeURIComponent(cta.whatsappMessage)}`\n : cta.url;\n window.open(whatsappUrl, '_blank');\n break;\n }\n\n case 'external':\n window.open(cta.url, '_blank');\n break;\n\n default:\n logger('warn', `Unknown CTA type: ${(cta as CtaConfig).type}`, { cta });\n }\n } catch (error) {\n logger('error', 'Failed to handle CTA click', { cta, error });\n }\n}\n","import { useCallback, useEffect } from 'react';\nimport { Alert, Box, Button, Text } from '@nimbus-ds/components';\nimport type { LiveStateResponse, TrackingConfig } from '../types';\nimport { buildTrackingProperties, buildEventName } from '../utils/analytics';\nimport { handleCtaClick } from '../utils/cta';\nimport { useTrackEvent } from '../hooks/useTrackEvent';\nimport { useLiveStateContext } from '../providers/LiveStateProvider';\n\nexport interface LiveStateAlertProps {\n data: LiveStateResponse;\n trackingConfig: TrackingConfig;\n /** When provided, shows a close button and calls this on dismiss */\n onClose?: () => void;\n /**\n * Override CTA click behaviour.\n * When provided, replaces default navigation so the caller controls what\n * happens on click (e.g. prevent navigation in a simulator, use a router).\n */\n onCtaClick?: (cta: import('../types').CtaConfig) => void;\n}\n\n/**\n * LiveStateAlert\n *\n * Renders an Alert component (Nimbus) for alert/warning types.\n * Automatically tracks view, click, and close events.\n * Shows a close button when onClose is provided.\n *\n * See examples/ directory for usage examples.\n */\nexport function LiveStateAlert({\n data,\n trackingConfig,\n onClose,\n onCtaClick,\n}: LiveStateAlertProps) {\n const track = useTrackEvent();\n const { log } = useLiveStateContext();\n const appearance = data.type === 'alert' ? 'danger' : 'warning';\n\n const buildEventProperties = useCallback(\n () =>\n buildTrackingProperties({\n context: data.context,\n campaignId: data.campaignId,\n group: data.group,\n ...data.metadata,\n ...trackingConfig.properties,\n }),\n [data, trackingConfig.properties],\n );\n\n const handleClick = useCallback(() => {\n const eventName = buildEventName(\n trackingConfig.prefix,\n trackingConfig.page,\n data.context,\n 'click',\n );\n track(eventName, buildEventProperties());\n if (onCtaClick) {\n onCtaClick(data.cta);\n } else {\n handleCtaClick(data.cta, log);\n }\n }, [data, trackingConfig, buildEventProperties, track, onCtaClick, log]);\n\n const handleClose = useCallback(() => {\n const eventName = buildEventName(\n trackingConfig.prefix,\n trackingConfig.page,\n data.context,\n 'close',\n );\n track(eventName, buildEventProperties());\n onClose?.();\n }, [onClose, data, trackingConfig, buildEventProperties, track]);\n\n useEffect(() => {\n const eventName = buildEventName(\n trackingConfig.prefix,\n trackingConfig.page,\n data.context,\n 'view',\n );\n track(eventName, buildEventProperties());\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return (\n <Alert\n appearance={appearance}\n title={data.title}\n onRemove={onClose ? handleClose : undefined}>\n <Box\n display=\"flex\"\n alignItems=\"flex-start\"\n gap=\"3\"\n flexWrap=\"wrap\"\n flexDirection=\"column\">\n <Box flex=\"1 1 auto\" minWidth=\"220px\">\n <Text>{data.message}</Text>\n </Box>\n\n <Box display=\"flex\" justifyContent=\"flex-end\" marginTop=\"2\">\n <Button appearance=\"primary\" onClick={handleClick}>\n {data.cta.label}\n </Button>\n </Box>\n </Box>\n </Alert>\n );\n}\n","import { useCallback, useEffect, useState } from 'react';\nimport { Card, Box, Text, Link, Icon } from '@nimbus-ds/components';\nimport { MoneyIcon, CloseIcon } from '@nimbus-ds/icons';\nimport type { LiveStateResponse, TrackingConfig } from '../types';\nimport { buildTrackingProperties, buildEventName } from '../utils/analytics';\nimport { handleCtaClick } from '../utils/cta';\nimport { useTrackEvent } from '../hooks/useTrackEvent';\nimport { useLiveStateContext } from '../providers/LiveStateProvider';\n\nexport interface LiveStateInfoProps {\n data: LiveStateResponse;\n trackingConfig: TrackingConfig;\n defaultVariant?: 'blue' | 'white';\n /** When provided, shows a close button and calls this on dismiss */\n onClose?: () => void;\n /**\n * Override mobile layout detection.\n * When provided, bypasses `window.innerWidth < 750` so simulators and\n * testing tools can force a specific layout without resizing the window.\n */\n isMobile?: boolean;\n /**\n * Override CTA click behaviour.\n * When provided, replaces default navigation so the caller controls what\n * happens on click (e.g. prevent navigation in a simulator, use a router).\n */\n onCtaClick?: (cta: import('../types').CtaConfig) => void;\n}\n\nconst ICON_COLOR_MAP = {\n blue: { background: '#0050C3', icon: '#FFFFFF' },\n white: { background: '#FFFFFF', icon: '#0059D5' },\n} as const;\n\n/**\n * LiveStateInfo\n *\n * Renders a Card component (Nimbus) for info type.\n * Uses MoneyIcon with colored circle background, Text components,\n * and a Link-style CTA — matching the LendingBanner design reference.\n * Automatically tracks view, click, and close events.\n * Shows a close button when onClose is provided.\n *\n * See examples/ directory for usage examples.\n */\nexport function LiveStateInfo({\n data,\n trackingConfig,\n defaultVariant = 'blue',\n onClose,\n isMobile: isMobileProp,\n onCtaClick,\n}: LiveStateInfoProps) {\n const track = useTrackEvent();\n const { log } = useLiveStateContext();\n const variant = data.variant || defaultVariant;\n const iconConfig = ICON_COLOR_MAP[variant];\n\n const [isMobileWindow, setIsMobileWindow] = useState(\n () => window.innerWidth < 750,\n );\n\n useEffect(() => {\n const handler = () => setIsMobileWindow(window.innerWidth < 750);\n window.addEventListener('resize', handler);\n return () => window.removeEventListener('resize', handler);\n }, []);\n\n // Prop overrides window detection — allows simulators/tests to force layout\n const isMobile = isMobileProp ?? isMobileWindow;\n\n const buildEventProperties = useCallback(\n () =>\n buildTrackingProperties({\n context: data.context,\n campaignId: data.campaignId,\n group: data.group,\n ...data.metadata,\n ...trackingConfig.properties,\n }),\n [data, trackingConfig.properties],\n );\n\n const handleClick = useCallback(() => {\n const eventName = buildEventName(\n trackingConfig.prefix,\n trackingConfig.page,\n data.context,\n 'click',\n );\n track(eventName, buildEventProperties());\n if (onCtaClick) {\n onCtaClick(data.cta);\n } else {\n handleCtaClick(data.cta, log);\n }\n }, [data, trackingConfig, buildEventProperties, track, onCtaClick, log]);\n\n const handleClose = useCallback(() => {\n const eventName = buildEventName(\n trackingConfig.prefix,\n trackingConfig.page,\n data.context,\n 'close',\n );\n track(eventName, buildEventProperties());\n onClose?.();\n }, [onClose, data, trackingConfig, buildEventProperties, track]);\n\n useEffect(() => {\n const eventName = buildEventName(\n trackingConfig.prefix,\n trackingConfig.page,\n data.context,\n 'view',\n );\n track(eventName, buildEventProperties());\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const ctaLink = (\n <Link\n as=\"a\"\n textDecoration=\"none\"\n onClick={handleClick}\n data-testid=\"live-state-cta-link\">\n <Text color=\"primary-interactive\" fontSize=\"base\">\n {data.cta.label}\n </Text>\n </Link>\n );\n\n return (\n <Card>\n <Box display=\"flex\" flexDirection=\"row\" justifyContent=\"space-between\">\n {/* Left: icon + text (+ cta on mobile) */}\n <Box\n display=\"flex\"\n gap=\"4\"\n alignItems={isMobile ? 'flex-start' : 'center'}\n paddingRight=\"1-5\">\n <Box minWidth=\"32px\">\n <div\n style={{\n width: 32,\n height: 32,\n borderRadius: '35%',\n borderColor: variant === 'white' ? '#E7E7E7' : 'transparent',\n borderWidth: 1,\n borderStyle: 'solid',\n background: iconConfig.background,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n }}>\n <MoneyIcon style={{ color: iconConfig.icon }} />\n </div>\n </Box>\n\n <Box display=\"grid\" gap=\"2\">\n <Box>\n <Text fontSize=\"base\" color=\"neutral-textHigh\">\n {data.title}\n </Text>\n <Text fontSize=\"base\" color=\"neutral-textLow\">\n {data.message}\n </Text>\n </Box>\n\n {/* CTA shown below text on mobile */}\n {isMobile && ctaLink}\n </Box>\n </Box>\n\n {/* Right: cta (desktop only) + close button */}\n {(!isMobile || onClose) && (\n <Box\n display=\"flex\"\n flexDirection={isMobile ? 'column' : 'row'}\n alignItems={isMobile ? 'flex-end' : 'center'}\n gap=\"2\">\n {/* CTA shown on the right side on desktop */}\n {!isMobile && ctaLink}\n\n {onClose && (\n <button\n type=\"button\"\n data-testid=\"live-state-close-button\"\n aria-label=\"Fechar notificação\"\n onClick={handleClose}\n style={{\n background: 'none',\n border: 'none',\n padding: 0,\n cursor: 'pointer',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n }}>\n <Icon\n source={<CloseIcon width=\"18px\" height=\"18px\" />}\n color=\"neutral-interactive\"\n />\n </button>\n )}\n </Box>\n )}\n </Box>\n </Card>\n );\n}\n","import type { LiveStateRendererProps } from '../types';\nimport { useClosable } from '../hooks/useClosable';\nimport { useLiveStateContext } from '../providers/LiveStateProvider';\nimport { LiveStateAlert } from './LiveStateAlert';\nimport { LiveStateInfo } from './LiveStateInfo';\n\n/**\n * LiveStateRenderer\n *\n * Main component that validates payload, filters by context, and renders\n * the appropriate visual component (Alert or Info) based on type.\n * Handles closable notifications via useClosable when closable is configured.\n *\n * See examples/ directory for usage examples.\n */\nexport function LiveStateRenderer({\n data,\n loading,\n trackingConfig,\n allowedContexts,\n defaultVariant,\n isMobile,\n onCtaClick,\n}: LiveStateRendererProps) {\n const { log } = useLiveStateContext();\n const { isVisible, close } = useClosable({\n context: data?.context ?? '',\n id: data?.campaignId,\n maxCloseTimes: data?.metadata?.maxCloseTimes,\n expiresIn: data?.metadata?.expiresIn,\n });\n\n // Don't render while loading or no data\n if (loading || !data) {\n return null;\n }\n\n // Validate required fields\n if (\n !data.context ||\n !data.type ||\n !data.title ||\n !data.message ||\n !data.cta?.label ||\n !data.cta?.url ||\n !data.cta?.type\n ) {\n log('warn', 'Invalid payload, missing required fields', { data });\n return null;\n }\n\n // Filter by allowed contexts\n if (allowedContexts && !allowedContexts.includes(data.context)) {\n return null;\n }\n\n // Awaiting localStorage check\n if (isVisible === null) {\n return null;\n }\n\n // Hidden — user reached maxCloseTimes\n if (!isVisible) {\n return null;\n }\n\n // Pass onClose whenever closable is true.\n // campaignId is optional — useClosable handles id=undefined gracefully\n // (always visible, no localStorage tracking).\n const onClose = data.closable ? close : undefined;\n\n // Render appropriate component based on type\n if (data.type === 'alert' || data.type === 'warning') {\n return (\n <LiveStateAlert\n data={data}\n onClose={onClose}\n trackingConfig={trackingConfig}\n onCtaClick={onCtaClick}\n />\n );\n }\n\n if (data.type === 'info') {\n return (\n <LiveStateInfo\n data={data}\n onClose={onClose}\n trackingConfig={trackingConfig}\n defaultVariant={defaultVariant}\n isMobile={isMobile}\n onCtaClick={onCtaClick}\n />\n );\n }\n\n log('warn', `Unknown type: ${data.type}`, { data });\n return null;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/hooks/useLiveState.ts","../src/hooks/useTrackEvent.ts","../src/utils/closable-storage.ts","../src/hooks/useClosable.ts","../src/utils/cta.ts","../src/utils/sanitize.ts","../src/components/RichText.tsx","../src/components/LiveStateAlert.tsx","../src/components/LiveStateInfo.tsx","../src/components/LiveStateRenderer.tsx"],"sourcesContent":["import useSWR from 'swr';\nimport { useLiveStateContext } from '../providers/LiveStateProvider';\nimport type { LiveStateResponse } from '../types';\n\ninterface UseLiveStateReturn {\n /** Live state data */\n liveState: LiveStateResponse | null;\n\n /** True while the first fetch is in progress */\n isLoading: boolean;\n\n /**\n * True when the last fetch attempt resulted in an error.\n * Use this to implement fallbacks or custom error logging.\n * Note: the lib already logs a warning to the console on every error.\n */\n isError: boolean;\n\n /**\n * The raw error from the last failed fetch, or null.\n * Pair with `isError` for more specific error handling.\n */\n error: Error | null;\n\n /** Manually trigger a re-fetch */\n refresh: () => Promise<void>;\n}\n\n/**\n * useLiveState hook\n *\n * Fetches and manages live state data using SWR with automatic caching,\n * deduplication, and revalidation.\n *\n * Must be used within LiveStateProvider. See examples/ directory for usage.\n */\nexport function useLiveState(): UseLiveStateReturn {\n const { fetcher, log } = useLiveStateContext();\n\n const { data, error, isLoading, mutate } = useSWR<LiveStateResponse | null>(\n 'live-state',\n fetcher,\n {\n revalidateOnFocus: false,\n revalidateOnReconnect: true,\n dedupingInterval: 60000, // 1 minute\n shouldRetryOnError: false, // Fail silently\n onError: err => {\n log('error', 'Failed to fetch live state', { error: err });\n },\n },\n );\n\n const refresh = async () => {\n try {\n await mutate();\n } catch (err) {\n log('error', 'Failed to refresh live state', { error: err });\n }\n };\n\n return {\n liveState: data ?? null,\n isLoading,\n isError: error != null,\n error: error ?? null,\n refresh,\n };\n}\n","import { useCallback } from 'react';\nimport { useLiveStateContext } from '../providers/LiveStateProvider';\nimport { trackEvent } from '../utils/analytics';\nimport type { TrackingProperties } from '../types';\n\n/**\n * useTrackEvent\n *\n * Returns a `track` function that fires both the internal analytics\n * (Amplitude + Clarity) and the optional `onEvent` callback provided\n * to LiveStateProvider — useful for simulators and debugging tools.\n */\nexport function useTrackEvent() {\n const { onEvent, disabled, log } = useLiveStateContext();\n\n const track = useCallback(\n (eventName: string, properties?: TrackingProperties) => {\n // onEvent callback fires even when disabled — it is an observability\n // hook, not an analytics SDK, so the caller opts in deliberately.\n onEvent?.(eventName, properties);\n\n // Skip SDK calls when analytics are disabled (e.g. dev/test environments)\n if (!disabled) {\n trackEvent(eventName, properties, log);\n }\n },\n [onEvent, disabled, log],\n );\n\n return track;\n}\n","export const LIVE_STATE_STORAGE_KEY = '@tiendanube/live-state:closable';\n\n/** @internal */\nconst STORAGE_KEY = LIVE_STATE_STORAGE_KEY;\n\nexport interface ClosableState {\n count: number;\n closedAt: number;\n}\n\nexport type ClosableStates = Record<string, Record<string, ClosableState>>;\n\n/**\n * Reads closable states from localStorage.\n * Normalises legacy format (plain number) to the current shape ({ count, closedAt }).\n * Returns an empty object on any error (fail-silent).\n */\nexport function getClosableStates(): ClosableStates {\n try {\n const raw = localStorage.getItem(STORAGE_KEY);\n if (!raw) return {};\n\n const parsed = JSON.parse(raw) as unknown;\n if (typeof parsed !== 'object' || parsed === null) return {};\n\n const normalized: ClosableStates = {};\n\n for (const [ctx, ctxState] of Object.entries(\n parsed as Record<string, unknown>,\n )) {\n if (typeof ctxState !== 'object' || ctxState === null) continue;\n\n normalized[ctx] = {};\n\n for (const [id, entry] of Object.entries(\n ctxState as Record<string, unknown>,\n )) {\n if (typeof entry === 'number') {\n // legacy format: plain close count\n normalized[ctx][id] = { count: entry, closedAt: 0 };\n } else if (typeof entry === 'object' && entry !== null) {\n const candidate = entry as Partial<ClosableState>;\n normalized[ctx][id] = {\n count: typeof candidate.count === 'number' ? candidate.count : 0,\n closedAt:\n typeof candidate.closedAt === 'number' ? candidate.closedAt : 0,\n };\n }\n }\n }\n\n return normalized;\n } catch {\n return {};\n }\n}\n\n/**\n * Persists closable states to localStorage.\n * Fails silently if localStorage is unavailable or full.\n */\nexport function setClosableStates(states: ClosableStates): void {\n try {\n localStorage.setItem(STORAGE_KEY, JSON.stringify(states));\n } catch {\n // localStorage unavailable or quota exceeded — application continues normally\n }\n}\n","import { useState, useEffect, useCallback } from 'react';\nimport {\n getClosableStates,\n setClosableStates,\n} from '../utils/closable-storage';\n\nexport interface UseClosableOptions {\n /** Notification context (e.g. 'awareness', 'charge') */\n context: string;\n /** Unique notification ID (campaignId). If absent, always visible — no close tracking. */\n id: string | undefined;\n /** Maximum number of times the user can close before it is hidden permanently */\n maxCloseTimes?: number;\n /** Milliseconds after which the close counter resets (optional) */\n expiresIn?: number;\n}\n\nexport interface UseClosableReturn {\n /** Whether the notification should be shown. null = initial check in progress. */\n isVisible: boolean | null;\n /** Call this to record a close action */\n close: () => void;\n}\n\n/**\n * Manages the close state of a notification using localStorage.\n *\n * After the user closes a notification `maxCloseTimes` times, it is hidden\n * permanently. Optionally, the counter resets after `expiresIn` milliseconds.\n *\n * Fails silently — errors in localStorage never break the application.\n *\n * @example\n * ```tsx\n * const { isVisible, close } = useClosable({\n * context: 'awareness',\n * id: 'awareness-2026-q1',\n * maxCloseTimes: 3,\n * expiresIn: 86_400_000, // 1 day\n * });\n *\n * if (!isVisible) return null;\n * return <Notification onClose={close} />;\n * ```\n */\nexport function useClosable({\n context,\n id,\n maxCloseTimes = 3,\n expiresIn,\n}: UseClosableOptions): UseClosableReturn {\n const [isVisible, setIsVisible] = useState<boolean | null>(null);\n\n useEffect(() => {\n // No ID means no close tracking — always show\n if (!id) {\n setIsVisible(true);\n return;\n }\n\n const states = getClosableStates();\n const entry = states[context]?.[id];\n\n if (!entry) {\n setIsVisible(true);\n return;\n }\n\n const { count, closedAt } = entry;\n\n // Check expiry — reset counter and show if expired\n if (expiresIn != null && closedAt > 0) {\n const elapsed = Date.now() - closedAt;\n if (elapsed > expiresIn) {\n const updated = { ...states };\n if (updated[context]) {\n delete updated[context][id];\n setClosableStates(updated);\n }\n setIsVisible(true);\n return;\n }\n }\n\n setIsVisible(count < maxCloseTimes);\n }, [context, id, maxCloseTimes, expiresIn]);\n\n const close = useCallback(() => {\n // Always hide visually\n setIsVisible(false);\n\n // Only persist to localStorage if there is an id to track\n if (!id) return;\n\n const states = getClosableStates();\n const currentCount = states[context]?.[id]?.count ?? 0;\n\n const updated: typeof states = {\n ...states,\n [context]: {\n ...(states[context] ?? {}),\n [id]: {\n count: currentCount + 1,\n closedAt: Date.now(),\n },\n },\n };\n\n setClosableStates(updated);\n }, [context, id]);\n\n return { isVisible, close };\n}\n","import type { CtaConfig } from '../types';\nimport { createLogger, type LoggerFn } from './logger';\n\n/**\n * Handle CTA click based on type\n *\n * @param cta - CTA configuration from LiveStateResponse\n * @param log - Optional logger (defaults to console.warn)\n */\nexport function handleCtaClick(cta: CtaConfig, log?: LoggerFn): void {\n const logger = createLogger(log);\n try {\n switch (cta.type) {\n case 'redirect':\n window.location.href = cta.url;\n break;\n\n case 'whatsapp': {\n const whatsappUrl = cta.whatsappMessage\n ? `${cta.url}?text=${encodeURIComponent(cta.whatsappMessage)}`\n : cta.url;\n window.open(whatsappUrl, '_blank');\n break;\n }\n\n case 'external':\n window.open(cta.url, '_blank');\n break;\n\n default:\n logger('warn', `Unknown CTA type: ${(cta as CtaConfig).type}`, { cta });\n }\n } catch (error) {\n logger('error', 'Failed to handle CTA click', { cta, error });\n }\n}\n","/**\n * Minimal HTML sanitiser for notification content.\n *\n * Allows only a strict allowlist of inline formatting tags that are expected\n * in notification messages (strong, em, b, i, br, span). All other tags and\n * attributes are stripped, preventing XSS even if the backend is compromised.\n *\n * This intentionally avoids a runtime dependency on DOMPurify or similar\n * libraries — the allowlist is narrow enough to be maintained safely here.\n */\n\n/** Tags that are safe to render inside notification text */\nconst ALLOWED_TAGS = new Set(['strong', 'em', 'b', 'i', 'br', 'span']);\n\n/**\n * Tags whose entire content (open tag + inner text + close tag) must be\n * removed, not just the surrounding tags. This prevents script injection\n * even if the backend is compromised.\n */\nconst BLOCK_TAGS_RE =\n /<(script|style|iframe|object|embed|form|input|button|textarea|select|link|meta|head|body|html|svg|math)[\\s\\S]*?<\\/\\1\\s*>|<(script|style|iframe|object|embed|form|input|button|textarea|select|link|meta|head|body|html|svg|math)[^>]*\\/?>/gi;\n\n/**\n * Strips all HTML tags except those in ALLOWED_TAGS.\n * Attributes are removed from all allowed tags to prevent event-handler\n * injection (e.g. `<span onclick=\"...\">`).\n * Dangerous tags (script, style, iframe, …) have their entire content removed.\n *\n * Returns the sanitised HTML string, safe to pass to `dangerouslySetInnerHTML`.\n */\nexport function sanitizeHtml(input: string): string {\n // Step 1: remove dangerous tags and their inner content entirely\n const withoutDangerous = input.replace(BLOCK_TAGS_RE, '');\n\n // Step 2: remove all attributes from every remaining tag\n const withoutAttributes = withoutDangerous.replace(\n /<([a-zA-Z][a-zA-Z0-9]*)\\s[^>]*>/g,\n '<$1>',\n );\n\n // Step 3: strip any remaining tag that is not in the allowlist\n return withoutAttributes.replace(\n /<\\/?([a-zA-Z][a-zA-Z0-9]*)[^>]*>/g,\n (match, tag: string) => (ALLOWED_TAGS.has(tag.toLowerCase()) ? match : ''),\n );\n}\n\n/**\n * Returns true when the string contains at least one HTML tag,\n * indicating it should be rendered as rich text.\n */\nexport function containsHtml(input: string): boolean {\n return /<[a-zA-Z][^>]*>/.test(input);\n}\n","import { Text } from '@nimbus-ds/components';\nimport type { ComponentProps } from 'react';\nimport { sanitizeHtml, containsHtml } from '../utils/sanitize';\n\ntype TextProps = ComponentProps<typeof Text>;\n\ninterface RichTextProps extends Omit<TextProps, 'children'> {\n content: string;\n}\n\n/**\n * RichText\n *\n * Renders a text string that may contain inline HTML formatting (e.g. <strong>,\n * <em>, <br>). When HTML is detected, the content is sanitised through a strict\n * allowlist before being injected via dangerouslySetInnerHTML. Plain strings\n * are rendered via the Nimbus Text component without any DOM injection.\n *\n * Allowed tags: strong, em, b, i, br, span. All attributes are stripped.\n */\nexport function RichText({ content, ...textProps }: RichTextProps) {\n if (containsHtml(content)) {\n const safe = sanitizeHtml(content);\n return (\n <Text {...textProps}>\n {/* eslint-disable-next-line react/no-danger */}\n <span dangerouslySetInnerHTML={{ __html: safe }} />\n </Text>\n );\n }\n\n return <Text {...textProps}>{content}</Text>;\n}\n","import { useCallback, useEffect } from 'react';\nimport { Alert, Box, Button } from '@nimbus-ds/components';\nimport type { LiveStateResponse, TrackingConfig } from '../types';\nimport { buildTrackingProperties, buildEventName } from '../utils/analytics';\nimport { handleCtaClick } from '../utils/cta';\nimport { useTrackEvent } from '../hooks/useTrackEvent';\nimport { useLiveStateContext } from '../providers/LiveStateProvider';\nimport { RichText } from './RichText';\n\nexport interface LiveStateAlertProps {\n data: LiveStateResponse;\n trackingConfig: TrackingConfig;\n /** When provided, shows a close button and calls this on dismiss */\n onClose?: () => void;\n /**\n * Override CTA click behaviour.\n * When provided, replaces default navigation so the caller controls what\n * happens on click (e.g. prevent navigation in a simulator, use a router).\n */\n onCtaClick?: (cta: import('../types').CtaConfig) => void;\n}\n\n/**\n * LiveStateAlert\n *\n * Renders an Alert component (Nimbus) for alert/warning types.\n * Automatically tracks view, click, and close events.\n * Shows a close button when onClose is provided.\n *\n * See examples/ directory for usage examples.\n */\nexport function LiveStateAlert({\n data,\n trackingConfig,\n onClose,\n onCtaClick,\n}: LiveStateAlertProps) {\n const track = useTrackEvent();\n const { log } = useLiveStateContext();\n const appearance = data.type === 'alert' ? 'danger' : 'warning';\n\n const buildEventProperties = useCallback(\n () =>\n buildTrackingProperties({\n context: data.context,\n campaignId: data.campaignId,\n group: data.group,\n ...data.metadata,\n ...trackingConfig.properties,\n }),\n [data, trackingConfig.properties],\n );\n\n const handleClick = useCallback(() => {\n const eventName = buildEventName(\n trackingConfig.prefix,\n trackingConfig.page,\n data.context,\n 'click',\n );\n track(eventName, buildEventProperties());\n if (onCtaClick) {\n onCtaClick(data.cta);\n } else {\n handleCtaClick(data.cta, log);\n }\n }, [data, trackingConfig, buildEventProperties, track, onCtaClick, log]);\n\n const handleClose = useCallback(() => {\n const eventName = buildEventName(\n trackingConfig.prefix,\n trackingConfig.page,\n data.context,\n 'close',\n );\n track(eventName, buildEventProperties());\n onClose?.();\n }, [onClose, data, trackingConfig, buildEventProperties, track]);\n\n useEffect(() => {\n const eventName = buildEventName(\n trackingConfig.prefix,\n trackingConfig.page,\n data.context,\n 'view',\n );\n track(eventName, buildEventProperties());\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return (\n <Alert\n appearance={appearance}\n title={data.title}\n onRemove={onClose ? handleClose : undefined}>\n <Box\n display=\"flex\"\n alignItems=\"flex-start\"\n gap=\"3\"\n flexWrap=\"wrap\"\n flexDirection=\"column\">\n <Box flex=\"1 1 auto\" minWidth=\"220px\">\n <RichText content={data.message} />\n </Box>\n\n <Box display=\"flex\" justifyContent=\"flex-end\" marginTop=\"2\">\n <Button appearance=\"primary\" onClick={handleClick}>\n {data.cta.label}\n </Button>\n </Box>\n </Box>\n </Alert>\n );\n}\n","import { useCallback, useEffect, useState } from 'react';\nimport { Card, Box, Text, Link, Icon } from '@nimbus-ds/components';\nimport { MoneyIcon, CloseIcon } from '@nimbus-ds/icons';\nimport type { LiveStateResponse, TrackingConfig } from '../types';\nimport { buildTrackingProperties, buildEventName } from '../utils/analytics';\nimport { handleCtaClick } from '../utils/cta';\nimport { useTrackEvent } from '../hooks/useTrackEvent';\nimport { useLiveStateContext } from '../providers/LiveStateProvider';\nimport { RichText } from './RichText';\n\nexport interface LiveStateInfoProps {\n data: LiveStateResponse;\n trackingConfig: TrackingConfig;\n defaultVariant?: 'blue' | 'white';\n /** When provided, shows a close button and calls this on dismiss */\n onClose?: () => void;\n /**\n * Override mobile layout detection.\n * When provided, bypasses `window.innerWidth < 750` so simulators and\n * testing tools can force a specific layout without resizing the window.\n */\n isMobile?: boolean;\n /**\n * Override CTA click behaviour.\n * When provided, replaces default navigation so the caller controls what\n * happens on click (e.g. prevent navigation in a simulator, use a router).\n */\n onCtaClick?: (cta: import('../types').CtaConfig) => void;\n}\n\nconst ICON_COLOR_MAP = {\n blue: { background: '#0050C3', icon: '#FFFFFF' },\n white: { background: '#FFFFFF', icon: '#0059D5' },\n} as const;\n\n/**\n * LiveStateInfo\n *\n * Renders a Card component (Nimbus) for info type.\n * Uses MoneyIcon with colored circle background, Text components,\n * and a Link-style CTA — matching the LendingBanner design reference.\n * Automatically tracks view, click, and close events.\n * Shows a close button when onClose is provided.\n *\n * See examples/ directory for usage examples.\n */\nexport function LiveStateInfo({\n data,\n trackingConfig,\n defaultVariant = 'blue',\n onClose,\n isMobile: isMobileProp,\n onCtaClick,\n}: LiveStateInfoProps) {\n const track = useTrackEvent();\n const { log } = useLiveStateContext();\n const variant = data.variant || defaultVariant;\n const iconConfig = ICON_COLOR_MAP[variant];\n\n const [isMobileWindow, setIsMobileWindow] = useState(\n () => window.innerWidth < 750,\n );\n\n useEffect(() => {\n const handler = () => setIsMobileWindow(window.innerWidth < 750);\n window.addEventListener('resize', handler);\n return () => window.removeEventListener('resize', handler);\n }, []);\n\n // Prop overrides window detection — allows simulators/tests to force layout\n const isMobile = isMobileProp ?? isMobileWindow;\n\n const buildEventProperties = useCallback(\n () =>\n buildTrackingProperties({\n context: data.context,\n campaignId: data.campaignId,\n group: data.group,\n ...data.metadata,\n ...trackingConfig.properties,\n }),\n [data, trackingConfig.properties],\n );\n\n const handleClick = useCallback(() => {\n const eventName = buildEventName(\n trackingConfig.prefix,\n trackingConfig.page,\n data.context,\n 'click',\n );\n track(eventName, buildEventProperties());\n if (onCtaClick) {\n onCtaClick(data.cta);\n } else {\n handleCtaClick(data.cta, log);\n }\n }, [data, trackingConfig, buildEventProperties, track, onCtaClick, log]);\n\n const handleClose = useCallback(() => {\n const eventName = buildEventName(\n trackingConfig.prefix,\n trackingConfig.page,\n data.context,\n 'close',\n );\n track(eventName, buildEventProperties());\n onClose?.();\n }, [onClose, data, trackingConfig, buildEventProperties, track]);\n\n useEffect(() => {\n const eventName = buildEventName(\n trackingConfig.prefix,\n trackingConfig.page,\n data.context,\n 'view',\n );\n track(eventName, buildEventProperties());\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const ctaLink = (\n <Link\n as=\"a\"\n textDecoration=\"none\"\n onClick={handleClick}\n data-testid=\"live-state-cta-link\">\n <Text color=\"primary-interactive\" fontSize=\"base\">\n {data.cta.label}\n </Text>\n </Link>\n );\n\n return (\n <Card>\n <Box display=\"flex\" flexDirection=\"row\" justifyContent=\"space-between\">\n {/* Left: icon + text (+ cta on mobile) */}\n <Box\n display=\"flex\"\n gap=\"4\"\n alignItems={isMobile ? 'flex-start' : 'center'}\n paddingRight=\"1-5\">\n <Box minWidth=\"32px\">\n <div\n style={{\n width: 32,\n height: 32,\n borderRadius: '35%',\n borderColor: variant === 'white' ? '#E7E7E7' : 'transparent',\n borderWidth: 1,\n borderStyle: 'solid',\n background: iconConfig.background,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n }}>\n <MoneyIcon style={{ color: iconConfig.icon }} />\n </div>\n </Box>\n\n <Box display=\"grid\" gap=\"2\">\n <Box>\n <RichText content={data.title} fontSize=\"base\" color=\"neutral-textHigh\" />\n <RichText content={data.message} fontSize=\"base\" color=\"neutral-textLow\" />\n </Box>\n\n {/* CTA shown below text on mobile */}\n {isMobile && ctaLink}\n </Box>\n </Box>\n\n {/* Right: cta (desktop only) + close button */}\n {(!isMobile || onClose) && (\n <Box\n display=\"flex\"\n flexDirection={isMobile ? 'column' : 'row'}\n alignItems={isMobile ? 'flex-end' : 'center'}\n gap=\"2\">\n {/* CTA shown on the right side on desktop */}\n {!isMobile && ctaLink}\n\n {onClose && (\n <button\n type=\"button\"\n data-testid=\"live-state-close-button\"\n aria-label=\"Fechar notificação\"\n onClick={handleClose}\n style={{\n background: 'none',\n border: 'none',\n padding: 0,\n cursor: 'pointer',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n }}>\n <Icon\n source={<CloseIcon width=\"18px\" height=\"18px\" />}\n color=\"neutral-interactive\"\n />\n </button>\n )}\n </Box>\n )}\n </Box>\n </Card>\n );\n}\n","import type { LiveStateRendererProps } from '../types';\nimport { useClosable } from '../hooks/useClosable';\nimport { useLiveStateContext } from '../providers/LiveStateProvider';\nimport { LiveStateAlert } from './LiveStateAlert';\nimport { LiveStateInfo } from './LiveStateInfo';\n\n/**\n * LiveStateRenderer\n *\n * Main component that validates payload, filters by context, and renders\n * the appropriate visual component (Alert or Info) based on type.\n * Handles closable notifications via useClosable when closable is configured.\n *\n * See examples/ directory for usage examples.\n */\nexport function LiveStateRenderer({\n data,\n loading,\n trackingConfig,\n allowedContexts,\n defaultVariant,\n isMobile,\n onCtaClick,\n}: LiveStateRendererProps) {\n const { log } = useLiveStateContext();\n const { isVisible, close } = useClosable({\n context: data?.context ?? '',\n id: data?.campaignId,\n maxCloseTimes: data?.metadata?.maxCloseTimes,\n expiresIn: data?.metadata?.expiresIn,\n });\n\n // Don't render while loading or no data\n if (loading || !data) {\n return null;\n }\n\n // Validate required fields\n if (\n !data.context ||\n !data.type ||\n !data.title ||\n !data.message ||\n !data.cta?.label ||\n !data.cta?.url ||\n !data.cta?.type\n ) {\n log('warn', 'Invalid payload, missing required fields', { data });\n return null;\n }\n\n // Filter by allowed contexts\n if (allowedContexts && !allowedContexts.includes(data.context)) {\n return null;\n }\n\n // Awaiting localStorage check\n if (isVisible === null) {\n return null;\n }\n\n // Hidden — user reached maxCloseTimes\n if (!isVisible) {\n return null;\n }\n\n // Pass onClose whenever closable is true.\n // campaignId is optional — useClosable handles id=undefined gracefully\n // (always visible, no localStorage tracking).\n const onClose = data.closable ? close : undefined;\n\n // Render appropriate component based on type\n if (data.type === 'alert' || data.type === 'warning') {\n return (\n <LiveStateAlert\n data={data}\n onClose={onClose}\n trackingConfig={trackingConfig}\n onCtaClick={onCtaClick}\n />\n );\n }\n\n if (data.type === 'info') {\n return (\n <LiveStateInfo\n data={data}\n onClose={onClose}\n trackingConfig={trackingConfig}\n defaultVariant={defaultVariant}\n isMobile={isMobile}\n onCtaClick={onCtaClick}\n />\n );\n }\n\n log('warn', `Unknown type: ${data.type}`, { data });\n return null;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LiveStateAlert.d.ts","sourceRoot":"","sources":["../../../src/components/LiveStateAlert.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"LiveStateAlert.d.ts","sourceRoot":"","sources":["../../../src/components/LiveStateAlert.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAOlE,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,iBAAiB,CAAC;IACxB,cAAc,EAAE,cAAc,CAAC;IAC/B,oEAAoE;IACpE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,UAAU,EAAE,SAAS,KAAK,IAAI,CAAC;CAC1D;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,EAC7B,IAAI,EACJ,cAAc,EACd,OAAO,EACP,UAAU,GACX,EAAE,mBAAmB,2CA6ErB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LiveStateInfo.d.ts","sourceRoot":"","sources":["../../../src/components/LiveStateInfo.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"LiveStateInfo.d.ts","sourceRoot":"","sources":["../../../src/components/LiveStateInfo.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAOlE,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,iBAAiB,CAAC;IACxB,cAAc,EAAE,cAAc,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAClC,oEAAoE;IACpE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,UAAU,EAAE,SAAS,KAAK,IAAI,CAAC;CAC1D;AAOD;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,cAAc,EACd,cAAuB,EACvB,OAAO,EACP,QAAQ,EAAE,YAAY,EACtB,UAAU,GACX,EAAE,kBAAkB,2CA0JpB"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Text } from '@nimbus-ds/components';
|
|
2
|
+
import { ComponentProps } from 'react';
|
|
3
|
+
type TextProps = ComponentProps<typeof Text>;
|
|
4
|
+
interface RichTextProps extends Omit<TextProps, 'children'> {
|
|
5
|
+
content: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* RichText
|
|
9
|
+
*
|
|
10
|
+
* Renders a text string that may contain inline HTML formatting (e.g. <strong>,
|
|
11
|
+
* <em>, <br>). When HTML is detected, the content is sanitised through a strict
|
|
12
|
+
* allowlist before being injected via dangerouslySetInnerHTML. Plain strings
|
|
13
|
+
* are rendered via the Nimbus Text component without any DOM injection.
|
|
14
|
+
*
|
|
15
|
+
* Allowed tags: strong, em, b, i, br, span. All attributes are stripped.
|
|
16
|
+
*/
|
|
17
|
+
export declare function RichText({ content, ...textProps }: RichTextProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=RichText.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RichText.d.ts","sourceRoot":"","sources":["../../../src/components/RichText.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAG5C,KAAK,SAAS,GAAG,cAAc,CAAC,OAAO,IAAI,CAAC,CAAC;AAE7C,UAAU,aAAc,SAAQ,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC;IACzD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,EAAE,OAAO,EAAE,GAAG,SAAS,EAAE,EAAE,aAAa,2CAYhE"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal HTML sanitiser for notification content.
|
|
3
|
+
*
|
|
4
|
+
* Allows only a strict allowlist of inline formatting tags that are expected
|
|
5
|
+
* in notification messages (strong, em, b, i, br, span). All other tags and
|
|
6
|
+
* attributes are stripped, preventing XSS even if the backend is compromised.
|
|
7
|
+
*
|
|
8
|
+
* This intentionally avoids a runtime dependency on DOMPurify or similar
|
|
9
|
+
* libraries — the allowlist is narrow enough to be maintained safely here.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Strips all HTML tags except those in ALLOWED_TAGS.
|
|
13
|
+
* Attributes are removed from all allowed tags to prevent event-handler
|
|
14
|
+
* injection (e.g. `<span onclick="...">`).
|
|
15
|
+
* Dangerous tags (script, style, iframe, …) have their entire content removed.
|
|
16
|
+
*
|
|
17
|
+
* Returns the sanitised HTML string, safe to pass to `dangerouslySetInnerHTML`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function sanitizeHtml(input: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Returns true when the string contains at least one HTML tag,
|
|
22
|
+
* indicating it should be rendered as rich text.
|
|
23
|
+
*/
|
|
24
|
+
export declare function containsHtml(input: string): boolean;
|
|
25
|
+
//# sourceMappingURL=sanitize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../../src/utils/sanitize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAaH;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAelD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEnD"}
|
package/package.json
CHANGED