import type { ReactNode } from 'react'; import { store } from './store'; import type { PromiseOptions, PromiseResult, ToastContent, ToastOptions, ToastRecord, ToastType, } from './types'; /** `toast(...)` options, minus the fields the call signature already sets. */ type Options = Omit; const create = (type: ToastType) => (message?: ReactNode, options?: Options): string => store.add({ ...options, content: message, type }); const resolveMessage = ( value: ReactNode | ((input: T) => ReactNode) | undefined, input: T, fallback: ReactNode ): ReactNode => { if (typeof value === 'function') return (value as (input: T) => ReactNode)(input); return value !== undefined ? value : fallback; }; /** * Show a toast. Works from anywhere — including before `` mounts, * in which case the toast is queued and rendered as soon as it does. */ function toastFn(message?: ReactNode, options?: Options): string { return store.add({ ...options, content: message, type: options?.type }); } const api = { default: create('default'), /** Sonner alias for the neutral toast. */ message: create('default'), success: create('success'), error: create('error'), info: create('info'), warning: create('warning'), /** Sonner alias for `warning`. */ warn: create('warning'), /** * Fully custom toast. Accepts a node, or a render function receiving the * toast id — the Sonner form, so `toast.custom(id => ...)` ports directly. */ custom: (content?: ToastContent, options?: Options): string => typeof content === 'function' ? store.add({ ...options, jsx: content, type: 'custom' }) : store.add({ ...options, content, type: 'custom' }), loading: (message?: ReactNode, options?: Options): string => store.add({ duration: Infinity, ...options, content: message, type: 'loading' }), /** Track a promise through loading → success / error in a single toast. */ promise( input: Promise | (() => Promise), options: PromiseOptions = {} ): PromiseResult { const id = store.add({ id: options.id, content: options.loading !== undefined ? options.loading : 'Loading…', description: typeof options.description === 'function' ? undefined : options.description, type: 'loading', duration: Infinity, cancel: options.cancel, position: options.position, }); const promise = typeof input === 'function' ? input() : input; const settled = promise.then( (data) => { store.update(id, { content: resolveMessage(options.success, data, 'Success'), description: resolveMessage(options.description, data, undefined), type: 'success', duration: options.duration, cancel: undefined, status: 'success', }); options.onSuccess?.(data); return { ok: true as const, data }; }, (error: unknown) => { store.update(id, { content: resolveMessage(options.error, error, 'Something went wrong'), description: resolveMessage(options.description, error, undefined), type: 'error', duration: options.errorDuration !== undefined ? options.errorDuration : options.duration, cancel: undefined, status: 'error', }); options.onError?.(error); return { ok: false as const, error }; } ); // Settling is handled here so an un-awaited `toast.promise` never becomes an // unhandled rejection; `unwrap()` re-throws for callers that do want it. const finished = settled.then((result) => { options.finally?.(); return result; }); return { id, unwrap: () => finished.then((result) => { if (result.ok) return result.data; throw result.error; }), }; }, /** Dismiss one toast, or every toast when called with no argument. */ dismiss: (id?: string | number): void => store.dismiss(id), /** Remove a toast without firing its dismissal callbacks. */ remove: (id: string | number): void => store.remove(id), /** Patch an existing toast. */ update: (id: string | number, patch: ToastOptions & { type?: ToastType }): void => store.update(id, patch), /** Legacy alias for `dismiss()`. */ clearAll: (): void => store.dismiss(), /** Currently queued toasts, visible or not. */ getToasts: (): ToastRecord[] => store.getToasts(), /** Toasts that have already closed, oldest first. */ getHistory: (): ToastRecord[] => store.getHistory(), /** Is this toast still queued or on screen? */ isActive: (id: string | number): boolean => store.isActive(id), }; export type ToastFunction = typeof toastFn & typeof api; export const toast: ToastFunction = Object.assign(toastFn, api);