import { Illustration, Assets, type IllustrationNames } from '@wise/art'; import { clsx } from 'clsx'; import { ReactNode, useEffect, useState, MouseEvent } from 'react'; import Body from '../body'; import { Typography } from '../common'; import Link from '../link'; import type { AlertAction } from '../alert'; import IconButton from '../iconButton'; import { Cross } from '@transferwise/icons'; import { useIntl } from 'react-intl'; import closeBtnMessages from '../common/closeButton/CloseButton.messages'; // WARNING: Changing this will cause nudges to reappear wherever persist nudge is used and privacy team will need to be updated too export const STORAGE_NAME = 'dismissedNudges'; const getLocalStorage = (): string[] => { try { const storageItem = localStorage.getItem(STORAGE_NAME); if (storageItem) { const storage: unknown = JSON.parse(storageItem); if (Array.isArray(storage)) { return storage.map((item) => String(item)); } } } catch (error) {} return []; }; type MediaNameType = | `${Assets.GLOBE}` | `${Assets.LOCK}` | `${Assets.WALLET}` | `${Assets.GEAR}` | `${Assets.INVITE_LETTER}` | `${Assets.PERSONAL_CARD}` | `${Assets.BUSINESS_CARD}` | `${Assets.HEART}` | `${Assets.MULTI_CURRENCY}` | `${Assets.SHOPPING_BAG}` | `${Assets.FLOWER}` | `${Assets.GIFT_BOX}` | `${Assets.BACKPACK}`; type BaseProps = { /** @deprecated Use `mediaName` property instead. */ media?: ReactNode; /** Media name */ mediaName?: MediaNameType; title: ReactNode; link?: ReactNode; href?: string; onClick?: (event?: MouseEvent) => void; /** Fired when the user clicks on close button */ onDismiss?: () => void; /** An optional call to action to sit under the main body of the nudge. If your label is short, use aria-label to provide more context */ action?: AlertAction; className?: string; }; export interface OptionalId extends BaseProps { id?: string; persistDismissal?: false; isPreviouslyDismissed?: undefined; } export interface RequiredPersistProps extends BaseProps { /** This ID should be completely unique to the page and feature as it uses a shared array which could conflict with other nudges in Wise */ id: string; /** Use persist dismissal to keep the nudge dismissed using the browser's localStorage */ persistDismissal: true; /** * Fired on mount for determining if nudge has been dismissed before * * @param {boolean} value - set to true if dismissed previously */ isPreviouslyDismissed?: (value: boolean) => void; } export type Props = OptionalId | RequiredPersistProps; const Nudge = ({ mediaName, title, link, href, onClick, onDismiss, persistDismissal, isPreviouslyDismissed, id, className, action, }: Props) => { const intl = useIntl(); const [isDismissed, setIsDismissed] = useState(false); const [isMounted, setIsMounted] = useState(false); const handleOnDismiss = () => { const dismissedNudgesStorage = getLocalStorage(); if (persistDismissal && id) { try { localStorage.setItem(STORAGE_NAME, JSON.stringify([...dismissedNudgesStorage, id])); } catch (error) {} setIsDismissed(true); } if (onDismiss) { onDismiss(); } }; useEffect(() => { if (persistDismissal && id) { const dismissedNudgesStorage = getLocalStorage(); let isDismissed = false; if (dismissedNudgesStorage?.find((item) => item === id)) { setIsDismissed(true); isDismissed = true; } if (isPreviouslyDismissed) { isPreviouslyDismissed(isDismissed); } } setIsMounted(true); // eslint-disable-next-line react-hooks/exhaustive-deps }, [id, persistDismissal]); if (persistDismissal && (isDismissed || !isMounted)) { return null; } return (
{!!mediaName && (
)}
{title} {/* Merge these two Link instances into one */} {link && ( {link} )} {action && ( {action.text} )}
{onDismiss || persistDismissal ? ( ) : null}
); }; export default Nudge;