diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx
index 95b8ca8f..e7e42812 100644
--- a/apps/web/src/main.tsx
+++ b/apps/web/src/main.tsx
@@ -6,7 +6,7 @@ import '@tamagui/font-inter/css/700.css'
import { StrictMode } from 'react'
-import { Provider } from '@status-im/components'
+import { Provider, ToastContainer } from '@status-im/components'
import { createRoot } from 'react-dom/client'
import App from './app'
@@ -17,6 +17,7 @@ createRoot(root).render(
+
)
diff --git a/apps/web/styles/app.css b/apps/web/styles/app.css
index f1e960bf..cfecd650 100644
--- a/apps/web/styles/app.css
+++ b/apps/web/styles/app.css
@@ -17,6 +17,7 @@ body,
}
#app {
+ isolation: isolate;
height: 100%;
display: grid;
grid-template-columns: 352px 1fr auto;
diff --git a/packages/components/.storybook/preview.tsx b/packages/components/.storybook/preview.tsx
index 0d1d407e..2941a73d 100644
--- a/packages/components/.storybook/preview.tsx
+++ b/packages/components/.storybook/preview.tsx
@@ -1,4 +1,4 @@
-import { Provider } from '../src'
+import { Provider, ToastContainer } from '../src'
import { Parameters, Decorator } from '@storybook/react'
import './reset.css'
@@ -17,6 +17,7 @@ const withThemeProvider: Decorator = (Story, _context) => {
return (
+
)
}
diff --git a/packages/components/package.json b/packages/components/package.json
index 310c3279..fdc5743c 100644
--- a/packages/components/package.json
+++ b/packages/components/package.json
@@ -33,6 +33,7 @@
"@radix-ui/react-dropdown-menu": "^2.0.4",
"@radix-ui/react-popover": "^1.0.5",
"@radix-ui/react-tabs": "^1.0.3",
+ "@radix-ui/react-toast": "^1.1.3",
"@radix-ui/react-tooltip": "^1.0.5",
"@status-im/icons": "*",
"@tamagui/animations-css": "1.7.7",
diff --git a/packages/components/src/index.tsx b/packages/components/src/index.tsx
index 0470d07c..f158813f 100644
--- a/packages/components/src/index.tsx
+++ b/packages/components/src/index.tsx
@@ -12,6 +12,7 @@ export * from './provider'
export * from './sidebar'
export * from './sidebar-members'
export * from './text'
+export * from './toast'
export * from './topbar'
export * from './user-list'
diff --git a/packages/components/src/toast/index.tsx b/packages/components/src/toast/index.tsx
new file mode 100644
index 00000000..a8612795
--- /dev/null
+++ b/packages/components/src/toast/index.tsx
@@ -0,0 +1,3 @@
+export type { ToastProps } from './toast'
+export { Toast } from './toast'
+export { ToastContainer, useToast } from './toast-container'
diff --git a/packages/components/src/toast/toast-container.tsx b/packages/components/src/toast/toast-container.tsx
new file mode 100644
index 00000000..ed15f6e1
--- /dev/null
+++ b/packages/components/src/toast/toast-container.tsx
@@ -0,0 +1,89 @@
+import { useMemo } from 'react'
+
+import { Provider, Root, Viewport } from '@radix-ui/react-toast'
+import { styled } from 'tamagui'
+import { create } from 'zustand'
+
+import { Toast } from './toast'
+
+import type { ToastProps } from './toast'
+
+type ToastState = {
+ toast: ToastProps | null
+ dismiss: () => void
+ positive: (
+ message: string,
+ actionProps?: Pick
+ ) => void
+ negative: (
+ message: string,
+ actionProps?: Pick
+ ) => void
+ custom: (
+ message: string,
+ icon: React.ReactElement,
+ actionProps?: Pick
+ ) => void
+}
+
+const useStore = create()(set => ({
+ toast: null,
+ positive: (message, actionProps) =>
+ set({ toast: { ...actionProps, message, type: 'positive' } }),
+ negative: (message, actionProps) =>
+ set({ toast: { ...actionProps, message, type: 'negative' } }),
+ custom: (message, icon, actionProps) =>
+ set({ toast: { ...actionProps, message, icon } }),
+ dismiss: () => set({ toast: null }),
+}))
+
+const ToastContainer = () => {
+ const store = useStore()
+
+ if (store.toast === null) {
+ return null
+ }
+
+ const handleOpenChange = (open: boolean) => {
+ if (!open) {
+ store.dismiss()
+ }
+ }
+
+ return (
+
+
+
+
+
+
+ )
+}
+
+const useToast = () => {
+ const store = useStore()
+
+ return useMemo(
+ () => ({
+ positive: store.positive,
+ negative: store.negative,
+ custom: store.custom,
+ }),
+ [store]
+ )
+}
+
+export { ToastContainer, useToast }
+
+const ToastRoot = styled(Root, {
+ name: 'ToastRoot',
+ acceptsClassName: true,
+
+ bottom: 12,
+ right: 12,
+ zIndex: 1000,
+})
diff --git a/packages/components/src/toast/toast.stories.tsx b/packages/components/src/toast/toast.stories.tsx
new file mode 100644
index 00000000..346ded2c
--- /dev/null
+++ b/packages/components/src/toast/toast.stories.tsx
@@ -0,0 +1,73 @@
+import { PlaceholderIcon } from '@status-im/icons/20'
+import { Stack } from '@tamagui/core'
+
+import { Button } from '../button'
+import { Toast, useToast } from './'
+
+import type { Meta, StoryObj } from '@storybook/react'
+
+const meta: Meta = {
+ component: Toast,
+ args: {},
+ argTypes: {},
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'https://www.figma.com/file/IBmFKgGL1B4GzqD8LQTw6n/Design-System-for-Desktop%2FWeb?node-id=3928-77614&t=hp2XwjFgrFl3hhDm-4',
+ },
+ },
+}
+
+type Story = StoryObj
+
+const ToastWithHook = () => {
+ const toast = useToast()
+
+ return (
+
+ )
+}
+
+export const Default: Story = {
+ args: {},
+ render: () => (
+
+
+
+ ),
+}
+
+export const AllVariants: Story = {
+ args: {},
+ render: () => (
+
+
+
+ } message="Something happened" />
+
+
+
+
+ ),
+}
+
+export default meta
diff --git a/packages/components/src/toast/toast.tsx b/packages/components/src/toast/toast.tsx
new file mode 100644
index 00000000..7361e23b
--- /dev/null
+++ b/packages/components/src/toast/toast.tsx
@@ -0,0 +1,91 @@
+import { cloneElement, forwardRef } from 'react'
+
+import { Action, Description } from '@radix-ui/react-toast'
+import { CorrectIcon, IncorrectIcon } from '@status-im/icons/20'
+import { Stack, styled } from '@tamagui/core'
+
+import { Button } from '../button'
+import { Text } from '../text'
+
+type Props = {
+ message: string
+ action?: string
+ onAction?: () => void
+} & (
+ | {
+ type: 'positive' | 'negative'
+ }
+ | {
+ type?: never
+ icon: React.ReactElement
+ }
+)
+
+const Toast = (props: Props) => {
+ const { message, action, onAction } = props
+
+ const renderIcon = () => {
+ if (!props.type) {
+ return cloneElement(props.icon, { color: '$white-70' })
+ }
+
+ if (props.type === 'positive') {
+ return
+ }
+
+ return
+ }
+
+ return (
+
+
+ {renderIcon()}
+
+
+ {message}
+
+
+
+ {action && (
+
+
+
+
+
+ )}
+
+ )
+}
+
+const _Toast = forwardRef(Toast)
+
+export { _Toast as Toast }
+export type { Props as ToastProps }
+
+const Base = styled(Stack, {
+ name: 'Toast',
+
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 8,
+ gap: 12,
+ width: 351,
+ minHeight: 40,
+ backgroundColor: '$neutral-80-opa-70',
+ borderRadius: 12,
+ justifyContent: 'space-between',
+
+ variants: {
+ action: {
+ true: {
+ paddingVertical: 8,
+ },
+ false: {
+ paddingVertical: 10,
+ },
+ },
+ },
+})
diff --git a/packages/icons/20/correct-icon.tsx b/packages/icons/20/correct-icon.tsx
index d6108798..f374c527 100644
--- a/packages/icons/20/correct-icon.tsx
+++ b/packages/icons/20/correct-icon.tsx
@@ -18,15 +18,8 @@ const SvgCorrectIcon = (props: IconProps) => {
xmlns="http://www.w3.org/2000/svg"
{...props}
>
-
-
+
+
)
}
diff --git a/packages/icons/20/incorrect-icon.tsx b/packages/icons/20/incorrect-icon.tsx
new file mode 100644
index 00000000..bf9077b6
--- /dev/null
+++ b/packages/icons/20/incorrect-icon.tsx
@@ -0,0 +1,31 @@
+import { useTheme } from '@tamagui/core'
+import { Circle, Path, Svg } from 'react-native-svg'
+
+import type { IconProps } from '../types'
+
+const SvgIncorrectIcon = (props: IconProps) => {
+ const { color: token = '$neutral-100' } = props
+ const theme = useTheme()
+ /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
+ // @ts-ignore
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const color = theme[token]?.val ?? token
+ return (
+
+ )
+}
+export default SvgIncorrectIcon
diff --git a/packages/icons/20/index.ts b/packages/icons/20/index.ts
index cce26a42..9321de0a 100644
--- a/packages/icons/20/index.ts
+++ b/packages/icons/20/index.ts
@@ -85,6 +85,7 @@ export { default as HistoryIcon } from './history-icon'
export { default as HoldIcon } from './hold-icon'
export { default as ImageIcon } from './image-icon'
export { default as InactiveIcon } from './inactive-icon'
+export { default as IncorrectIcon } from './incorrect-icon'
export { default as InfoBadgeIcon } from './info-badge-icon'
export { default as InfoIcon } from './info-icon'
export { default as ItalicIcon } from './italic-icon'
diff --git a/packages/icons/src/20/correct-icon.svg b/packages/icons/src/20/correct-icon.svg
index 7dab968d..5a5656e2 100644
--- a/packages/icons/src/20/correct-icon.svg
+++ b/packages/icons/src/20/correct-icon.svg
@@ -5,17 +5,10 @@
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
-
+
diff --git a/packages/icons/src/20/incorrect-icon.svg b/packages/icons/src/20/incorrect-icon.svg
new file mode 100644
index 00000000..35d5e858
--- /dev/null
+++ b/packages/icons/src/20/incorrect-icon.svg
@@ -0,0 +1,15 @@
+
diff --git a/yarn.lock b/yarn.lock
index b48e9863..e35a37c6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3561,6 +3561,25 @@
"@radix-ui/react-roving-focus" "1.0.3"
"@radix-ui/react-use-controllable-state" "1.0.0"
+"@radix-ui/react-toast@^1.1.3":
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-toast/-/react-toast-1.1.3.tgz#41098f05bace7976cd4c07f6ff418261f86ede6e"
+ integrity sha512-yHFgpxi9wjbfPvpSPdYAzivCqw48eA1ofT8m/WqYOVTxKPdmQMuVKRYPlMmj4C1d6tJdFj/LBa1J4iY3fL4OwQ==
+ dependencies:
+ "@babel/runtime" "^7.13.10"
+ "@radix-ui/primitive" "1.0.0"
+ "@radix-ui/react-collection" "1.0.2"
+ "@radix-ui/react-compose-refs" "1.0.0"
+ "@radix-ui/react-context" "1.0.0"
+ "@radix-ui/react-dismissable-layer" "1.0.3"
+ "@radix-ui/react-portal" "1.0.2"
+ "@radix-ui/react-presence" "1.0.0"
+ "@radix-ui/react-primitive" "1.0.2"
+ "@radix-ui/react-use-callback-ref" "1.0.0"
+ "@radix-ui/react-use-controllable-state" "1.0.0"
+ "@radix-ui/react-use-layout-effect" "1.0.0"
+ "@radix-ui/react-visually-hidden" "1.0.2"
+
"@radix-ui/react-tooltip@^1.0.5":
version "1.0.5"
resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.0.5.tgz#fe20274aeac874db643717fc7761d5a8abdd62d1"
@@ -6364,7 +6383,7 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
-"@types/react-dom@^18.0.11":
+"@types/react-dom@18.0.11", "@types/react-dom@^18.0.11":
version "18.0.11"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.11.tgz#321351c1459bc9ca3d216aefc8a167beec334e33"
integrity sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==
@@ -6378,7 +6397,7 @@
dependencies:
"@types/react" "*"
-"@types/react@*", "@types/react@>=16", "@types/react@^18.0.28":
+"@types/react@*", "@types/react@18.0.28", "@types/react@>=16", "@types/react@^18.0.28":
version "18.0.28"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.28.tgz#accaeb8b86f4908057ad629a26635fe641480065"
integrity sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==