Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/mini-apps-ui-kit-react/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @worldcoin/mini-apps-ui-kit-react

## 1.4.0

### Minor Changes

- Add support for long form toasts.

## 1.3.0

### Minor Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/mini-apps-ui-kit-react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@worldcoin/mini-apps-ui-kit-react",
"version": "1.3.0",
"version": "1.4.0",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const AlertDialogContent = React.forwardRef<
));
AlertDialogContent.displayName = "AlertDialogContent";

const AlertDialogHeader = ({ icon, children, ...props }: AlertDialogHeaderProps) => {
const AlertDialogHeader = ({ icon, children, className, ...props }: AlertDialogHeaderProps) => {
const { dismissible } = useAlertDialog();

const closeButton = dismissible && (
Expand All @@ -89,7 +89,7 @@ const AlertDialogHeader = ({ icon, children, ...props }: AlertDialogHeaderProps)

if (icon) {
return (
<div className="mb-4 flex flex-col gap-6">
<div className={cn("mb-4 flex flex-col gap-6", className)}>
<div className={cn(baseClasses)} {...props}>
<div className="flex flex-col">{icon}</div>
{closeButton}
Expand All @@ -100,7 +100,7 @@ const AlertDialogHeader = ({ icon, children, ...props }: AlertDialogHeaderProps)
}

return (
<div className={cn(baseClasses, "gap-4 mb-4 items-center")} {...props}>
<div className={cn(baseClasses, "gap-4 mb-4 items-center", className)} {...props}>
<div className="flex flex-col gap-6">{children}</div>
{closeButton}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,7 @@ export interface AlertDialogContentProps
/**
* Props for the AlertDialogHeader component
*/
export interface AlertDialogHeaderProps
extends Omit<React.HTMLAttributes<HTMLDivElement>, "className"> {
export interface AlertDialogHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
/** Optional icon to display in the header */
icon?: React.ReactNode;
children?: React.ReactNode;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,63 @@
"use client";

import { cn } from "@/lib/utils";

import {
AlertDialog,
AlertDialogClose,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "../AlertDialog";
import { Button } from "../Button";
import { CircularState } from "../CircularState";
import { Toast, ToastProvider, ToastViewport } from "./Toast";
import { useToast } from "./use-toast";

function Toaster({ duration }: { duration?: number }) {
const { toasts } = useToast();
const { toasts, drawer } = useToast();

return (
<ToastProvider duration={duration} swipeDirection="up">
{toasts.map(({ id, ...props }) => (
<Toast key={id} {...props} />
))}
<ToastViewport />
</ToastProvider>
<>
<ToastProvider duration={duration} swipeDirection="up">
{toasts.map(({ id, ...props }) => (
<Toast key={id} {...props} />
))}
<ToastViewport />
</ToastProvider>
{drawer && (
<AlertDialog open={drawer.open} onOpenChange={drawer.onOpenChange}>
<AlertDialogContent>
<AlertDialogHeader
icon={
<CircularState
size="lg"
value={drawer.variant === "success" ? "success" : "critical"}
/>
}
>
<AlertDialogTitle>{drawer.title}</AlertDialogTitle>
{drawer.description && (
<AlertDialogDescription className={cn(!drawer.action && "mb-0")}>
{drawer.description}
</AlertDialogDescription>
)}
</AlertDialogHeader>
{drawer.action && (
<AlertDialogFooter>
<AlertDialogClose>
<Button fullWidth onClick={drawer.action.onClick}>
{drawer.action.label}
</Button>
</AlertDialogClose>
</AlertDialogFooter>
)}
</AlertDialogContent>
</AlertDialog>
)}
</>
);
}

Expand Down
205 changes: 176 additions & 29 deletions packages/mini-apps-ui-kit-react/src/components/Toast/use-toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,46 @@ type ToasterToast = ToastProps & {
id: string;
};

type DrawerToast = {
id: string;
title: string;
description: string;
action?: {
label: string;
onClick: () => void;
};
variant: "success" | "error";
open: boolean;
onOpenChange: (open: boolean) => void;
};

// Base Toast type without id
type BaseToast = Omit<ToasterToast, "id">;

// Discriminated union payloads
type RegularToastPayload = BaseToast;

type DrawerToastPayload = {
title: string;
description: string;
action?: {
label: string;
onClick: () => void;
};
variant: "success" | "error";
};

type ToastPayload = RegularToastPayload | DrawerToastPayload;

const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
ADD_DRAWER: "ADD_DRAWER",
UPDATE_DRAWER: "UPDATE_DRAWER",
DISMISS_DRAWER: "DISMISS_DRAWER",
REMOVE_DRAWER: "REMOVE_DRAWER",
} as const;

let count = 0;
Expand Down Expand Up @@ -44,10 +79,25 @@ type Action =
| {
type: ActionType["REMOVE_TOAST"];
toastId?: ToasterToast["id"];
}
| {
type: ActionType["ADD_DRAWER"];
drawer: DrawerToast;
}
| {
type: ActionType["UPDATE_DRAWER"];
drawer: Partial<DrawerToast>;
}
| {
type: ActionType["DISMISS_DRAWER"];
}
| {
type: ActionType["REMOVE_DRAWER"];
};

interface State {
toasts: ToasterToast[];
drawer: DrawerToast | null;
}

const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
Expand Down Expand Up @@ -120,12 +170,36 @@ export const reducer = (state: State, action: Action): State => {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
};

case "ADD_DRAWER":
return {
...state,
drawer: action.drawer,
};

case "UPDATE_DRAWER":
return {
...state,
drawer: state.drawer ? { ...state.drawer, ...action.drawer } : null,
};

case "DISMISS_DRAWER":
return {
...state,
drawer: state.drawer ? { ...state.drawer, open: false } : null,
};

case "REMOVE_DRAWER":
return {
...state,
drawer: null,
};
}
};

const listeners: Array<(state: State) => void> = [];

let memoryState: State = { toasts: [] };
let memoryState: State = { toasts: [], drawer: null };

function dispatch(action: Action) {
memoryState = reducer(memoryState, action);
Expand All @@ -134,35 +208,70 @@ function dispatch(action: Action) {
});
}

type Toast = Omit<ToasterToast, "id">;
const toast = (payload: ToastPayload) => {
// Check if it's a drawer payload by looking for description
if ("description" in payload && payload.description) {
// Show as drawer
const id = "drawer";
const drawerPayload = payload as DrawerToastPayload;

const toast = ({ ...props }: Toast) => {
const id = genId();
const update = (props: Partial<DrawerToast>) =>
dispatch({
type: "UPDATE_DRAWER",
drawer: { ...props, id },
});
const dismiss = () => dispatch({ type: "DISMISS_DRAWER" });

const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
type: "ADD_DRAWER",
drawer: {
id,
title: drawerPayload.title,
description: drawerPayload.description,
action: drawerPayload.action,
variant: drawerPayload.variant,
open: true,
onOpenChange: (open: boolean) => {
if (!open) dismiss();
},
},
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });

dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss();
return {
id: id,
dismiss,
update,
};
} else {
// Show as regular toast
const id = genId();
const regularPayload = payload as RegularToastPayload;

const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });

dispatch({
type: "ADD_TOAST",
toast: {
...regularPayload,
id,
open: true,
onOpenChange: (open: boolean) => {
if (!open) dismiss();
},
},
},
});
});

return {
id: id,
dismiss,
update,
};
return {
id: id,
dismiss,
update,
};
}
};

function useToast() {
Expand All @@ -179,18 +288,56 @@ function useToast() {
}, [state]);

return {
...state,
toasts: state.toasts,
drawer: state.drawer,
toast: {
success: (props: Omit<Toast, "variant">) => {
toast({ ...props, variant: "success" });
success: (payload: {
title: string;
description?: string;
action?: { label: string; onClick: () => void };
}) => {
if ("description" in payload && payload.description) {
toast({
title: payload.title,
description: payload.description,
action: payload.action,
variant: "success",
});
} else {
toast({
title: payload.title,
variant: "success",
});
}
haptics.notification("success");
},
error: (props: Omit<Toast, "variant">) => {
toast({ ...props, variant: "error" });
error: (payload: {
title: string;
description?: string;
action?: { label: string; onClick: () => void };
}) => {
if ("description" in payload && payload.description) {
toast({
title: payload.title,
description: payload.description,
action: payload.action,
variant: "error",
});
} else {
toast({
title: payload.title,
variant: "error",
});
}
haptics.notification("error");
},
},
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
dismiss: (toastId?: string) => {
if (toastId === "drawer") {
return dispatch({ type: "DISMISS_DRAWER" });
}
dispatch({ type: "DISMISS_TOAST", toastId });
},
};
}

Expand Down
Loading