diff --git a/packages/react/src/sds/UpsellingKit/UpsellingAlert/index.stories.tsx b/packages/react/src/sds/UpsellingKit/UpsellingAlert/index.stories.tsx index 86056932a0..e4b1024328 100644 --- a/packages/react/src/sds/UpsellingKit/UpsellingAlert/index.stories.tsx +++ b/packages/react/src/sds/UpsellingKit/UpsellingAlert/index.stories.tsx @@ -6,6 +6,21 @@ import { withSnapshot } from "@/lib/storybook-utils/parameters" import { UpsellingAlert } from "." +/** + * Example dismiss handler: persist the dismissal so the alert stays hidden for + * a number of days and reappears afterwards. + * + * Takes a per-alert `storageKey` so multiple upselling alerts on the same page + * don't overwrite each other's dismissal state. Namespace it by the module / + * feature the alert promotes. + */ +const dismissForDays = (storageKey: string) => () => { + const HIDE_FOR_DAYS = 7 + const hideUntil = Date.now() + HIDE_FOR_DAYS * 24 * 60 * 60 * 1000 + window.localStorage.setItem(storageKey, String(hideUntil)) + alert(`Dismissed — hidden for ${HIDE_FOR_DAYS} days.`) +} + const meta: Meta = { title: "UpsellingAlert", component: UpsellingAlert, @@ -20,7 +35,22 @@ const meta: Meta = { mapping: icons, options: [undefined, ...Object.keys(icons)], }, + onDismiss: { + description: + "When enabled, shows a dismiss (close) button to the right of the action button.", + control: "boolean", + }, }, + render: ({ onDismiss, ...args }) => ( + + ), } export default meta @@ -70,6 +100,41 @@ export const WithIcon: Story = { }, } +/** + * When `onDismiss` is provided, a close button is rendered to the right of the + * upselling action button. The consumer decides what happens on dismiss — here + * we persist the dismissal under a per-alert key so the alert stays hidden for + * a number of days and reappears afterwards. Use a distinct key per alert so + * multiple upselling alerts don't overwrite each other. Toggle the `onDismiss` + * control to show/hide the button. + */ +export const Dismissible: Story = { + args: { + ...WithIcon.args, + onDismiss: dismissForDays("upselling-alert-hidden-until:performance"), + }, +} + +/** + * When the action only opens a modal or navigates instead of creating an + * upselling request, set `showConfirmation: false` so the success dialog + * ("request sent") isn't shown for a request that was never made. + */ +export const WithoutConfirmation: Story = { + args: { + ...Default.args, + title: "Open the product details", + description: + "Use showConfirmation: false when the action only opens a modal or navigates, so no success dialog is shown for a request that was never sent.", + action: { + ...Default.args!.action!, + label: "Learn more", + showConfirmation: false, + onRequest: () => alert("Open product modal"), + }, + }, +} + export const Snapshot: Story = { parameters: withSnapshot({}), args: Default.args, diff --git a/packages/react/src/sds/UpsellingKit/UpsellingAlert/index.tsx b/packages/react/src/sds/UpsellingKit/UpsellingAlert/index.tsx index 97363b6bfc..832d6a436f 100644 --- a/packages/react/src/sds/UpsellingKit/UpsellingAlert/index.tsx +++ b/packages/react/src/sds/UpsellingKit/UpsellingAlert/index.tsx @@ -1,5 +1,8 @@ +import { F0Button } from "@/components/F0Button" import { F0Icon, type IconType } from "@/components/F0Icon" +import { Cross } from "@/icons/app" import { withDataTestId } from "@/lib/data-testid" +import { useI18n } from "@/lib/providers/i18n/i18n-provider" import { cn } from "@/lib/utils" import { UpsellingButton, type UpsellingButtonProps } from "../UpsellingButton" @@ -12,6 +15,13 @@ type AlertAction = { loadingState: UpsellingButtonProps["loadingState"] nextSteps: UpsellingButtonProps["nextSteps"] closeLabel: UpsellingButtonProps["closeLabel"] + /** + * Whether to show the confirmation dialog after the request resolves. + * Defaults to `true`. Set to `false` when `onRequest` only opens a modal or + * navigates instead of creating an upselling request, so the success dialog + * ("request sent") is not shown for an action that sent nothing. + */ + showConfirmation?: UpsellingButtonProps["showConfirmation"] } export interface UpsellingAlertProps { @@ -31,6 +41,37 @@ export interface UpsellingAlertProps { * The upselling action button configuration. */ action: AlertAction + /** + * Called when the user dismisses the alert. When provided, a close button is + * shown just to the right of the upselling action button. + * + * The consumer is responsible for deciding what happens on dismiss — for + * example, hiding the alert for a number of days and showing it again later + * by persisting the dismissal (e.g. in a cookie or local storage) and + * unmounting the component while it should stay hidden. + */ + onDismiss?: () => void +} + +/** + * Leaf component wrapping the close button so `useI18n()` is only read when + * an `onDismiss` handler is actually provided. Keeps UpsellingAlert renderable + * without an `I18nProvider` for consumers (and tests) that don't use the + * dismiss affordance. + */ +const DismissButton = ({ onDismiss }: { onDismiss: () => void }) => { + const { actions } = useI18n() + return ( + + ) } function _UpsellingAlert({ @@ -38,52 +79,69 @@ function _UpsellingAlert({ title, description, action, + onDismiss, }: UpsellingAlertProps) { return (
-
+
- {icon && ( -
- -
- )} -
-

{title}

- {description && ( -

- {description} -

+
+ {icon && ( +
+ +
)} +
+

{title}

+ {description && ( +

+ {description} +

+ )} +
+
+
+
-
- -
+ {onDismiss && ( +
+ +
+ )}