├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── examples └── notifications-solana │ ├── .gitignore │ ├── README.md │ ├── next.config.mjs │ ├── package.json │ ├── postcss.config.js │ ├── public │ ├── next.svg │ └── vercel.svg │ ├── src │ ├── app │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── providers.tsx │ ├── components │ │ ├── NoSSR.tsx │ │ ├── dialect.tsx │ │ ├── theme.tsx │ │ └── wallet.tsx │ └── icons │ │ ├── BookIcon.tsx │ │ ├── GitHubIcon.tsx │ │ ├── MoonIcon.tsx │ │ └── SunIcon.tsx │ ├── tailwind.config.ts │ └── tsconfig.json ├── package.json ├── packages ├── react-sdk-blockchain-solana │ ├── package.json │ ├── src │ │ ├── context.tsx │ │ ├── index.ts │ │ ├── types.ts │ │ └── utils.ts │ └── tsup.config.ts ├── react-sdk │ ├── package.json │ ├── src │ │ ├── context │ │ │ ├── DialectContext │ │ │ │ ├── Gate │ │ │ │ │ └── index.ts │ │ │ │ ├── LocalMessages │ │ │ │ │ └── index.ts │ │ │ │ ├── Sdk │ │ │ │ │ └── index.ts │ │ │ │ ├── Wallet │ │ │ │ │ ├── constants.ts │ │ │ │ │ └── index.ts │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── internal │ │ │ │ ├── swrCache.ts │ │ │ │ └── useLocalStorage.ts │ │ │ ├── useDapp.ts │ │ │ ├── useDappAddresses.ts │ │ │ ├── useDappNotificationSubscriptions.ts │ │ │ ├── useDialectGate.ts │ │ │ ├── useDialectSdk.ts │ │ │ ├── useDialectWallet.ts │ │ │ ├── useNotificationChannel.ts │ │ │ ├── useNotificationChannelDappSubscription.ts │ │ │ ├── useNotificationDapp.ts │ │ │ ├── useNotificationSubscriptions.ts │ │ │ ├── useNotificationThread.ts │ │ │ ├── useNotificationThreadMessages.ts │ │ │ ├── useThread.ts │ │ │ ├── useThreadMessages.ts │ │ │ ├── useThreads.ts │ │ │ └── useUnreadNotifications.ts │ │ ├── index.ts │ │ ├── types.ts │ │ └── utils │ │ │ ├── container.tsx │ │ │ ├── index.ts │ │ │ └── scopes.ts │ └── tsup.config.ts └── react-ui │ ├── .npmignore │ ├── .prettierignore │ ├── .storybook │ ├── main.js │ └── preview.jsx │ ├── README.md │ ├── package.json │ ├── postcss.config.js │ ├── prettier.config.mjs │ ├── src │ ├── index.css │ ├── index.ts │ ├── types.ts │ ├── ui │ │ ├── core │ │ │ ├── Header.tsx │ │ │ ├── icons │ │ │ │ ├── ArrowLeftIcon.tsx │ │ │ │ ├── ArrowRightIcon.tsx │ │ │ │ ├── BellButtonIcon.tsx │ │ │ │ ├── BellButtonIconOutline.tsx │ │ │ │ ├── BellIcon.tsx │ │ │ │ ├── CheckIcon.tsx │ │ │ │ ├── CloseIcon.tsx │ │ │ │ ├── DialectLogo.tsx │ │ │ │ ├── ExclamationIcon.tsx │ │ │ │ ├── LoaderIcon.tsx │ │ │ │ ├── ResendIcon.tsx │ │ │ │ ├── SettingsIcon.tsx │ │ │ │ ├── SpinnerDots.tsx │ │ │ │ ├── TelegramIcon.tsx │ │ │ │ ├── TrashIcon.tsx │ │ │ │ ├── WalletIcon.tsx │ │ │ │ ├── XmarkIcon.tsx │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── model │ │ │ │ ├── api │ │ │ │ │ ├── smart-messages.ts │ │ │ │ │ └── smart-messages.types.ts │ │ │ │ ├── index.ts │ │ │ │ └── useSmartMessage.ts │ │ │ ├── primitives │ │ │ │ ├── Badge.tsx │ │ │ │ ├── Button.tsx │ │ │ │ ├── Checkbox.tsx │ │ │ │ ├── IconButton.tsx │ │ │ │ ├── Input.tsx │ │ │ │ ├── Link.tsx │ │ │ │ ├── Switch.tsx │ │ │ │ ├── TextButton.tsx │ │ │ │ └── index.ts │ │ │ └── wallet-state │ │ │ │ ├── NoWalletState.tsx │ │ │ │ ├── NotAuthorizedState.tsx │ │ │ │ ├── SigningMessageState.tsx │ │ │ │ ├── SigningTransactionState.tsx │ │ │ │ └── WalletStatesWrapper.tsx │ │ ├── index.ts │ │ ├── notifications │ │ │ ├── Notifications.tsx │ │ │ ├── NotificationsButton.tsx │ │ │ ├── NotificationsFeed │ │ │ │ ├── NoNotifications.tsx │ │ │ │ ├── NotificationMessage │ │ │ │ │ ├── ButtonAction.tsx │ │ │ │ │ ├── LinkAction.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── utils.ts │ │ │ │ ├── NotificationsFeed.tsx │ │ │ │ ├── NotificationsFeedHeader.tsx │ │ │ │ ├── NotificationsFeedScreen.tsx │ │ │ │ ├── NotificationsList.tsx │ │ │ │ ├── NotificationsLoading.tsx │ │ │ │ ├── context.ts │ │ │ │ └── index.ts │ │ │ ├── Settings │ │ │ │ ├── AppInfo.tsx │ │ │ │ ├── Channels │ │ │ │ │ ├── ChannelNotificationsToggle.tsx │ │ │ │ │ ├── EmailChannel │ │ │ │ │ │ ├── EmailInput.tsx │ │ │ │ │ │ ├── EmailVerificationCodeInput.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── TelegramChannel │ │ │ │ │ │ ├── TelegramHandleInput.tsx │ │ │ │ │ │ ├── TelegramVerificationCodeInput.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── WalletChannel.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── model │ │ │ │ │ │ └── useVerificationCode.ts │ │ │ │ ├── NotificationTypes.tsx │ │ │ │ ├── Settings.tsx │ │ │ │ ├── SettingsHeader.tsx │ │ │ │ ├── SettingsLoading.tsx │ │ │ │ ├── SettingsScreen.tsx │ │ │ │ ├── TosAndPrivacy.tsx │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ └── internal │ │ │ │ ├── ExternalPropsProvider.tsx │ │ │ │ ├── Router.tsx │ │ │ │ └── useClickAway.ts │ │ └── theme.tsx │ └── utils │ │ ├── displayAddress.ts │ │ ├── index.ts │ │ ├── objects.ts │ │ ├── random.ts │ │ └── types.ts │ ├── stories │ ├── Button.stories.tsx │ ├── Checkbox.stories.tsx │ ├── CustomIcons.stories.tsx │ ├── Input.stories.tsx │ ├── NotificationsFeed.stories.tsx │ └── Switch.stories.tsx │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── tsup.config.ts │ └── vite.config.js ├── prettier.config.mjs ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | dist/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:react/recommended", 10 | "plugin:react/jsx-runtime", 11 | "plugin:react-hooks/recommended", 12 | "prettier" 13 | ], 14 | "parser": "@typescript-eslint/parser", 15 | "plugins": ["@typescript-eslint"] 16 | } 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.lockb binary diff=lockb -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | **/node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | **/target 9 | **/.anchor 10 | 11 | # testing 12 | **/coverage 13 | 14 | # next.js 15 | **/.next/ 16 | **/out/ 17 | 18 | # production & package dist files 19 | **/build 20 | **/lib 21 | **/dist 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | /packages/**/version.ts 27 | 28 | # debug 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | 33 | # local env files 34 | .env.local 35 | .env.development.local 36 | .env.test.local 37 | .env.production.local 38 | 39 | # vercel 40 | .vercel 41 | 42 | # idea 43 | .idea 44 | 45 | *storybook.log -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Dialect](https://www.dialect.to/) React SDK & UI 💬 2 | 3 | ![react-sdk](https://img.shields.io/npm/v/@dialectlabs/react-sdk?color=success&label=react-sdk) ![npm](https://img.shields.io/npm/v/@dialectlabs/react-ui?color=success&label=react-ui) ![npm](https://img.shields.io/npm/v/@dialectlabs/react-sdk-blockchain-solana?color=success&label=react-sdk-blockchain-solana) 4 | 5 | React components to use Dialect's wallet alerts. 6 | 7 | Want to learn how to add Dialect to your dapp? See our [Docs](https://docs.dialect.to/). 8 | 9 | ### Table of Contents 10 | 11 | - [Installation](#Installation) 12 | - [FAQ](#FAQ) 13 | - [Development](#Development) 14 | 15 | ## Installation 16 | 17 | **npm:** 18 | 19 | ```shell 20 | npm install @dialectlabs/react-ui --save 21 | 22 | npm install @dialectlabs/react-sdk-blockchain-solana --save 23 | ``` 24 | 25 | **yarn:** 26 | 27 | ```shell 28 | yarn add @dialectlabs/react-ui 29 | 30 | yarn add @dialectlabs/react-sdk-blockchain-solana 31 | ``` 32 | 33 | ## Development 34 | 35 | ### Prerequisites 36 | 37 | - Git 38 | - yarn 1 39 | - Nodejs (>=18) 40 | 41 | ### Get Started 42 | 43 | This repo utilizes [Workspaces](https://docs.npmjs.com/cli/v10/using-npm/workspaces). Publishable packages are located under `packages` directory. `examples` directory contains apps to demonstrate how can Dialect be used. 44 | 45 | The simplest way to develop on Dialect's component library and headless react contexts locally is to run one of the demo apps in the `examples/` directory, and ensure you are targeting the local instances of `packages/react-sdk`, `packages/react-ui` and `packages/react-sdk-blockchain-solana`. How to best do this is described below. 46 | 47 | Once set up, you'll have live, hot-reloading on changes. Some manual configuration is required to enable this. 48 | 49 | #### Enable hot-reloading from an `examples/` app 50 | 51 | Choose one of the `examples/` apps you'd like to do development from and then make the following changes in its source. For illustration purposes we choose `examples/notifications-solana`. 52 | 53 | For example you want to make changes in `react-ui` library 54 | 55 | Run 56 | 57 | ```shell 58 | yarn run dev:react-ui 59 | ``` 60 | 61 | All of the above changes require restarting the next server and clearing cache (just in case), if you've already started it. 62 | 63 | You can now run the example by following the instructions in the next section. 64 | 65 | #### Start the examples 66 | 67 | To get started, launch example's next dev server: 68 | 69 | ```shell 70 | yarn install # in root dir 71 | cd examples/notifications-solana 72 | yarn run dev 73 | ``` 74 | 75 | Now you have a hot reload of the packages in the workspace. 76 | 77 | ### Publishing 78 | 79 | 1. 80 | 81 | ```shell 82 | yarn run build:all 83 | pushd packages/react-sdk/ 84 | npm publish --access public 85 | popd 86 | pushd packages/react-sdk-blockchain-solana/ 87 | npm publish --access public 88 | popd 89 | pushd packages/react-ui/ 90 | npm publish --access public 91 | popd 92 | ``` 93 | 94 | 2. Update all versions of packages to the new one(e.g. bump react, react-ui version in examples, starters folder) 95 | 96 | 97 | This project is tested with BrowserStack. 98 | -------------------------------------------------------------------------------- /examples/notifications-solana/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /examples/notifications-solana/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /examples/notifications-solana/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /examples/notifications-solana/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dialectlabs/example-notifications-solana", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@dialectlabs/react-ui": "^2.1.0", 13 | "@dialectlabs/react-sdk-blockchain-solana": "^2.1.0", 14 | "@solana/wallet-adapter-react": "^0.15.35", 15 | "@solana/wallet-adapter-react-ui": "^0.9.35", 16 | "@solana/web3.js": "^1.91.3", 17 | "react": "^18", 18 | "react-dom": "^18", 19 | "next": "14.1.4" 20 | }, 21 | "devDependencies": { 22 | "typescript": "^5", 23 | "@types/node": "^20", 24 | "@types/react": "^18", 25 | "@types/react-dom": "^18", 26 | "autoprefixer": "^10.0.1", 27 | "postcss": "^8", 28 | "tailwindcss": "^3.3.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/notifications-solana/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /examples/notifications-solana/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/notifications-solana/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/notifications-solana/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dialectlabs/react/574599450996e433ca38887d4e0e9737e24cca36/examples/notifications-solana/src/app/favicon.ico -------------------------------------------------------------------------------- /examples/notifications-solana/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @import url('@solana/wallet-adapter-react-ui/styles.css'); 6 | 7 | 8 | :root { 9 | --background: theme('colors.light.90'); 10 | --textColor: theme('colors.dark.90'); 11 | --themeSwitchBg: theme('colors.light.70'); 12 | --buttonBg: theme('backgroundImage.gradient-button'); 13 | --buttonTextColor: white; 14 | } 15 | 16 | [data-theme="dark"] { 17 | --background: black; 18 | --textColor: white; 19 | --themeSwitchBg: theme('colors.dark.70'); 20 | --buttonBg: theme('colors.light.100'); 21 | --buttonTextColor: theme('colors.dark.100'); 22 | } 23 | 24 | 25 | body, html { 26 | background-color: var(--background); 27 | color: var(--textColor); 28 | -webkit-font-smoothing: antialiased; 29 | } 30 | 31 | .dt-modal { 32 | box-shadow: 0 16px 40px 0 rgba(27, 27, 28, 0.16); 33 | } 34 | 35 | .wallet-adapter-dropdown { 36 | @apply relative flex w-full; 37 | } 38 | 39 | .wallet-adapter-button-trigger { 40 | @apply text-button flex flex-1 items-center justify-center truncate whitespace-nowrap rounded-lg h-10 p-4 font-medium hover:opacity-90; 41 | } 42 | 43 | .wallet-adapter-button-trigger, .wallet-adapter-button-trigger:not([disabled]):hover { 44 | background: var(--buttonBg); 45 | color: var(--buttonTextColor); 46 | } 47 | 48 | .wallet-adapter-button > * > svg > path{ 49 | fill: var(--buttonTextColor); 50 | } 51 | 52 | .wallet-adapter-dropdown-list { 53 | background-color: theme('backgroundColor.dark.90'); 54 | } 55 | .wallet-adapter-dropdown-list-item:not([disabled]):hover { 56 | background-color: theme('backgroundColor.dark.80'); 57 | } 58 | -------------------------------------------------------------------------------- /examples/notifications-solana/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { Inter } from 'next/font/google'; 3 | import './globals.css'; 4 | import { Providers } from './providers'; 5 | 6 | const inter = Inter({ subsets: ['latin'] }); 7 | 8 | export const metadata: Metadata = { 9 | title: 'Dialect Solana Notifications Example', 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | 20 | {children} 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /examples/notifications-solana/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { DialectSolanaNotificationsButton } from '@/components/dialect'; 4 | import NoSSR from '@/components/NoSSR'; 5 | import { getInitialTheme, ThemeSwitch } from '@/components/theme'; 6 | import { SolanaWalletButton } from '@/components/wallet'; 7 | import { BookIcon } from '@/icons/BookIcon'; 8 | import { GitHubIcon } from '@/icons/GitHubIcon'; 9 | import { ThemeType } from '@dialectlabs/react-ui'; 10 | import { useState } from 'react'; 11 | 12 | export default function Home() { 13 | const [theme, setTheme] = useState(getInitialTheme()); 14 | return ( 15 |
16 |
17 | 18 |
19 | 25 | 26 | Read our Docs 27 | 28 | 34 | 35 | View the Code 36 | 37 |
38 |
39 | 40 | 41 | 42 |
43 |
44 |
45 |
46 |
47 |

@dialectlabs/react

48 |

examples/notifications-solana

49 |
50 | 51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /examples/notifications-solana/src/app/providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { 3 | ConnectionProvider, 4 | WalletProvider, 5 | } from '@solana/wallet-adapter-react'; 6 | import { WalletModalProvider } from '@solana/wallet-adapter-react-ui'; 7 | import { clusterApiUrl } from '@solana/web3.js'; 8 | import React from 'react'; 9 | 10 | const endpoint = clusterApiUrl('devnet'); 11 | 12 | export const Providers: React.FC = (props) => { 13 | return ( 14 | 15 | 16 | {props.children} 17 | 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /examples/notifications-solana/src/components/NoSSR.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic'; 2 | 3 | const NoSSRInner = ({ children }: { children: React.ReactNode }) => ( 4 | <>{children} 5 | ); 6 | 7 | export default dynamic(() => Promise.resolve(NoSSRInner), { ssr: false }); 8 | -------------------------------------------------------------------------------- /examples/notifications-solana/src/components/dialect.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import '@dialectlabs/react-ui/index.css'; 4 | 5 | import { 6 | DialectSolanaSdk, 7 | Environment, 8 | } from '@dialectlabs/react-sdk-blockchain-solana'; 9 | import { 10 | Icons, 11 | NotificationTypeStyles, 12 | NotificationsButton, 13 | ThemeType, 14 | } from '@dialectlabs/react-ui'; 15 | 16 | const DAPP_ADDRESS = 17 | process.env.NEXT_PUBLIC_DAPP_ADDRESS ?? 18 | 'D1ALECTfeCZt9bAbPWtJk7ntv24vDYGPmyS7swp7DY5h'; 19 | 20 | NotificationTypeStyles.offer_outbid = { 21 | Icon: , 22 | iconColor: '#FFFFFF', 23 | iconBackgroundColor: '#FF0000', 24 | iconBackgroundBackdropColor: '#FF00001A', 25 | linkColor: '#FF0000', 26 | actionGradientStartColor: '#FF00001A', 27 | }; 28 | 29 | export const DialectSolanaNotificationsButton = (props: { 30 | theme: ThemeType; 31 | }) => { 32 | return ( 33 | 40 | 41 | 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /examples/notifications-solana/src/components/theme.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { MoonIcon } from '@/icons/MoonIcon'; 4 | import { SunIcon } from '@/icons/SunIcon'; 5 | import { ThemeType } from '@dialectlabs/react-ui'; 6 | import { useCallback, useEffect } from 'react'; 7 | 8 | export const getInitialTheme = (): ThemeType => { 9 | return typeof window !== 'undefined' 10 | ? (window.localStorage.getItem('data-theme') as ThemeType) ?? 'light' 11 | : 'light'; 12 | }; 13 | 14 | export const ThemeSwitch = (props: { 15 | theme: ThemeType; 16 | onThemeChange: (theme: ThemeType) => void; 17 | }) => { 18 | const { theme, onThemeChange } = props; 19 | 20 | useEffect(() => { 21 | document.documentElement.setAttribute('data-theme', theme); 22 | window.localStorage.setItem('data-theme', theme); 23 | }, [theme]); 24 | 25 | const changeTheme = useCallback(() => { 26 | const nextTheme = theme === 'light' ? 'dark' : 'light'; 27 | onThemeChange(nextTheme); 28 | }, [theme, onThemeChange]); 29 | 30 | const translation = theme === 'dark' ? 'translate-x-4' : 'translate-x-0'; 31 | 32 | const icon = <>{theme === 'light' ? : }; 33 | 34 | return ( 35 | 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /examples/notifications-solana/src/components/wallet.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import dynamic from 'next/dynamic'; 3 | 4 | const WalletIcon = () => ( 5 | 6 | 10 | 11 | ); 12 | 13 | const CONNECT_WALLET = ( 14 | <> 15 |
16 | 17 | Connect Wallet 18 |
19 | 20 | ); 21 | const LABELS = { 22 | 'change-wallet': 'Change wallet', 23 | connecting: 'Connecting ...', 24 | 'copy-address': 'Copy address', 25 | copied: 'Copied', 26 | disconnect: 'Disconnect', 27 | 'has-wallet': 'Connect', 28 | 'no-wallet': CONNECT_WALLET, 29 | } as const; 30 | 31 | const BaseWalletMultiButton = dynamic( 32 | async () => 33 | (await import('@solana/wallet-adapter-react-ui')).BaseWalletMultiButton, 34 | { ssr: false }, 35 | ); 36 | 37 | export const SolanaWalletButton = () => { 38 | return ( 39 |
40 | {/*@ts-expect-error labels type*/} 41 | 42 |
43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /examples/notifications-solana/src/icons/BookIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | export const BookIcon = (props: SVGProps) => ( 4 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /examples/notifications-solana/src/icons/GitHubIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | export const GitHubIcon = (props: SVGProps) => ( 4 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /examples/notifications-solana/src/icons/MoonIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | export const MoonIcon = (props: SVGProps) => ( 4 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /examples/notifications-solana/src/icons/SunIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | export const SunIcon = (props: SVGProps) => ( 4 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /examples/notifications-solana/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss'; 2 | 3 | const config: Config = { 4 | content: [ 5 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './src/components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './src/app/**/*.{js,ts,jsx,tsx,mdx}', 8 | ], 9 | theme: { 10 | colors: { 11 | dark: { 12 | 100: '#1b1b1c', 13 | 90: '#232324', 14 | 80: '#2a2a2b', 15 | 70: '#323335', 16 | 60: '#383a3c', 17 | 50: '#434445', 18 | 40: '#656564', 19 | 30: '#737373', 20 | 20: '#888989', 21 | }, 22 | light: { 23 | 100: '#ffffff', 24 | 90: '#f9f9f9', 25 | 80: '#f2f3f5', 26 | 70: '#ebebeb', 27 | 60: '#dee1e7', 28 | 50: '#d7d7d7', 29 | 40: '#c4c6c8', 30 | 30: '#b3b3b3', 31 | }, 32 | accent: { 33 | cyan: '#09CBBF', 34 | red: '#F62D2D', 35 | }, 36 | primary: '#FFFFFF', 37 | transparent: 'transparent', 38 | current: 'currentColor', 39 | }, 40 | extend: { 41 | backgroundImage: { 42 | 'gradient-button': 43 | 'linear-gradient(95deg, #2B2D2D 4.07%, #414445 51.31%, #2B2D2D 95.93%)', 44 | }, 45 | fontSize: { 46 | button: [ 47 | '0.938rem', 48 | { 49 | fontWeight: 400, 50 | lineHeight: '1.25rem', 51 | }, 52 | ], 53 | }, 54 | }, 55 | }, 56 | plugins: [], 57 | }; 58 | export default config; 59 | -------------------------------------------------------------------------------- /examples/notifications-solana/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dialectlabs/workspaces", 3 | "version": "1.0.0", 4 | "description": "", 5 | "private": true, 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/dialectlabs/react" 9 | }, 10 | "scripts": { 11 | "bootstrap": "yarn build:all", 12 | "build:all": "yarn workspace @dialectlabs/react-sdk build && yarn workspace @dialectlabs/react-sdk-blockchain-solana build && yarn workspace @dialectlabs/react-ui build", 13 | "dev:react-sdk": "yarn workspace @dialectlabs/react-sdk dev", 14 | "dev:react-sdk-blockchain-solana": "yarn workspace @dialectlabs/react-sdk-blockchain-solana dev", 15 | "dev:react-ui": "yarn workspace @dialectlabs/react-ui dev", 16 | "dev:all": "concurrently \"yarn:dev:*(!all)\"" 17 | }, 18 | "devDependencies": { 19 | "@typescript-eslint/eslint-plugin": "^7.5.0", 20 | "@typescript-eslint/parser": "^7.5.0", 21 | "concurrently": "^8.2.2", 22 | "eslint": "^8.57.0", 23 | "eslint-config-prettier": "^9.1.0", 24 | "eslint-plugin-react": "^7.33.2", 25 | "eslint-plugin-react-hooks": "^4.6.0", 26 | "prettier": "^3.2.5", 27 | "prettier-plugin-organize-imports": "^3.2.4", 28 | "prettier-plugin-tailwindcss": "^0.5.13", 29 | "tsup": "^8.0.2", 30 | "typescript": "^5.4.3" 31 | }, 32 | "keywords": [], 33 | "author": "@dialectlabs", 34 | "license": "Apache-2.0", 35 | "workspaces": [ 36 | "examples/*", 37 | "packages/*" 38 | ], 39 | "engines": { 40 | "node": ">=18" 41 | }, 42 | "dependencies": {} 43 | } 44 | -------------------------------------------------------------------------------- /packages/react-sdk-blockchain-solana/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dialectlabs/react-sdk-blockchain-solana", 3 | "version": "2.1.1", 4 | "type": "module", 5 | "license": "MIT", 6 | "private": false, 7 | "sideEffects": false, 8 | "main": "dist/index.cjs", 9 | "module": "dist/index.js", 10 | "types": "dist/index.d.ts", 11 | "exports": { 12 | ".": { 13 | "import": "./dist/index.js", 14 | "require": "./dist/index.cjs" 15 | } 16 | }, 17 | "files": [ 18 | "dist" 19 | ], 20 | "scripts": { 21 | "build": "tsup-node", 22 | "dev": "tsup-node --watch" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/dialectlabs/react" 27 | }, 28 | "dependencies": { 29 | "@dialectlabs/blockchain-sdk-solana": "^1.2.1", 30 | "@dialectlabs/react-sdk": "^2.1.0", 31 | "@solana/web3.js": "^1.95.2" 32 | }, 33 | "devDependencies": { 34 | "tsup": "^8.0.2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/react-sdk-blockchain-solana/src/context.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | DialectSolanaWalletAdapter as DialectSdkSolanaWalletAdapter, 3 | SolanaSdkFactory, 4 | } from '@dialectlabs/blockchain-sdk-solana'; 5 | import { 6 | DialectContextProvider, 7 | DialectWalletStatesHolder, 8 | } from '@dialectlabs/react-sdk'; 9 | import type { ConfigProps } from '@dialectlabs/sdk'; 10 | import { WalletContextState, useWallet } from '@solana/wallet-adapter-react'; 11 | import { PublicKey, Transaction, VersionedTransaction } from '@solana/web3.js'; 12 | import React, { useCallback, useEffect, useMemo } from 'react'; 13 | 14 | export type Props = { 15 | dappAddress: string; 16 | config?: ConfigProps; 17 | customWalletAdapter?: DialectSolanaWalletAdapter; 18 | children: React.ReactNode; 19 | }; 20 | 21 | export interface DialectSolanaWalletAdapter { 22 | publicKey?: PublicKey | null; 23 | signTransaction?: ( 24 | tx: T, 25 | ) => Promise; 26 | signMessage?: (msg: Uint8Array) => Promise; 27 | } 28 | 29 | type UnifiedWallet = WalletContextState | DialectSolanaWalletAdapter; 30 | 31 | const SolanaBlockchainSdkWrapper = (props: Props) => { 32 | const walletFromContext = useWallet(); 33 | const isWalletFromContext = props.customWalletAdapter === undefined; 34 | const wallet = isWalletFromContext 35 | ? walletFromContext 36 | : props.customWalletAdapter; 37 | 38 | const { 39 | walletConnected: { set: setWalletConnected }, 40 | connectionInitiatedState: { set: setConnectionInitiated }, 41 | isSigningFreeTransactionState: { set: setIsSigningFreeTransaction }, 42 | isSigningMessageState: { set: setIsSigningMessage }, 43 | hardwareWalletForcedState: { get: isHardwareWalletForced }, 44 | } = DialectWalletStatesHolder.useContainer(); 45 | 46 | //used to pass solana wallet context state to internal state holder 47 | const wrapSolanaWallet = useCallback( 48 | (wallet: UnifiedWallet): DialectSdkSolanaWalletAdapter => { 49 | return { 50 | publicKey: wallet.publicKey ?? undefined, 51 | signTransaction: wallet.signTransaction 52 | ? // eslint-disable-next-line @typescript-eslint/no-explicit-any 53 | async (tx: any) => { 54 | const isFreeTx = 55 | tx.recentBlockhash && 56 | tx.recentBlockhash === PublicKey.default.toString(); 57 | if (isFreeTx) { 58 | setIsSigningFreeTransaction(true); 59 | } 60 | try { 61 | return await wallet.signTransaction!(tx); 62 | } catch (e) { 63 | if (isFreeTx) { 64 | // assuming free tx is the tx for auth 65 | setConnectionInitiated(false); 66 | } 67 | throw e; 68 | } finally { 69 | if (isFreeTx) { 70 | setIsSigningFreeTransaction(false); 71 | } 72 | } 73 | } 74 | : undefined, 75 | signMessage: 76 | !isHardwareWalletForced && wallet.signMessage 77 | ? async (msg) => { 78 | setIsSigningMessage(true); 79 | try { 80 | return await wallet.signMessage!(msg); 81 | } catch (e) { 82 | // assuming sign message used only for auth 83 | setConnectionInitiated(false); 84 | throw e; 85 | } finally { 86 | setIsSigningMessage(false); 87 | } 88 | } 89 | : undefined, 90 | }; 91 | }, 92 | [ 93 | isHardwareWalletForced, 94 | setConnectionInitiated, 95 | setIsSigningFreeTransaction, 96 | setIsSigningMessage, 97 | ], 98 | ); 99 | 100 | const blockchainSdkFactory = useMemo(() => { 101 | if (!wallet || !wallet.publicKey) { 102 | return null; 103 | } 104 | return SolanaSdkFactory.create({ 105 | wallet: wrapSolanaWallet(wallet), 106 | }); 107 | }, [wallet, wrapSolanaWallet]); 108 | 109 | useEffect(() => { 110 | setWalletConnected(Boolean(wallet && wallet.publicKey)); 111 | }, [setWalletConnected, wallet]); 112 | 113 | return ( 114 | 118 | ); 119 | }; 120 | 121 | export const DialectSolanaSdk = (props: Props) => { 122 | return ( 123 | 124 | 125 | 126 | ); 127 | }; 128 | -------------------------------------------------------------------------------- /packages/react-sdk-blockchain-solana/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@dialectlabs/react-sdk'; 2 | 3 | export * from './context'; 4 | export * from './types'; 5 | -------------------------------------------------------------------------------- /packages/react-sdk-blockchain-solana/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | DialectSolanaWalletAdapter, 3 | SolanaConfigProps as SolanaSdkConfigProps, 4 | } from '@dialectlabs/blockchain-sdk-solana'; 5 | 6 | type WalletOptional = Omit< 7 | T, 8 | 'wallet' 9 | > & { 10 | wallet?: DialectSolanaWalletAdapter | null; 11 | }; 12 | 13 | export type SolanaConfigProps = WalletOptional; 14 | -------------------------------------------------------------------------------- /packages/react-sdk-blockchain-solana/src/utils.ts: -------------------------------------------------------------------------------- 1 | export const EMPTY_OBJ = {}; 2 | export const EMPTY_ARR = []; 3 | // eslint-disable-next-line @typescript-eslint/no-empty-function 4 | export const NOOP = (): void => {}; 5 | -------------------------------------------------------------------------------- /packages/react-sdk-blockchain-solana/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | splitting: false, 6 | sourcemap: true, 7 | clean: true, 8 | dts: true, 9 | format: ['cjs', 'esm'], 10 | target: ['esnext'], 11 | }); 12 | -------------------------------------------------------------------------------- /packages/react-sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dialectlabs/react-sdk", 3 | "version": "2.1.1", 4 | "type": "module", 5 | "private": false, 6 | "sideEffects": false, 7 | "main": "dist/index.cjs", 8 | "module": "dist/index.js", 9 | "types": "dist/index.d.ts", 10 | "exports": { 11 | ".": { 12 | "import": "./dist/index.js", 13 | "require": "./dist/index.cjs" 14 | } 15 | }, 16 | "files": [ 17 | "dist" 18 | ], 19 | "scripts": { 20 | "build": "tsup-node", 21 | "dev": "tsup-node --watch" 22 | }, 23 | "devDependencies": { 24 | "@types/react": "^18.2.73", 25 | "tsup": "^8.0.2" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/dialectlabs/react" 30 | }, 31 | "dependencies": { 32 | "@dialectlabs/sdk": "^1.9.4", 33 | "swr": "^2.2.5" 34 | }, 35 | "peerDependencies": { 36 | "react": ">=18" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/react-sdk/src/context/DialectContext/Gate/index.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from 'react'; 2 | import { createContainer } from '../../../utils/container'; 3 | 4 | export type Gate = () => boolean | Promise; 5 | 6 | const PASSING_GATE = () => true; 7 | 8 | export interface DialectGateState { 9 | isGatePassed: boolean; 10 | isGateLoading: boolean; 11 | } 12 | 13 | function useDialectGate(gate: Gate = PASSING_GATE): DialectGateState { 14 | const [isGateLoading, setIsGateLoading] = useState(false); 15 | const [isGatePassed, setIsGatePassed] = useState(false); 16 | 17 | const gateWrapper = useCallback(async () => { 18 | setIsGateLoading(true); 19 | try { 20 | const gateRes = await gate(); 21 | setIsGatePassed(gateRes); 22 | return gateRes; 23 | } catch { 24 | setIsGatePassed(false); 25 | } finally { 26 | setIsGateLoading(false); 27 | } 28 | }, [gate]); 29 | 30 | useEffect( 31 | function verifyGate() { 32 | gateWrapper(); 33 | }, 34 | [gateWrapper] 35 | ); 36 | 37 | return { 38 | isGatePassed, 39 | isGateLoading, 40 | }; 41 | } 42 | 43 | export const DialectGate = createContainer(useDialectGate); 44 | -------------------------------------------------------------------------------- /packages/react-sdk/src/context/DialectContext/LocalMessages/index.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | import type { LocalThreadMessage } from '../../../types'; 3 | import { createContainer } from '../../../utils/container'; 4 | 5 | interface LocalMessagesState { 6 | localMessages: Record; 7 | putLocalMessage: (threadAddr: string, msg: LocalThreadMessage) => void; 8 | deleteLocalMessage: (threadAddr: string, id: string) => void; 9 | } 10 | 11 | function useLocalMessages(): LocalMessagesState { 12 | const [localMessages, setLocalMessages] = useState< 13 | LocalMessagesState['localMessages'] 14 | >({}); 15 | 16 | const putLocalMessage = useCallback( 17 | (threadAddr: string, msg: LocalThreadMessage) => { 18 | setLocalMessages((prev) => { 19 | const threadMessages = prev[threadAddr]; 20 | if (!threadMessages) { 21 | return { 22 | ...prev, 23 | [threadAddr]: [msg], 24 | }; 25 | } 26 | 27 | return { 28 | ...prev, 29 | [threadAddr]: [ 30 | msg, 31 | ...threadMessages.filter( 32 | (prevMsg) => prevMsg.deduplicationId !== msg.deduplicationId 33 | ), 34 | ], 35 | }; 36 | }); 37 | }, 38 | [] 39 | ); 40 | 41 | const deleteLocalMessage = useCallback((threadAddr: string, id: string) => { 42 | setLocalMessages((prev) => { 43 | const threadMessages = prev[threadAddr]; 44 | if (!threadMessages) { 45 | return { 46 | ...prev, 47 | [threadAddr]: [], 48 | }; 49 | } 50 | return { 51 | ...prev, 52 | [threadAddr]: threadMessages.filter( 53 | (prevMsg) => prevMsg.deduplicationId !== id 54 | ), 55 | }; 56 | }); 57 | }, []); 58 | 59 | return { localMessages, putLocalMessage, deleteLocalMessage }; 60 | } 61 | 62 | export const LocalMessages = createContainer(useLocalMessages); 63 | -------------------------------------------------------------------------------- /packages/react-sdk/src/context/DialectContext/Sdk/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BlockchainSdk, 3 | BlockchainSdkFactory, 4 | ConfigProps, 5 | Dialect, 6 | DialectSdk as DialectSdkType, 7 | } from '@dialectlabs/sdk'; 8 | import { useEffect, useMemo } from 'react'; 9 | import { createContainer } from '../../../utils/container'; 10 | import { DialectWalletStatesHolder } from '../Wallet'; 11 | 12 | interface DialectSdkProps { 13 | config?: ConfigProps; 14 | blockchainSdkFactory?: BlockchainSdkFactory | null; 15 | } 16 | 17 | interface DialectSdkState { 18 | sdk: DialectSdkType | null; 19 | } 20 | 21 | const DEFAULT_CONFIG: ConfigProps = { 22 | dialectCloud: { 23 | tokenStore: 'local-storage', 24 | tokenLifetimeMinutes: 43200, // 1 month 25 | }, 26 | }; 27 | 28 | function useDialectSdk( 29 | { 30 | config = DEFAULT_CONFIG, 31 | blockchainSdkFactory, 32 | }: DialectSdkProps = {} as DialectSdkProps, 33 | ): DialectSdkState { 34 | const { 35 | walletConnected: { get: walletConnected }, 36 | connectionInitiatedState: { set: setConnectionInitiated }, 37 | } = DialectWalletStatesHolder.useContainer(); 38 | 39 | const sdk = useMemo(() => { 40 | if (!blockchainSdkFactory || !walletConnected) { 41 | return null; 42 | } 43 | return Dialect.sdk({ ...DEFAULT_CONFIG, ...config }, blockchainSdkFactory); 44 | }, [config, blockchainSdkFactory, walletConnected]); 45 | 46 | // The idea is to check if we already has token stored somewhere to skip NotAuthorized screen 47 | // so that we check if sdk is about to be configred with local storage 48 | // and if so, we validate the token 49 | // if token is valid, then NotAuthorized will be skipped 50 | useEffect( 51 | function preValidateSdkToken() { 52 | if (!sdk) return; 53 | if (sdk.info.hasValidAuthentication) { 54 | setConnectionInitiated(true); 55 | } 56 | }, 57 | [sdk, setConnectionInitiated], 58 | ); 59 | 60 | return { 61 | sdk, 62 | }; 63 | } 64 | 65 | export const DialectSdk = createContainer(useDialectSdk); 66 | -------------------------------------------------------------------------------- /packages/react-sdk/src/context/DialectContext/Wallet/constants.ts: -------------------------------------------------------------------------------- 1 | export const DIALECT_WALLET_CONFIG_STORAGE_KEY = 'dialect-wallet-config'; 2 | -------------------------------------------------------------------------------- /packages/react-sdk/src/context/DialectContext/Wallet/index.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction, useCallback, useState } from 'react'; 2 | import { useLocalStorage } from '../../../hooks/internal/useLocalStorage'; 3 | import { createContainer } from '../../../utils/container'; 4 | import { DIALECT_WALLET_CONFIG_STORAGE_KEY } from './constants'; 5 | 6 | interface State { 7 | get: T; 8 | set: Dispatch>; 9 | } 10 | 11 | export interface DialectWalletStatesHolderState { 12 | walletConnected: State; 13 | connectionInitiatedState: State; 14 | hardwareWalletForcedState: State; 15 | isSigningFreeTransactionState: State; 16 | isSigningMessageState: State; 17 | } 18 | 19 | export interface HardwareWalletConfig { 20 | hardwareWalletEnabled: boolean; 21 | } 22 | 23 | function useDialectWalletStatesHolder(): DialectWalletStatesHolderState { 24 | const [walletConnected, setWalletConnected] = useState(false); 25 | const [connectionInitiated, setConnectionInitiated] = useState(false); 26 | 27 | const [ 28 | localStorageHardwareWalletConfig, 29 | setLocalStorageHardwareWalletConfig, 30 | ] = useLocalStorage(DIALECT_WALLET_CONFIG_STORAGE_KEY, { 31 | hardwareWalletEnabled: false, 32 | }); 33 | const [hardwareWalletForced, setHardwareWalletForced] = useState( 34 | localStorageHardwareWalletConfig.hardwareWalletEnabled, 35 | ); 36 | const handleSetHardwareWalletForced: Dispatch> = 37 | useCallback( 38 | (valOrFunc) => { 39 | const newState = 40 | typeof valOrFunc === 'function' 41 | ? valOrFunc(hardwareWalletForced) 42 | : valOrFunc; 43 | 44 | setHardwareWalletForced(newState); 45 | setLocalStorageHardwareWalletConfig({ 46 | hardwareWalletEnabled: newState, 47 | }); 48 | }, 49 | [hardwareWalletForced, setLocalStorageHardwareWalletConfig], 50 | ); 51 | 52 | const [isSigningFreeTransaction, setIsSigningFreeTransaction] = 53 | useState(false); 54 | const [isSigningMessage, setIsSigningMessage] = useState(false); 55 | 56 | return { 57 | walletConnected: { 58 | get: walletConnected, 59 | set: setWalletConnected, 60 | }, 61 | connectionInitiatedState: { 62 | get: connectionInitiated, 63 | set: setConnectionInitiated, 64 | }, 65 | hardwareWalletForcedState: { 66 | get: hardwareWalletForced, 67 | set: handleSetHardwareWalletForced, 68 | }, 69 | isSigningFreeTransactionState: { 70 | get: isSigningFreeTransaction, 71 | set: setIsSigningFreeTransaction, 72 | }, 73 | isSigningMessageState: { 74 | get: isSigningMessage, 75 | set: setIsSigningMessage, 76 | }, 77 | }; 78 | } 79 | 80 | export const DialectWalletStatesHolder = createContainer( 81 | useDialectWalletStatesHolder, 82 | ); 83 | -------------------------------------------------------------------------------- /packages/react-sdk/src/context/DialectContext/index.tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | BlockchainSdk, 3 | BlockchainSdkFactory, 4 | ConfigProps, 5 | } from '@dialectlabs/sdk'; 6 | import React, { useContext } from 'react'; 7 | import { SWRConfig } from 'swr'; 8 | import { LocalMessages } from './LocalMessages'; 9 | import { DialectSdk } from './Sdk'; 10 | 11 | interface DialectContextValue { 12 | dappAddress: string; 13 | } 14 | 15 | export const DialectContext = React.createContext( 16 | {} as DialectContextValue, 17 | ); 18 | 19 | export type DialectContextProviderProps = { 20 | dappAddress: string; 21 | config?: ConfigProps; 22 | blockchainSdkFactory?: BlockchainSdkFactory | null; 23 | // gate?: Gate; 24 | children: React.ReactNode; 25 | }; 26 | 27 | export const useDialectContext = () => { 28 | return useContext(DialectContext); 29 | }; 30 | 31 | export const DialectContextProvider: React.FC< 32 | DialectContextProviderProps 33 | > = ({ config, blockchainSdkFactory, children, dappAddress }) => { 34 | return ( 35 | 36 | 37 | 38 | {/* */} 39 | {children} 40 | {/* */} 41 | 42 | 43 | 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /packages/react-sdk/src/context/index.tsx: -------------------------------------------------------------------------------- 1 | export { DialectContextProvider, useDialectContext } from './DialectContext'; 2 | export type { DialectContextProviderProps } from './DialectContext'; 3 | export { DialectWalletStatesHolder } from './DialectContext/Wallet'; 4 | -------------------------------------------------------------------------------- /packages/react-sdk/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useDapp } from './useDapp'; 2 | export { default as useDappAddresses } from './useDappAddresses'; 3 | export { default as useDappNotificationSubscriptions } from './useDappNotificationSubscriptions'; 4 | export { default as useDialectGate } from './useDialectGate'; 5 | export { default as useDialectSdk } from './useDialectSdk'; 6 | export { default as useDialectWallet } from './useDialectWallet'; 7 | export { default as useNotificationChannel } from './useNotificationChannel'; 8 | export { default as useNotificationChannelDappSubscription } from './useNotificationChannelDappSubscription'; 9 | export { default as useNotificationDapp } from './useNotificationDapp'; 10 | export { default as useNotificationSubscriptions } from './useNotificationSubscriptions'; 11 | export { default as useNotificationThread } from './useNotificationThread'; 12 | export { default as useNotificationThreadMessages } from './useNotificationThreadMessages'; 13 | export { default as useThread } from './useThread'; 14 | export { default as useThreadMessages } from './useThreadMessages'; 15 | export { default as useThreads } from './useThreads'; 16 | export { default as useUnreadNotifications } from './useUnreadNotifications'; 17 | -------------------------------------------------------------------------------- /packages/react-sdk/src/hooks/internal/swrCache.ts: -------------------------------------------------------------------------------- 1 | import type { AccountAddress, FindThreadQuery } from '@dialectlabs/sdk'; 2 | 3 | export const CACHE_KEY_THREADS = 'THREADS'; 4 | 5 | export const CACHE_KEY_THREAD_FN = (findParams: FindThreadQuery): string => { 6 | const prefix = 'THREAD_'; 7 | if ('id' in findParams) { 8 | return prefix + findParams.id.toString(); 9 | } 10 | if ('otherMembers' in findParams) { 11 | return ( 12 | prefix + 13 | findParams.otherMembers 14 | .filter((it) => it) 15 | .map((it) => it.toString()) 16 | .join(':') 17 | ); 18 | } 19 | throw new Error('should never happen'); 20 | }; 21 | 22 | export const CACHE_KEY_MESSAGES_FN = (id: string) => `MESSAGES_${id}`; 23 | 24 | export const CACHE_KEY_THREAD_SUMMARY_FN = (otherMembers: AccountAddress[]) => 25 | 'THREAD_SUMMARY_' + 26 | otherMembers 27 | .filter((it) => it) 28 | .map((it) => it.toString()) 29 | .join(':'); 30 | 31 | export const CACHE_KEY_THREADS_SUMMARY = 'THREADS_GENERAL_SUMMARY'; 32 | 33 | export const DAPPS_CACHE_KEY = 'DAPPS'; 34 | 35 | export const DAPP_CACHE_KEY_FN = (walletAddress: AccountAddress) => 36 | 'DAPPS_' + walletAddress; 37 | 38 | export const DAPP_ADDRESSES_CACHE_KEY_FN = (dappAddress?: AccountAddress) => 39 | 'DAPP_ADDRESSES_' + dappAddress; 40 | 41 | export const WALLET_ADDRESSES_CACHE_KEY_FN = (walletAddress: AccountAddress) => 42 | 'WALLET_ADDRESSES_' + walletAddress; 43 | 44 | export const WALLET_DAPP_ADDRESSES_CACHE_KEY_FN = ( 45 | walletAddress: AccountAddress, 46 | dappAddress: AccountAddress, 47 | ) => 'WALLET_DAPP_ADDRESSES_' + walletAddress + '_' + dappAddress; 48 | 49 | export const WALLET_NOTIFICATION_SUBSCRIPTIONS_CACHE_KEY_FN = ( 50 | walletAddress: AccountAddress, 51 | dappAddress: AccountAddress = '', 52 | ) => `WALLET_NOTIFICATION_SUBSCRIPTIONS_${walletAddress}${dappAddress}`; 53 | 54 | export const DAPP_NOTIFICATION_SUBSCRIPTIONS_CACHE_KEY_FN = ( 55 | dappAddress?: AccountAddress, 56 | ) => 'DAPP_NOTIFICATION_SUBSCRIPTIONS_' + dappAddress; 57 | -------------------------------------------------------------------------------- /packages/react-sdk/src/hooks/internal/useLocalStorage.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, useCallback, useState } from 'react'; 2 | 3 | const isLocalStorageAvailable = typeof globalThis.localStorage !== 'undefined'; 4 | 5 | export function useLocalStorage( 6 | key: string, 7 | initialValue: T 8 | ): [T, Dispatch] { 9 | // State to store our value 10 | // Pass initial state function to useState so logic is only executed once 11 | const [storedValue, setStoredValue] = useState(() => { 12 | if (!isLocalStorageAvailable) { 13 | return initialValue; 14 | } 15 | try { 16 | // Get from local storage by key 17 | const item = globalThis.localStorage.getItem(key); 18 | if (item !== null) { 19 | return JSON.parse(item); 20 | } 21 | 22 | // Save initial value 23 | globalThis.localStorage.setItem(key, JSON.stringify(initialValue)); 24 | return initialValue; 25 | // Parse stored json or if none return initialValue 26 | } catch (error) { 27 | // If error also return initialValue 28 | console.error(error); 29 | return initialValue; 30 | } 31 | }); 32 | 33 | // Return a wrapped version of useState's setter function that 34 | // persists the new value to localStorage. 35 | const setValue: Dispatch = useCallback( 36 | (value) => { 37 | if (!isLocalStorageAvailable) { 38 | console.warn( 39 | `Tried setting localStorage key “${key}” even though environment is not a browser` 40 | ); 41 | } 42 | try { 43 | // Save state 44 | setStoredValue(value); 45 | // Save to local storage 46 | if (isLocalStorageAvailable) { 47 | globalThis.localStorage.setItem(key, JSON.stringify(value)); 48 | } 49 | } catch (error) { 50 | // Handle the error case 51 | console.error(error); 52 | } 53 | }, 54 | [key] 55 | ); 56 | return [storedValue, setValue]; 57 | } 58 | -------------------------------------------------------------------------------- /packages/react-sdk/src/hooks/useDapp.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | BlockchainType, 3 | Dapp, 4 | DialectSdkError, 5 | ReadOnlyDapp, 6 | } from '@dialectlabs/sdk'; 7 | import { useMemo } from 'react'; 8 | import useSWR from 'swr'; 9 | import { EMPTY_ARR, EMPTY_OBJ } from '../utils'; 10 | import { DAPPS_CACHE_KEY, DAPP_CACHE_KEY_FN } from './internal/swrCache'; 11 | import useDialectSdk from './useDialectSdk'; 12 | 13 | interface UseDappValue { 14 | dapp: Dapp | null; 15 | isFetching: boolean; 16 | errorFetching: DialectSdkError | null; 17 | 18 | dapps: Record; 19 | } 20 | 21 | interface UseDappParams { 22 | refreshInterval?: number; 23 | verified?: boolean; 24 | blockchainType?: BlockchainType; 25 | } 26 | 27 | function useDapp({ 28 | refreshInterval, 29 | verified = true, 30 | blockchainType, 31 | }: UseDappParams = EMPTY_OBJ): UseDappValue { 32 | const { dapps } = useDialectSdk(); 33 | const { 34 | wallet: { address: walletAddress }, 35 | } = useDialectSdk(); 36 | const { data: dapp, error } = useSWR( 37 | DAPP_CACHE_KEY_FN(walletAddress), 38 | () => dapps.find(), 39 | { refreshInterval, refreshWhenOffline: true }, 40 | ); 41 | 42 | const { data: dappsList = EMPTY_ARR } = useSWR( 43 | DAPPS_CACHE_KEY, 44 | () => dapps.findAll({ verified, blockchainType }), 45 | { 46 | refreshInterval: 0, 47 | revalidateIfStale: false, 48 | revalidateOnFocus: false, 49 | revalidateOnReconnect: false, 50 | }, 51 | ); 52 | 53 | const allDapps = useMemo( 54 | () => 55 | dappsList.reduce( 56 | (acc, dapp) => { 57 | const address = dapp.address; 58 | if (!acc[address]) { 59 | acc[address] = dapp; 60 | } 61 | return acc; 62 | }, 63 | {} as Record, 64 | ), 65 | [dappsList], 66 | ); 67 | 68 | return { 69 | dapp: dapp || null, 70 | isFetching: !error && dapp === undefined, 71 | errorFetching: error, 72 | 73 | dapps: allDapps, 74 | }; 75 | } 76 | 77 | export default useDapp; 78 | -------------------------------------------------------------------------------- /packages/react-sdk/src/hooks/useDappAddresses.ts: -------------------------------------------------------------------------------- 1 | import type { DappAddress, DialectSdkError } from '@dialectlabs/sdk'; 2 | import { useEffect } from 'react'; 3 | import useSWR from 'swr'; 4 | import { EMPTY_ARR, EMPTY_OBJ } from '../utils'; 5 | import { DAPP_ADDRESSES_CACHE_KEY_FN } from './internal/swrCache'; 6 | import useDapp from './useDapp'; 7 | 8 | interface UseDappAddressesValue { 9 | addresses: DappAddress[]; 10 | isFetching: boolean; 11 | errorFetching: DialectSdkError | null; 12 | } 13 | 14 | interface UseDappAddressesParams { 15 | refreshInterval?: number; 16 | } 17 | 18 | function useDappAddresses({ 19 | refreshInterval, 20 | }: UseDappAddressesParams = EMPTY_OBJ): UseDappAddressesValue { 21 | const { dapp } = useDapp(); 22 | const dappAddressesApi = dapp?.dappAddresses; 23 | 24 | const { 25 | data: addresses, 26 | error = null, 27 | mutate, 28 | } = useSWR( 29 | DAPP_ADDRESSES_CACHE_KEY_FN(dapp?.address), 30 | dappAddressesApi ? () => dappAddressesApi.findAll() : null, 31 | { 32 | refreshInterval, 33 | refreshWhenOffline: true, 34 | } 35 | ); 36 | 37 | useEffect( 38 | function invalidateAddresses() { 39 | mutate(); 40 | }, 41 | [mutate, dappAddressesApi] 42 | ); 43 | 44 | return { 45 | addresses: addresses || EMPTY_ARR, 46 | isFetching: Boolean(dapp) && !error && addresses === undefined, 47 | errorFetching: error, 48 | }; 49 | } 50 | 51 | export default useDappAddresses; 52 | -------------------------------------------------------------------------------- /packages/react-sdk/src/hooks/useDappNotificationSubscriptions.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | DappNotificationSubscription, 3 | DialectSdkError, 4 | } from '@dialectlabs/sdk'; 5 | import useSWR from 'swr'; 6 | import { EMPTY_ARR, EMPTY_OBJ } from '../utils'; 7 | import { DAPP_NOTIFICATION_SUBSCRIPTIONS_CACHE_KEY_FN } from './internal/swrCache'; 8 | import useDapp from './useDapp'; 9 | 10 | interface UseDappNotificationSubscriptionsValue { 11 | subscriptions: DappNotificationSubscription[]; 12 | isFetching: boolean; 13 | errorFetching: DialectSdkError | null; 14 | } 15 | 16 | interface UseDappNotificationSubscriptions { 17 | refreshInterval?: number; 18 | } 19 | 20 | function useDappNotificationSubscriptions({ 21 | refreshInterval, 22 | }: UseDappNotificationSubscriptions = EMPTY_OBJ): UseDappNotificationSubscriptionsValue { 23 | const { dapp } = useDapp(); 24 | const subscriptionsApi = dapp?.notificationSubscriptions; 25 | 26 | const { data: subscriptions, error: errorFetching = null } = useSWR( 27 | DAPP_NOTIFICATION_SUBSCRIPTIONS_CACHE_KEY_FN(dapp?.address), 28 | subscriptionsApi ? () => subscriptionsApi.findAll() : null, 29 | { 30 | refreshInterval, 31 | refreshWhenOffline: true, 32 | } 33 | ); 34 | 35 | return { 36 | subscriptions: subscriptions || EMPTY_ARR, 37 | isFetching: Boolean(dapp) && !errorFetching && subscriptions === undefined, 38 | errorFetching, 39 | }; 40 | } 41 | 42 | export default useDappNotificationSubscriptions; 43 | -------------------------------------------------------------------------------- /packages/react-sdk/src/hooks/useDialectGate.ts: -------------------------------------------------------------------------------- 1 | import { DialectGate, DialectGateState } from '../context/DialectContext/Gate'; 2 | 3 | const useDialectGate = (): Required => { 4 | const gateState = DialectGate.useContainer(); 5 | return gateState; 6 | }; 7 | 8 | export default useDialectGate; 9 | -------------------------------------------------------------------------------- /packages/react-sdk/src/hooks/useDialectSdk.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | BlockchainSdk, 3 | DialectSdk as DialectSdkType, 4 | } from '@dialectlabs/sdk'; 5 | import { DialectSdk } from '../context/DialectContext/Sdk'; 6 | 7 | const useDialectSdk = ( 8 | unsafe?: TUnsafe 9 | ): TUnsafe extends true 10 | ? DialectSdkType | null 11 | : DialectSdkType => { 12 | const { sdk } = DialectSdk.useContainer(); 13 | if (unsafe) { 14 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 15 | // @ts-ignore 16 | return sdk; 17 | } 18 | if (!sdk) { 19 | throw new Error('sdk not initialized'); 20 | } 21 | return sdk; 22 | }; 23 | 24 | export default useDialectSdk; 25 | -------------------------------------------------------------------------------- /packages/react-sdk/src/hooks/useDialectWallet.ts: -------------------------------------------------------------------------------- 1 | import { DialectWalletStatesHolder } from '../context/DialectContext/Wallet'; 2 | 3 | const useDialectWallet = () => { 4 | const walletCtx = DialectWalletStatesHolder.useContainer(); 5 | return walletCtx; 6 | }; 7 | 8 | export default useDialectWallet; 9 | -------------------------------------------------------------------------------- /packages/react-sdk/src/hooks/useNotificationChannel.ts: -------------------------------------------------------------------------------- 1 | import type { Address, AddressType, DialectSdkError } from '@dialectlabs/sdk'; 2 | import { useCallback, useState } from 'react'; 3 | import useSWR from 'swr'; 4 | import { EMPTY_ARR } from '../utils'; 5 | import { WALLET_ADDRESSES_CACHE_KEY_FN } from './internal/swrCache'; 6 | import useDialectSdk from './useDialectSdk'; 7 | 8 | interface CreateParams { 9 | value: string; 10 | } 11 | 12 | interface UpdateParams { 13 | value: string; 14 | } 15 | 16 | interface VerifyParams { 17 | code: string; 18 | } 19 | 20 | interface UseNotificationChannelValue { 21 | globalAddress?: Address; 22 | create: (params: CreateParams) => Promise
; 23 | update: (params: UpdateParams) => Promise
; 24 | delete: () => Promise; 25 | 26 | verify: (params: VerifyParams) => Promise; 27 | resend: () => Promise; 28 | 29 | isCreatingAddress: boolean; 30 | isUpdatingAddress: boolean; 31 | isDeletingAddress: boolean; 32 | isSendingCode: boolean; 33 | isVerifyingCode: boolean; 34 | 35 | isFetching: boolean; 36 | errorFetching: DialectSdkError | null; 37 | } 38 | 39 | interface UseNotificationChannelParams { 40 | addressType: AddressType; 41 | refreshInterval?: number; 42 | } 43 | 44 | function useNotificationChannel({ 45 | addressType, 46 | refreshInterval, 47 | }: UseNotificationChannelParams): UseNotificationChannelValue { 48 | const { wallet: walletsApi } = useDialectSdk(); 49 | 50 | const [isCreatingAddress, setCreatingAddress] = useState(false); 51 | const [isUpdatingAddress, setUpdatingAddress] = useState(false); 52 | const [isDeletingAddress, setDeletingAddress] = useState(false); 53 | const [isVerifyingCode, setVerifyingCode] = useState(false); 54 | const [isSendingCode, setSendingCode] = useState(false); 55 | 56 | // Fetch all wallet addresses (it doesn't include a enabled prop) 57 | const { 58 | data: addresses = EMPTY_ARR, 59 | error: errorFetchingAddresses = null, 60 | mutate: mutateAddresses, 61 | } = useSWR( 62 | WALLET_ADDRESSES_CACHE_KEY_FN(walletsApi.address), 63 | () => walletsApi.addresses.findAll(), 64 | { 65 | refreshInterval, 66 | refreshWhenOffline: true, 67 | } 68 | ); 69 | 70 | const isFetchingAddresses = 71 | !errorFetchingAddresses && addresses === undefined; 72 | 73 | const address = addresses.find((it) => it.type === addressType); 74 | 75 | const errorFetching = errorFetchingAddresses; 76 | 77 | const createAddress = useCallback( 78 | async ({ value }: CreateParams) => { 79 | if (isCreatingAddress) { 80 | // Do not create address if it's already creating 81 | return null; 82 | } 83 | setCreatingAddress(true); 84 | try { 85 | // not sure if optimistic update needed here, cause: 86 | // - address creating is the action that user expect to take a while 87 | await walletsApi.addresses.create({ 88 | type: addressType, 89 | value, 90 | }); 91 | return ( 92 | (await mutateAddresses())?.find((it) => it.type === addressType) || 93 | null 94 | ); 95 | } finally { 96 | setCreatingAddress(false); 97 | } 98 | }, 99 | [addressType, isCreatingAddress, mutateAddresses, walletsApi] 100 | ); 101 | 102 | const updateAddress = useCallback( 103 | async ({ value }: UpdateParams) => { 104 | if (!address || isUpdatingAddress) { 105 | return null; 106 | } 107 | setUpdatingAddress(true); 108 | try { 109 | await walletsApi.addresses.update({ 110 | addressId: address.id, 111 | value, 112 | }); 113 | return ( 114 | (await mutateAddresses())?.find((it) => it.type === addressType) || 115 | null 116 | ); 117 | } finally { 118 | setUpdatingAddress(false); 119 | } 120 | }, 121 | [address, addressType, isUpdatingAddress, mutateAddresses, walletsApi] 122 | ); 123 | 124 | const deleteAddress = useCallback(async () => { 125 | if (!address || isDeletingAddress) { 126 | return; 127 | } 128 | setDeletingAddress(true); 129 | try { 130 | await walletsApi.addresses.delete({ addressId: address.id }); 131 | await mutateAddresses(); 132 | } finally { 133 | setDeletingAddress(false); 134 | } 135 | }, [address, isDeletingAddress, mutateAddresses, walletsApi]); 136 | 137 | const verifyCode = useCallback( 138 | async ({ code }: VerifyParams) => { 139 | if (!address || isVerifyingCode) { 140 | return; 141 | } 142 | setVerifyingCode(true); 143 | try { 144 | await walletsApi.addresses.verify({ 145 | addressId: address.id, 146 | code, 147 | }); 148 | await mutateAddresses(); 149 | } finally { 150 | setVerifyingCode(false); 151 | } 152 | }, 153 | [address, isVerifyingCode, mutateAddresses, walletsApi] 154 | ); 155 | 156 | const resendCode = useCallback(async () => { 157 | if (!address || isSendingCode) { 158 | return; 159 | } 160 | setSendingCode(true); 161 | try { 162 | await walletsApi.addresses.resendVerificationCode({ 163 | addressId: address.id, 164 | }); 165 | await mutateAddresses(); 166 | } finally { 167 | setSendingCode(false); 168 | } 169 | }, [address, isSendingCode, mutateAddresses, walletsApi]); 170 | 171 | return { 172 | globalAddress: address, 173 | 174 | create: createAddress, 175 | update: updateAddress, 176 | delete: deleteAddress, 177 | verify: verifyCode, 178 | resend: resendCode, 179 | 180 | isCreatingAddress, 181 | isUpdatingAddress, 182 | isDeletingAddress, 183 | isSendingCode, 184 | isVerifyingCode, 185 | 186 | isFetching: isFetchingAddresses, 187 | errorFetching, 188 | }; 189 | } 190 | 191 | export default useNotificationChannel; 192 | -------------------------------------------------------------------------------- /packages/react-sdk/src/hooks/useNotificationChannelDappSubscription.ts: -------------------------------------------------------------------------------- 1 | import type { AccountAddress, Address, AddressType } from '@dialectlabs/sdk'; 2 | import { useCallback, useState } from 'react'; 3 | import useSWR from 'swr'; 4 | import { EMPTY_ARR } from '../utils'; 5 | import { WALLET_DAPP_ADDRESSES_CACHE_KEY_FN } from './internal/swrCache'; 6 | import useDialectSdk from './useDialectSdk'; 7 | import useNotificationChannel from './useNotificationChannel'; 8 | 9 | interface UseNotificationChannelDappSubscriptionParams { 10 | addressType: AddressType; 11 | dappAddress: AccountAddress; 12 | } 13 | 14 | interface ToggleSubscriptionParams { 15 | enabled: boolean; 16 | address?: Address | null; // This is hack for telegram use case until it got refactored on backend 17 | } 18 | 19 | interface UseNotificationChannelDappSubscriptionValue { 20 | enabled: boolean; 21 | isToggling: boolean; 22 | isFetchingSubscriptions: boolean; 23 | toggleSubscription: (params: ToggleSubscriptionParams) => Promise; 24 | } 25 | 26 | const useNotificationChannelDappSubscription = ({ 27 | dappAddress, 28 | addressType, 29 | }: UseNotificationChannelDappSubscriptionParams): UseNotificationChannelDappSubscriptionValue => { 30 | const { wallet: walletsApi } = useDialectSdk(); 31 | const { globalAddress } = useNotificationChannel({ addressType }); 32 | 33 | const [isToggling, setIsToggling] = useState(false); 34 | 35 | const { 36 | data: dappSubscriptions = EMPTY_ARR, 37 | error: errorFetchingDappAddresses = null, 38 | mutate: mutateDappAddresses, 39 | } = useSWR( 40 | WALLET_DAPP_ADDRESSES_CACHE_KEY_FN(walletsApi.address, dappAddress), 41 | () => 42 | walletsApi.dappAddresses.findAll({ 43 | dappAccountAddress: dappAddress, 44 | }), 45 | ); 46 | 47 | const currentSubscription = globalAddress 48 | ? dappSubscriptions.find((it) => it.address.id === globalAddress.id) 49 | : null; 50 | 51 | const toggleSubscription = useCallback( 52 | async ({ enabled, address = globalAddress }: ToggleSubscriptionParams) => { 53 | if (!address || !dappAddress || isToggling) { 54 | return; 55 | } 56 | setIsToggling(true); 57 | try { 58 | await mutateDappAddresses( 59 | async () => { 60 | if (currentSubscription) { 61 | const updatedSub = await walletsApi.dappAddresses.update({ 62 | dappAddressId: currentSubscription.id, 63 | enabled, 64 | }); 65 | return dappSubscriptions.map((it) => 66 | it.id === updatedSub.id ? updatedSub : it, 67 | ); 68 | } else { 69 | const newSub = await walletsApi.dappAddresses.create({ 70 | addressId: address.id, 71 | dappAccountAddress: dappAddress, 72 | enabled, 73 | }); 74 | return [...dappSubscriptions, newSub]; 75 | } 76 | }, 77 | { 78 | optimisticData: dappSubscriptions.find( 79 | (it) => it.id === currentSubscription?.id, 80 | ) 81 | ? dappSubscriptions.map((it) => 82 | it.id === currentSubscription?.id ? { ...it, enabled } : it, 83 | ) 84 | : [ 85 | ...dappSubscriptions, 86 | { 87 | id: 'optimistic-dapp-subscription', 88 | enabled, 89 | address: address, 90 | dappId: dappAddress, 91 | }, 92 | ], 93 | rollbackOnError: true, 94 | }, 95 | ); 96 | } finally { 97 | setIsToggling(false); 98 | } 99 | }, 100 | [ 101 | currentSubscription, 102 | dappAddress, 103 | dappSubscriptions, 104 | globalAddress, 105 | isToggling, 106 | mutateDappAddresses, 107 | walletsApi.dappAddresses, 108 | ], 109 | ); 110 | 111 | const isFetchingSubscriptions = 112 | !errorFetchingDappAddresses && !dappSubscriptions; 113 | 114 | return { 115 | enabled: currentSubscription?.enabled || false, 116 | isToggling, 117 | isFetchingSubscriptions, 118 | toggleSubscription, 119 | }; 120 | }; 121 | 122 | export default useNotificationChannelDappSubscription; 123 | -------------------------------------------------------------------------------- /packages/react-sdk/src/hooks/useNotificationDapp.ts: -------------------------------------------------------------------------------- 1 | import type { Dapp, DialectSdkError } from '@dialectlabs/sdk'; 2 | import useSWR from 'swr'; 3 | import { useDialectContext } from '../context'; 4 | import { EMPTY_OBJ } from '../utils'; 5 | import { DAPP_CACHE_KEY_FN } from './internal/swrCache'; 6 | import useDialectSdk from './useDialectSdk'; 7 | 8 | interface UseDappValue { 9 | dapp: Dapp | null; 10 | isFetching: boolean; 11 | errorFetching: DialectSdkError | null; 12 | } 13 | 14 | interface UseDappParams { 15 | refreshInterval?: number; 16 | } 17 | 18 | function useNotificationDapp({ 19 | refreshInterval, 20 | }: UseDappParams = EMPTY_OBJ): UseDappValue { 21 | const { dappAddress } = useDialectContext(); 22 | const { dapps } = useDialectSdk(); 23 | 24 | const { data: dapp, error } = useSWR( 25 | DAPP_CACHE_KEY_FN(dappAddress), 26 | () => dapps.find({ address: dappAddress }), 27 | { refreshInterval, refreshWhenOffline: true }, 28 | ); 29 | 30 | return { 31 | dapp: dapp || null, 32 | isFetching: !error && dapp === undefined, 33 | errorFetching: error, 34 | }; 35 | } 36 | 37 | export default useNotificationDapp; 38 | -------------------------------------------------------------------------------- /packages/react-sdk/src/hooks/useNotificationSubscriptions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AccountAddress, 3 | DialectSdkError, 4 | UpsertNotificationSubscriptionCommand, 5 | WalletNotificationSubscription, 6 | } from '@dialectlabs/sdk'; 7 | import { useCallback, useState } from 'react'; 8 | import useSWR from 'swr'; 9 | import { EMPTY_ARR } from '../utils'; 10 | import { WALLET_NOTIFICATION_SUBSCRIPTIONS_CACHE_KEY_FN } from './internal/swrCache'; 11 | import useDialectSdk from './useDialectSdk'; 12 | 13 | interface UseNotificationSubscriptionsValue { 14 | subscriptions: WalletNotificationSubscription[]; 15 | 16 | update(params: UpdateNotificationSubscriptionCommand): Promise; 17 | 18 | isFetching: boolean; 19 | errorFetching: DialectSdkError | null; 20 | isUpdating: boolean; 21 | errorUpdating: DialectSdkError | null; 22 | } 23 | 24 | type UpdateNotificationSubscriptionCommand = 25 | UpsertNotificationSubscriptionCommand; 26 | 27 | interface UseUseNotificationSubscriptions { 28 | dappAddress: AccountAddress; 29 | refreshInterval?: number; 30 | } 31 | 32 | function useNotificationSubscriptions({ 33 | dappAddress, 34 | refreshInterval, 35 | }: UseUseNotificationSubscriptions): UseNotificationSubscriptionsValue { 36 | const { 37 | wallet: { address: walletAddress, notificationSubscriptions }, 38 | } = useDialectSdk(); 39 | 40 | const [isUpdating, setIsUpdating] = useState(false); 41 | const [errorUpdating, setErrorUpdating] = useState( 42 | null, 43 | ); 44 | 45 | const { 46 | data: subscriptions, 47 | error: errorFetching = null, 48 | mutate, 49 | } = useSWR( 50 | WALLET_NOTIFICATION_SUBSCRIPTIONS_CACHE_KEY_FN(walletAddress, dappAddress), 51 | notificationSubscriptions 52 | ? () => 53 | notificationSubscriptions.findAll({ dappAccountAddress: dappAddress }) 54 | : null, 55 | { 56 | refreshInterval, 57 | refreshWhenOffline: true, 58 | }, 59 | ); 60 | 61 | const update = useCallback( 62 | async (command: UpsertNotificationSubscriptionCommand) => { 63 | setIsUpdating(true); 64 | setErrorUpdating(null); 65 | try { 66 | await mutate( 67 | (prev) => { 68 | if (prev) { 69 | return prev.map((it) => 70 | it.notificationType.id === command.notificationTypeId 71 | ? { 72 | ...it, 73 | subscription: { 74 | ...it.subscription, 75 | config: command.config, 76 | }, 77 | } 78 | : it, 79 | ); 80 | } 81 | return prev; 82 | }, 83 | { revalidate: false }, // optimistic change first 84 | ); 85 | 86 | await notificationSubscriptions.upsert(command); 87 | await mutate(); 88 | } catch (e) { 89 | if (e instanceof DialectSdkError) { 90 | setErrorUpdating(e); 91 | } 92 | throw e; 93 | } finally { 94 | setIsUpdating(false); 95 | } 96 | }, 97 | [mutate, notificationSubscriptions], 98 | ); 99 | 100 | return { 101 | subscriptions: subscriptions || EMPTY_ARR, 102 | update, 103 | isFetching: subscriptions === undefined && !errorFetching, 104 | errorFetching, 105 | isUpdating, 106 | errorUpdating, 107 | }; 108 | } 109 | 110 | export default useNotificationSubscriptions; 111 | -------------------------------------------------------------------------------- /packages/react-sdk/src/hooks/useNotificationThread.ts: -------------------------------------------------------------------------------- 1 | import { CreateThreadCommand } from '@dialectlabs/sdk'; 2 | import useSWR from 'swr'; 3 | import useSWRMutation from 'swr/mutation'; 4 | import { useDialectContext } from '../context'; 5 | import { CACHE_KEY_THREAD_FN } from './internal/swrCache'; 6 | import useDialectSdk from './useDialectSdk'; 7 | 8 | interface UseNotificationThreadParams { 9 | refreshInterval?: number; 10 | } 11 | 12 | const useNotificationThread = ({ 13 | refreshInterval, 14 | }: UseNotificationThreadParams = {}) => { 15 | const { threads: threadsApi } = useDialectSdk(); 16 | const { dappAddress } = useDialectContext(); 17 | 18 | const { 19 | data: thread, 20 | isLoading, 21 | error, 22 | mutate, 23 | } = useSWR( 24 | CACHE_KEY_THREAD_FN({ otherMembers: [dappAddress] }), 25 | () => threadsApi.find({ otherMembers: [dappAddress] }), 26 | { 27 | refreshInterval, 28 | }, 29 | ); 30 | 31 | const { 32 | trigger: createThread, 33 | isMutating: isCreatingThread, 34 | error: errorCreatingThread, 35 | } = useSWRMutation( 36 | CACHE_KEY_THREAD_FN({ otherMembers: [dappAddress] }), 37 | (_, { arg }: { arg: CreateThreadCommand }) => threadsApi.create(arg), 38 | ); 39 | 40 | const { 41 | trigger: deleteThread, 42 | isMutating: isDeletingThread, 43 | error: errorDeletingThread, 44 | } = useSWRMutation( 45 | CACHE_KEY_THREAD_FN({ otherMembers: [dappAddress] }), 46 | async () => { 47 | await thread?.delete(); 48 | }, 49 | ); 50 | 51 | return { 52 | thread: thread ?? null, 53 | isThreadLoading: isLoading, 54 | errorLoadingThread: error, 55 | refreshThread: mutate, 56 | 57 | // todo: consider splitting into atomic hooks? 58 | create: createThread, 59 | isCreatingThread, 60 | errorCreatingThread, 61 | 62 | delete: deleteThread, 63 | isDeletingThread, 64 | errorDeletingThread, 65 | }; 66 | }; 67 | 68 | export default useNotificationThread; 69 | -------------------------------------------------------------------------------- /packages/react-sdk/src/hooks/useNotificationThreadMessages.ts: -------------------------------------------------------------------------------- 1 | import { SmartMessageStateDto, ThreadMessage } from '@dialectlabs/sdk'; 2 | import { useEffect, useState } from 'react'; 3 | import useSWR from 'swr'; 4 | import useSWRMutation from 'swr/mutation'; 5 | import { 6 | CACHE_KEY_MESSAGES_FN, 7 | CACHE_KEY_THREAD_SUMMARY_FN, 8 | } from './internal/swrCache'; 9 | import useNotificationThread from './useNotificationThread'; 10 | 11 | interface UseNotificationThreadMessagesParams { 12 | refreshInterval?: number; 13 | } 14 | 15 | const DEFAULT_INTERVAL = 10000; 16 | const FASTER_INTERVAL = 3000; 17 | 18 | const hasRunningAction = (message: ThreadMessage): boolean => { 19 | if (!message.metadata?.smartMessage?.content.state) { 20 | return false; 21 | } 22 | 23 | return [ 24 | SmartMessageStateDto.ReadyForExecution, 25 | SmartMessageStateDto.Executing, 26 | ].includes(message.metadata.smartMessage.content.state); 27 | }; 28 | 29 | const useNotificationThreadMessages = ( 30 | { 31 | refreshInterval: initialRefreshInterval = DEFAULT_INTERVAL, 32 | }: UseNotificationThreadMessagesParams = { 33 | refreshInterval: DEFAULT_INTERVAL, 34 | }, 35 | ) => { 36 | const [refreshInterval, setRefreshInterval] = useState( 37 | initialRefreshInterval, 38 | ); 39 | const { thread, isThreadLoading } = useNotificationThread(); 40 | 41 | const { 42 | data: messages, 43 | isLoading, 44 | error, 45 | mutate, 46 | } = useSWR( 47 | thread ? CACHE_KEY_MESSAGES_FN(thread.id.toString()) : null, 48 | () => thread?.messages() ?? [], 49 | { 50 | refreshInterval, 51 | }, 52 | ); 53 | 54 | const { trigger: markAsRead } = useSWRMutation( 55 | thread 56 | ? CACHE_KEY_THREAD_SUMMARY_FN( 57 | thread.otherMembers.map((member) => member.address), 58 | ) 59 | : null, 60 | async () => { 61 | if (!thread) { 62 | return; 63 | } 64 | 65 | await thread.markAsRead(); 66 | }, 67 | ); 68 | 69 | useEffect(() => { 70 | if (!messages) { 71 | return; 72 | } 73 | 74 | let executingAction = false; 75 | for (const message of messages) { 76 | if (hasRunningAction(message)) { 77 | executingAction = true; 78 | break; 79 | } 80 | } 81 | 82 | setRefreshInterval( 83 | executingAction ? FASTER_INTERVAL : initialRefreshInterval, 84 | ); 85 | }, [messages, initialRefreshInterval]); 86 | 87 | return { 88 | messages: messages ?? [], 89 | isMessagesLoading: isLoading || isThreadLoading, 90 | errorLoadingMessages: error, 91 | refreshMessages: mutate, 92 | markAsRead, 93 | }; 94 | }; 95 | 96 | export default useNotificationThreadMessages; 97 | -------------------------------------------------------------------------------- /packages/react-sdk/src/hooks/useThread.ts: -------------------------------------------------------------------------------- 1 | import { DialectSdkError, FindThreadQuery, Thread } from '@dialectlabs/sdk'; 2 | import { useCallback, useState } from 'react'; 3 | import useSWR, { useSWRConfig } from 'swr'; 4 | import { EMPTY_ARR } from '../utils'; 5 | import { isAdminable, isWritable } from '../utils/scopes'; 6 | import { CACHE_KEY_THREADS, CACHE_KEY_THREAD_FN } from './internal/swrCache'; 7 | import useDialectSdk from './useDialectSdk'; 8 | 9 | // TODO support multiple ways to resolve thread, eg. twitter, sns 10 | type ThreadSearchParams = FindThreadQuery; 11 | 12 | type UseThreadParams = { 13 | findParams: ThreadSearchParams; 14 | refreshInterval?: number; 15 | }; 16 | 17 | interface UseThreadValue { 18 | // sdk 19 | thread: Omit | null; 20 | 21 | delete(): Promise; 22 | 23 | // react-lib 24 | isFetchingThread: boolean; 25 | errorFetchingThread: DialectSdkError | null; 26 | isDeletingThread: boolean; 27 | errorDeletingThread: DialectSdkError | null; 28 | isWritable: boolean; 29 | isAdminable: boolean; 30 | } 31 | 32 | const useThread = ({ 33 | findParams, 34 | refreshInterval, 35 | }: UseThreadParams): UseThreadValue => { 36 | const { mutate: globalMutate } = useSWRConfig(); 37 | const { threads: threadsApi } = useDialectSdk(); 38 | 39 | const [isDeletingThread, setIsDeletingThread] = useState(false); 40 | const [errorDeletingThread, setErrorDeletingThread] = 41 | useState(null); 42 | 43 | const { 44 | data: thread, 45 | error: errorFetchingThread, 46 | mutate: mutateThread, 47 | } = useSWR( 48 | CACHE_KEY_THREAD_FN(findParams), 49 | () => threadsApi.find(findParams), 50 | { 51 | refreshInterval, 52 | refreshWhenOffline: true, 53 | } 54 | ); 55 | 56 | const deleteThread = useCallback(async () => { 57 | if (!thread) return; 58 | setIsDeletingThread(true); 59 | setErrorDeletingThread(null); 60 | try { 61 | const result = await thread.delete(); 62 | mutateThread(); 63 | globalMutate(CACHE_KEY_THREADS); 64 | return result; 65 | } catch (e) { 66 | if (e instanceof DialectSdkError) { 67 | setErrorDeletingThread(e); 68 | } 69 | throw e; 70 | } finally { 71 | setIsDeletingThread(false); 72 | } 73 | }, [globalMutate, mutateThread, thread]); 74 | 75 | return { 76 | // sdk 77 | thread: thread || null, 78 | delete: deleteThread, 79 | // react-lib 80 | isFetchingThread: thread === undefined && !errorFetchingThread, 81 | errorFetchingThread, 82 | isDeletingThread, 83 | errorDeletingThread, 84 | isWritable: isWritable(thread?.me.scopes || EMPTY_ARR), 85 | isAdminable: isAdminable(thread?.me.scopes || EMPTY_ARR), 86 | }; 87 | }; 88 | 89 | export default useThread; 90 | -------------------------------------------------------------------------------- /packages/react-sdk/src/hooks/useThreads.ts: -------------------------------------------------------------------------------- 1 | import { CreateThreadCommand, DialectSdkError, Thread } from '@dialectlabs/sdk'; 2 | import { useCallback, useEffect, useState } from 'react'; 3 | import useSWR, { useSWRConfig } from 'swr'; 4 | import { EMPTY_ARR, EMPTY_OBJ } from '../utils'; 5 | import { CACHE_KEY_THREADS, CACHE_KEY_THREAD_FN } from './internal/swrCache'; 6 | import useDialectSdk from './useDialectSdk'; 7 | 8 | interface UseThreadsParams { 9 | refreshInterval?: number; 10 | } 11 | 12 | interface UseThreadsValue { 13 | // sdk 14 | threads: Thread[]; 15 | create(command: CreateThreadCommand): Promise; 16 | // react 17 | isFetchingThreads: boolean; 18 | errorFetchingThreads: DialectSdkError | null; 19 | isCreatingThread: boolean; 20 | errorCreatingThread: DialectSdkError | null; 21 | } 22 | 23 | const useThreads = ({ 24 | refreshInterval, 25 | }: UseThreadsParams = EMPTY_OBJ): UseThreadsValue => { 26 | const { threads: threadsApi } = useDialectSdk(); 27 | const { mutate: globalMutate } = useSWRConfig(); 28 | 29 | const [isCreatingThread, setIsCreatingThread] = useState(false); 30 | const [errorCreatingThread, setErrorCreatingThread] = 31 | useState(null); 32 | 33 | const { 34 | data: threads, 35 | error: errorFetchingThreads, 36 | mutate, 37 | } = useSWR(CACHE_KEY_THREADS, () => threadsApi.findAll(), { 38 | refreshInterval, 39 | refreshWhenOffline: true, 40 | }); 41 | 42 | useEffect( 43 | function invalidateThreads() { 44 | mutate(); 45 | }, 46 | [mutate, threadsApi] 47 | ); 48 | 49 | // useDialectErrorsHandler(errorFetchingThreads, errorCreatingThread); 50 | 51 | const createThread = useCallback( 52 | async (cmd: CreateThreadCommand) => { 53 | setIsCreatingThread(true); 54 | setErrorCreatingThread(null); 55 | try { 56 | const res = await threadsApi.create(cmd); 57 | await mutate(); 58 | await globalMutate(CACHE_KEY_THREAD_FN({ id: res.id }), res); 59 | await globalMutate( 60 | CACHE_KEY_THREAD_FN({ 61 | otherMembers: cmd.otherMembers.map((it) => it.address), 62 | }), 63 | res 64 | ); 65 | return res; 66 | } catch (e) { 67 | if (e instanceof DialectSdkError) { 68 | setErrorCreatingThread(e); 69 | } 70 | throw e; 71 | } finally { 72 | setIsCreatingThread(false); 73 | } 74 | }, 75 | [threadsApi, mutate, globalMutate] 76 | ); 77 | 78 | return { 79 | threads: threads || EMPTY_ARR, 80 | create: createThread, 81 | 82 | // Do not use `isValidating` since it will produce visual flickering 83 | isFetchingThreads: threads == undefined && !errorFetchingThreads, 84 | errorFetchingThreads, 85 | isCreatingThread, 86 | errorCreatingThread, 87 | }; 88 | }; 89 | 90 | export default useThreads; 91 | -------------------------------------------------------------------------------- /packages/react-sdk/src/hooks/useUnreadNotifications.ts: -------------------------------------------------------------------------------- 1 | import type { ThreadSummary } from '@dialectlabs/sdk'; 2 | import useSWR, { KeyedMutator } from 'swr'; 3 | import { useDialectContext } from '../context'; 4 | import { EMPTY_OBJ } from '../utils'; 5 | import { CACHE_KEY_THREAD_SUMMARY_FN } from './internal/swrCache'; 6 | import useDialectSdk from './useDialectSdk'; 7 | 8 | interface UseUnreadNotificationsParams { 9 | refreshInterval?: number; 10 | revalidateOnMount?: boolean; 11 | revalidateOnFocus?: boolean; 12 | } 13 | 14 | interface UseUnreadMessageValue { 15 | hasNotificationsThread: boolean; 16 | unreadCount: number; 17 | refresh: () => void; 18 | hasUnreadMessages: boolean; 19 | } 20 | 21 | function useUnreadNotifications({ 22 | refreshInterval, 23 | revalidateOnMount = true, 24 | revalidateOnFocus = true, 25 | }: UseUnreadNotificationsParams = EMPTY_OBJ): UseUnreadMessageValue { 26 | const sdk = useDialectSdk(true); 27 | const { dappAddress } = useDialectContext(); 28 | 29 | const { data, mutate } = useSWR( 30 | CACHE_KEY_THREAD_SUMMARY_FN([dappAddress]), 31 | () => 32 | sdk?.threads.findSummary({ 33 | otherMembers: [dappAddress], 34 | }), 35 | { 36 | refreshInterval, 37 | refreshWhenOffline: true, 38 | revalidateOnMount, 39 | revalidateOnFocus, 40 | }, 41 | ); 42 | 43 | return { 44 | hasNotificationsThread: data !== null, 45 | hasUnreadMessages: Boolean(data?.me?.hasUnreadMessages), 46 | refresh: mutate as KeyedMutator, 47 | unreadCount: data?.me?.unreadMessagesCount ?? 0, 48 | }; 49 | } 50 | 51 | export default useUnreadNotifications; 52 | -------------------------------------------------------------------------------- /packages/react-sdk/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@dialectlabs/sdk'; 2 | export { version as REACT_SDK_VERSION } from '../package.json'; 3 | export * from './context'; 4 | export * from './hooks'; 5 | -------------------------------------------------------------------------------- /packages/react-sdk/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { DialectSdkError, ThreadMessage } from '@dialectlabs/sdk'; 2 | 3 | export interface LocalThreadMessage extends ThreadMessage { 4 | deduplicationId: string; 5 | isSending?: boolean; 6 | error?: DialectSdkError | null; 7 | } 8 | -------------------------------------------------------------------------------- /packages/react-sdk/src/utils/container.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const EMPTY: unique symbol = Symbol(); 4 | 5 | export interface ContainerProviderProps { 6 | initialState?: State; 7 | children: React.ReactNode; 8 | } 9 | 10 | export interface Container { 11 | Provider: React.ComponentType>; 12 | useContainer: () => Value; 13 | } 14 | 15 | export function createContainer( 16 | useHook: (initialState?: State) => Value 17 | ): Container { 18 | const Context = React.createContext(EMPTY); 19 | 20 | function Provider(props: ContainerProviderProps) { 21 | const value = useHook(props.initialState); 22 | return {props.children}; 23 | } 24 | 25 | function useContainer(): Value { 26 | const value = React.useContext(Context); 27 | if (value === EMPTY) { 28 | throw new Error('Component must be wrapped with '); 29 | } 30 | return value; 31 | } 32 | 33 | return { Provider, useContainer }; 34 | } 35 | 36 | export function useContainer( 37 | container: Container 38 | ): Value { 39 | return container.useContainer(); 40 | } 41 | -------------------------------------------------------------------------------- /packages/react-sdk/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export const EMPTY_OBJ = {}; 2 | export const EMPTY_ARR = []; 3 | // eslint-disable-next-line @typescript-eslint/no-empty-function 4 | export const NOOP = (): void => {}; 5 | -------------------------------------------------------------------------------- /packages/react-sdk/src/utils/scopes.ts: -------------------------------------------------------------------------------- 1 | import { ThreadMemberScope } from '@dialectlabs/sdk'; 2 | 3 | export function isWritable(scopes: ThreadMemberScope[]) { 4 | return scopes.includes(ThreadMemberScope.WRITE); 5 | } 6 | 7 | export function isAdminable(scopes: ThreadMemberScope[]) { 8 | return scopes.includes(ThreadMemberScope.ADMIN); 9 | } 10 | -------------------------------------------------------------------------------- /packages/react-sdk/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | splitting: false, 6 | sourcemap: true, 7 | clean: true, 8 | dts: true, 9 | format: ['cjs', 'esm'], 10 | target: ['esnext'], 11 | }); 12 | -------------------------------------------------------------------------------- /packages/react-ui/.npmignore: -------------------------------------------------------------------------------- 1 | yarn-error.log 2 | .storybook 3 | stories -------------------------------------------------------------------------------- /packages/react-ui/.prettierignore: -------------------------------------------------------------------------------- 1 | .storybook -------------------------------------------------------------------------------- /packages/react-ui/.storybook/main.js: -------------------------------------------------------------------------------- 1 | import { dirname, join } from 'path'; 2 | 3 | /** 4 | * This function is used to resolve the absolute path of a package. 5 | * It is needed in projects that use Yarn PnP or are set up within a monorepo. 6 | */ 7 | function getAbsolutePath(value) { 8 | return dirname(require.resolve(join(value, 'package.json'))); 9 | } 10 | 11 | /** @type { import('@storybook/react-vite').StorybookConfig } */ 12 | const config = { 13 | stories: ['../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)'], 14 | addons: [ 15 | getAbsolutePath('@storybook/addon-onboarding'), 16 | getAbsolutePath('@storybook/addon-links'), 17 | getAbsolutePath('@storybook/addon-essentials'), 18 | getAbsolutePath('@chromatic-com/storybook'), 19 | getAbsolutePath('@storybook/addon-interactions'), 20 | ], 21 | framework: { 22 | name: getAbsolutePath('@storybook/react-vite'), 23 | options: { 24 | builder: { 25 | viteConfigPath: './vite.config.js', 26 | }, 27 | }, 28 | }, 29 | docs: { 30 | autodocs: 'tag', 31 | }, 32 | }; 33 | export default config; 34 | -------------------------------------------------------------------------------- /packages/react-ui/.storybook/preview.jsx: -------------------------------------------------------------------------------- 1 | import '../src/index.css'; 2 | 3 | /** @type { import('@storybook/react').Preview } */ 4 | const preview = { 5 | parameters: { 6 | controls: { 7 | matchers: { 8 | color: /(background|color)$/i, 9 | date: /Date$/i, 10 | }, 11 | }, 12 | }, 13 | decorators: [ 14 | (Story) => ( 15 |
16 | 17 |
18 | ), 19 | ], 20 | }; 21 | 22 | export default preview; 23 | -------------------------------------------------------------------------------- /packages/react-ui/README.md: -------------------------------------------------------------------------------- 1 | # Dialect React UI 2 | 3 | Reusable React UI components to integrate web3 alerts. 4 | 5 | ## Prerequisites 6 | 7 | — [Inter](https://fonts.google.com/specimen/Inter) font imported. 8 | 9 | For example via `` tag 10 | 11 | ```html 12 | 13 | 14 | 18 | ``` 19 | 20 | — Import compiled styles into your app's entrypoint 21 | 22 | ```typescript 23 | import '@dialectlabs/react-ui/index.css'; 24 | ``` 25 | 26 | ## Development 27 | 28 | See root README. 29 | 30 | ## Build 31 | 32 | ```shell 33 | npm run build 34 | ``` 35 | 36 | This would generate a production version of `@dialectlabs/react-ui` 37 | -------------------------------------------------------------------------------- /packages/react-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dialectlabs/react-ui", 3 | "version": "2.2.0", 4 | "type": "module", 5 | "license": "Apache-2.0", 6 | "private": false, 7 | "sideEffects": false, 8 | "main": "dist/index.cjs", 9 | "module": "dist/index.js", 10 | "types": "dist/index.d.ts", 11 | "exports": { 12 | ".": { 13 | "import": "./dist/index.js", 14 | "require": "./dist/index.cjs" 15 | }, 16 | "./index.css": "./dist/index.css" 17 | }, 18 | "files": [ 19 | "dist" 20 | ], 21 | "scripts": { 22 | "build": "tsup-node", 23 | "dev": "tsup-node --watch", 24 | "storybook": "storybook dev -p 6006", 25 | "build-storybook": "storybook build" 26 | }, 27 | "devDependencies": { 28 | "@chromatic-com/storybook": "^1.2.25", 29 | "@storybook/addon-essentials": "^8.0.5", 30 | "@storybook/addon-interactions": "^8.0.5", 31 | "@storybook/addon-links": "^8.0.5", 32 | "@storybook/addon-onboarding": "^8.0.5", 33 | "@storybook/blocks": "^8.0.5", 34 | "@storybook/react": "^8.0.5", 35 | "@storybook/react-vite": "^8.0.5", 36 | "@storybook/test": "^8.0.5", 37 | "@types/react": "^18.2.73", 38 | "@vitejs/plugin-react": "^4.2.1", 39 | "autoprefixer": "^10.4.19", 40 | "postcss": "^8.4.38", 41 | "postcss-prefix-selector": "^1.16.0", 42 | "storybook": "^8.0.5", 43 | "tailwindcss": "^3.4.3", 44 | "tsup": "^8.0.2", 45 | "vite": "^5.2.6" 46 | }, 47 | "repository": { 48 | "type": "git", 49 | "url": "https://github.com/dialectlabs/react" 50 | }, 51 | "dependencies": { 52 | "@dialectlabs/react-sdk": "^2.1.0", 53 | "clsx": "^2.1.0", 54 | "react-linkify-it": "^1.0.8" 55 | }, 56 | "peerDependencies": { 57 | "@solana/wallet-adapter-react": "0.15.x", 58 | "react": ">=18" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/react-ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | let prefixOverrideList = ['html', 'body']; 2 | 3 | export default { 4 | plugins: { 5 | tailwindcss: {}, 6 | autoprefixer: {}, 7 | 'postcss-prefix-selector': { 8 | prefix: '.dialect', 9 | includeFiles: ['index.css'], 10 | transform: function (prefix, selector, prefixedSelector) { 11 | if (selector.startsWith('.dialect')) { 12 | return selector; 13 | } 14 | if (prefixOverrideList.includes(selector)) { 15 | return prefix; 16 | } else { 17 | return prefixedSelector; 18 | } 19 | }, 20 | }, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /packages/react-ui/prettier.config.mjs: -------------------------------------------------------------------------------- 1 | import repoConfig from '../../prettier.config.mjs'; 2 | /** @type {import("prettier").Config} */ 3 | const config = { 4 | ...repoConfig, 5 | tailwindConfig: "./tailwind.config.js" 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /packages/react-ui/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .dialect { 6 | font-family: inherit; 7 | } 8 | 9 | .dialect { 10 | --dt-border-radius-xs: 0.375rem; 11 | --dt-border-radius-s: 0.5rem; 12 | --dt-border-radius-m: 0.75rem; 13 | --dt-border-radius-l: 1rem; 14 | } 15 | 16 | .dialect { 17 | --dt-accent-brand: #09cbbf; 18 | --dt-accent-error: #f62d2d; 19 | --dt-accent-success: #09cbbf; 20 | --dt-bg-brand: #ebebeb; 21 | --dt-bg-primary: #ffffff; 22 | --dt-bg-secondary: #f9f9f9; 23 | --dt-bg-tertiary: #f2f3f5; 24 | --dt-brand-transparent: #b3b3b31a; 25 | --dt-button-primary: #2a2a2b; 26 | --dt-button-primary-disabled: #656564; 27 | --dt-button-primary-hover: #434445; 28 | --dt-button-secondary: #ebebeb; 29 | --dt-button-secondary-disabled: #f2f3f5; 30 | --dt-button-secondary-hover: #f2f3f5; 31 | --dt-error-transparent: #f62d2d1a; 32 | --dt-icon-primary: #2a2a2b; 33 | --dt-icon-secondary: #888989; 34 | --dt-icon-tertiary: #b3b3b3; 35 | --dt-input-checked: #09cbbf; 36 | --dt-input-primary: #dee1e7; 37 | --dt-input-secondary: #ffffff; 38 | --dt-input-unchecked: #d7d7d7; 39 | --dt-stroke-primary: #dee1e7; 40 | --dt-success-transparent: #09cbbf1a; 41 | --dt-text-accent: #08c0b4; 42 | --dt-text-inverse: #ffffff; 43 | --dt-text-primary: #232324; 44 | --dt-text-quaternary: #888989; 45 | --dt-text-secondary: #434445; 46 | --dt-text-tertiary: #737373; 47 | } 48 | 49 | .dialect[data-theme='dark'] { 50 | --dt-accent-brand: #09cbbf; 51 | --dt-accent-error: #ff4747; 52 | --dt-accent-success: #09cbbf; 53 | --dt-bg-brand: #656564; 54 | --dt-bg-primary: #1b1b1c; 55 | --dt-bg-secondary: #232324; 56 | --dt-bg-tertiary: #2a2a2b; 57 | --dt-brand-transparent: #b3b3b31a; 58 | --dt-button-primary: #ffffff; 59 | --dt-button-primary-disabled: #dee1e7; 60 | --dt-button-primary-hover: #f9f9f9; 61 | --dt-button-secondary: #323335; 62 | --dt-button-secondary-disabled: #434445; 63 | --dt-button-secondary-hover: #383a3c; 64 | --dt-error-transparent: #f62d2d1a; 65 | --dt-icon-primary: #ffffff; 66 | --dt-icon-secondary: #888989; 67 | --dt-icon-tertiary: #737373; 68 | --dt-input-checked: #09cbbf; 69 | --dt-input-primary: #434445; 70 | --dt-input-secondary: #1b1b1c; 71 | --dt-input-unchecked: #656564; 72 | --dt-stroke-primary: #323335; 73 | --dt-success-transparent: #09cbbf1a; 74 | --dt-text-accent: #09cbbf; 75 | --dt-text-inverse: #1b1b1c; 76 | --dt-text-primary: #ffffff; 77 | --dt-text-quaternary: #888989; 78 | --dt-text-secondary: #c4c6c8; 79 | --dt-text-tertiary: #888989; 80 | } 81 | 82 | .dialect .dt-modal { 83 | /* mobile */ 84 | @apply dt-fixed dt-right-0 dt-top-0 dt-z-[100] dt-h-full dt-w-full; 85 | /* non mobile */ 86 | @apply sm:dt-absolute sm:dt-top-16 sm:dt-h-[600px] sm:dt-w-[420px] sm:dt-rounded-[--dt-border-radius-l] ; 87 | /* styles */ 88 | @apply dt-overflow-hidden dt-border dt-border-[--dt-stroke-primary]; 89 | } 90 | -------------------------------------------------------------------------------- /packages/react-ui/src/index.ts: -------------------------------------------------------------------------------- 1 | export { version as REACT_UI_VERSION } from '../package.json'; 2 | export * from './types'; 3 | export * from './ui'; 4 | -------------------------------------------------------------------------------- /packages/react-ui/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | 3 | export type ChannelType = 'wallet' | 'email' | 'telegram'; 4 | 5 | export type ThemeType = 'light' | 'dark'; 6 | 7 | export type NotificationStyleToggleColor = { dark: string; light: string }; 8 | 9 | export interface NotificationStyle { 10 | Icon: ReactNode; 11 | iconColor?: string | NotificationStyleToggleColor; 12 | iconBackgroundColor?: string | NotificationStyleToggleColor; 13 | iconBackgroundBackdropColor?: string | NotificationStyleToggleColor; 14 | linkColor?: string | NotificationStyleToggleColor; 15 | actionGradientStartColor?: string | NotificationStyleToggleColor; 16 | } 17 | 18 | export interface NotificationStyleMap { 19 | [notificationTypeId: string]: NotificationStyle; 20 | } 21 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/Header.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import React from 'react'; 3 | import { ClassTokens, Icons } from '../theme'; 4 | import { IconButton } from './primitives'; 5 | 6 | interface HeaderProps { 7 | title: string; 8 | showBackButton: boolean; 9 | showSettingsButton: boolean; 10 | showCloseButton: boolean; 11 | onBackClick?: () => void; 12 | onSettingsClick?: () => void; 13 | onCloseClick?: () => void; 14 | } 15 | 16 | const BackButton: React.FC<{ onBackClick: HeaderProps['onBackClick'] }> = ({ 17 | onBackClick, 18 | }) => ( 19 | } 23 | /> 24 | ); 25 | 26 | const SettingsButton: React.FC<{ 27 | onSettingsClick: HeaderProps['onSettingsClick']; 28 | }> = ({ onSettingsClick }) => ( 29 | } 33 | /> 34 | ); 35 | 36 | const CloseButton: React.FC<{ 37 | onCloseClick: HeaderProps['onCloseClick']; 38 | }> = ({ onCloseClick }) => ( 39 | } 43 | /> 44 | ); 45 | 46 | export function Header({ 47 | title, 48 | showCloseButton = true, 49 | showSettingsButton = true, 50 | showBackButton = true, 51 | onSettingsClick, 52 | onBackClick, 53 | onCloseClick, 54 | }: HeaderProps) { 55 | const leftButtons = ( 56 | <>{showBackButton && } 57 | ); 58 | 59 | const rightButtons = ( 60 |
61 | {showSettingsButton && ( 62 | 63 | )} 64 | {showCloseButton && } 65 |
66 | ); 67 | 68 | return ( 69 |
75 |
76 | {leftButtons} 77 | 80 | {title} 81 | 82 |
83 | {rightButtons} 84 |
85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/icons/ArrowLeftIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | export const ArrowLeftIcon = (props: SVGProps) => ( 4 | 12 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/icons/ArrowRightIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | export const ArrowRightIcon = (props: SVGProps) => ( 4 | 12 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/icons/BellButtonIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | export const BellButtonIcon = (props: SVGProps) => ( 3 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/icons/BellButtonIconOutline.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | export const BellButtonIconOutline = (props: SVGProps) => ( 3 | 11 | 17 | 21 | 22 | ); 23 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/icons/BellIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | export const BellIcon = (props: SVGProps) => ( 3 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/icons/CheckIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | export const CheckIcon = (props: SVGProps) => ( 4 | 12 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/icons/CloseIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | export const CloseIcon = (props: SVGProps) => ( 4 | 12 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/icons/DialectLogo.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | export const DialectLogo = (props: SVGProps) => { 4 | return ( 5 | 13 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/icons/ExclamationIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | export const ExclamationIcon = (props: SVGProps) => ( 4 | 12 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/icons/LoaderIcon.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { SVGProps } from 'react'; 3 | 4 | export const LoaderIcon = (props: SVGProps) => ( 5 | 12 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/icons/ResendIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | export const ResendIcon = (props: SVGProps) => ( 4 | 12 | 16 | 20 | 21 | ); 22 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/icons/SettingsIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | export const SettingsIcon = (props: SVGProps) => ( 4 | 12 | 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/icons/SpinnerDots.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | 3 | const POINTS = [0, 45, 90, 135, 180, 225, 270, 315]; 4 | 5 | export const SpinnerDots = ({ 6 | width = 16, 7 | }: { 8 | width?: number; 9 | height?: number; 10 | }) => { 11 | const dots = useMemo( 12 | () => 13 | POINTS.map((point, index) => ( 14 | 23 | )), 24 | [], 25 | ); 26 | return ( 27 | <> 28 | 29 | 40 | {dots} 41 | 42 | 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/icons/TelegramIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | export const TelegramIcon = (props: SVGProps) => { 4 | return ( 5 | 13 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/icons/TrashIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | export const TrashIcon = (props: SVGProps) => ( 4 | 12 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/icons/WalletIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | export const WalletIcon = (props: SVGProps) => ( 4 | 12 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/icons/XmarkIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | export const XmarkIcon = (props: SVGProps) => ( 4 | 12 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/icons/index.ts: -------------------------------------------------------------------------------- 1 | export { ArrowLeftIcon } from './ArrowLeftIcon'; 2 | export { ArrowRightIcon } from './ArrowRightIcon'; 3 | export { BellButtonIcon } from './BellButtonIcon'; 4 | export { BellButtonIconOutline } from './BellButtonIconOutline'; 5 | export { BellIcon } from './BellIcon'; 6 | export { CheckIcon } from './CheckIcon'; 7 | export { CloseIcon } from './CloseIcon'; 8 | export { DialectLogo } from './DialectLogo'; 9 | export { ExclamationIcon } from './ExclamationIcon'; 10 | export { LoaderIcon } from './LoaderIcon'; 11 | export { ResendIcon } from './ResendIcon'; 12 | export { SettingsIcon } from './SettingsIcon'; 13 | export { SpinnerDots } from './SpinnerDots'; 14 | export { TelegramIcon } from './TelegramIcon'; 15 | export { TrashIcon } from './TrashIcon'; 16 | export { WalletIcon } from './WalletIcon'; 17 | export { XmarkIcon } from './XmarkIcon'; 18 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Header'; 2 | export * from './model'; 3 | export * from './primitives'; 4 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/model/api/smart-messages.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CreateSmartMessageTransactionCommandDto, 3 | SmartMessageTransactionDto, 4 | SubmitSmartMessageTransactionCommandDto, 5 | } from './smart-messages.types'; 6 | 7 | function createSmartMessageApi() { 8 | return { 9 | cancelSmartMessage: async ( 10 | dialectCloudUrl: string, 11 | smartMessageId: string, 12 | ) => { 13 | try { 14 | const res = await fetch( 15 | `${dialectCloudUrl}/api/v1/smart-messages/${smartMessageId}/cancel`, 16 | { 17 | method: 'POST', 18 | }, 19 | ); 20 | if (!res.ok) { 21 | console.warn(`Error cancelling smart message ${smartMessageId}`); 22 | return null; 23 | } 24 | return res.json(); 25 | } catch (e) { 26 | console.warn(`Error cancelling smart message ${smartMessageId}`, e); 27 | return null; 28 | } 29 | }, 30 | 31 | createSmartMessageTransaction: async ( 32 | dialectCloudUrl: string, 33 | smartMessageId: string, 34 | token: string, 35 | command: CreateSmartMessageTransactionCommandDto, 36 | ): Promise => { 37 | try { 38 | const res = await fetch( 39 | `${dialectCloudUrl}/api/v1/smart-messages/${smartMessageId}/create-transaction`, 40 | { 41 | method: 'POST', 42 | body: JSON.stringify(command), 43 | headers: { 44 | 'Content-Type': 'application/json', 45 | Authorization: `Bearer ${token}`, 46 | }, 47 | }, 48 | ); 49 | if (!res.ok) { 50 | console.warn( 51 | `Error creating transaction for smart message ${smartMessageId}`, 52 | ); 53 | return null; 54 | } 55 | return res.json(); 56 | } catch (e) { 57 | console.warn(`Error creating transaction for ${smartMessageId}`, e); 58 | return null; 59 | } 60 | }, 61 | 62 | submitSmartMessageTransaction: async ( 63 | dialectCloudUrl: string, 64 | smartMessageId: string, 65 | token: string, 66 | command: SubmitSmartMessageTransactionCommandDto, 67 | ) => { 68 | try { 69 | const res = await fetch( 70 | `${dialectCloudUrl}/api/v1/smart-messages/${smartMessageId}/submit-transaction`, 71 | { 72 | method: 'POST', 73 | body: JSON.stringify(command), 74 | headers: { 75 | 'Content-Type': 'application/json', 76 | Authorization: `Bearer ${token}`, 77 | }, 78 | }, 79 | ); 80 | 81 | if (!res.ok) { 82 | console.warn( 83 | `Error submitting transaction for smart message ${smartMessageId}`, 84 | ); 85 | return null; 86 | } 87 | return res.json(); 88 | } catch (e) { 89 | console.warn(`Error submitting transaction for ${smartMessageId}`, e); 90 | return null; 91 | } 92 | }, 93 | }; 94 | } 95 | 96 | export const smartMessageApi = createSmartMessageApi(); 97 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/model/api/smart-messages.types.ts: -------------------------------------------------------------------------------- 1 | // spec 2 | 3 | export enum SmartMessageStateDto { 4 | Created = 'CREATED', 5 | ReadyForExecution = 'READY_FOR_EXECUTION', 6 | Executing = 'EXECUTING', 7 | Succeeded = 'SUCCEEDED', 8 | Failed = 'FAILED', 9 | Canceled = 'CANCELED', 10 | } 11 | 12 | export enum ActionType { 13 | SignTransaction = 'SIGN_TRANSACTION', 14 | OpenLink = 'OPEN_LINK', 15 | Cancel = 'CANCEL', // cancel without a transaction, in other words a noop 16 | } 17 | 18 | export class SmartMessageButtonLayoutElementDto { 19 | type!: 'button'; 20 | text!: string; 21 | action!: SmartMessageSpecActionDto; 22 | } 23 | 24 | export class SmartMessageLabelLayoutElementDto { 25 | type!: 'label'; 26 | text!: string; 27 | } 28 | 29 | export class SmartMessageSpecOpenLinkActionDto { 30 | type!: ActionType.OpenLink; 31 | link!: string; 32 | } 33 | 34 | export class SmartMessageSpecSignTransactionActionDto { 35 | humanReadableId!: string; 36 | type!: ActionType.SignTransaction; 37 | } 38 | 39 | export class SmartMessageSpecCancelActionDto { 40 | humanReadableId!: string; 41 | type!: ActionType.Cancel; 42 | } 43 | 44 | export type SmartMessageSpecActionDto = 45 | | SmartMessageSpecOpenLinkActionDto 46 | | SmartMessageSpecSignTransactionActionDto 47 | | SmartMessageSpecCancelActionDto; 48 | 49 | export type SmartMessageLayoutElementDto = 50 | | SmartMessageButtonLayoutElementDto 51 | | SmartMessageLabelLayoutElementDto; 52 | 53 | export class SmartMessageLayoutDto { 54 | icon!: string | null; // TODO: this doesn't match smart message spec, need to propagate this change into the spec 55 | description!: string | null; 56 | header!: string | null; 57 | subheader!: string | null; 58 | elements!: SmartMessageLayoutElementDto[][]; 59 | } 60 | 61 | export class SmartMessageContentDto { 62 | state!: SmartMessageStateDto; 63 | layout!: SmartMessageLayoutDto; 64 | } 65 | 66 | export class SmartMessagePreviewParamsDto { 67 | state!: SmartMessageStateDto; 68 | } 69 | 70 | export class SmartMessageSystemParamsDto { 71 | state!: SmartMessageStateDto; 72 | workflowStateHumanReadableId!: string; 73 | createdByWalletAddress!: string; 74 | principalWalletAddress!: string; 75 | updatedByWalletAddress!: string; 76 | } 77 | 78 | export class SmartMessageTransactionDto { 79 | transaction!: string; 80 | message?: string; 81 | } 82 | 83 | // rest api 84 | 85 | export interface CreateSmartMessageTransactionCommandDto { 86 | actionHumanReadableId: string; 87 | } 88 | 89 | export interface SubmitSmartMessageTransactionCommandDto { 90 | transaction: string; 91 | actionHumanReadableId: string; 92 | } 93 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/model/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useSmartMessage'; 2 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/model/useSmartMessage.ts: -------------------------------------------------------------------------------- 1 | import { 2 | useDialectSdk, 3 | useNotificationThreadMessages, 4 | } from '@dialectlabs/react-sdk'; 5 | import { useWallet } from '@solana/wallet-adapter-react'; 6 | import { VersionedTransaction } from '@solana/web3.js'; 7 | import { Buffer } from 'buffer'; 8 | import { useCallback, useState } from 'react'; 9 | import { smartMessageApi } from './api/smart-messages'; 10 | 11 | export interface UseSmartMessageValue { 12 | handleSmartMessageAction: ( 13 | smartMessageId: string, 14 | actionHumanReadableId: string, 15 | ) => Promise; 16 | handleSmartMessageCancel: (smartMessageId: string) => Promise; 17 | isInitiatingSmartMessage: boolean; 18 | isCancellingSmartMessage: boolean; 19 | } 20 | 21 | export const useSmartMessage = (): UseSmartMessageValue => { 22 | const dialectSdk = useDialectSdk(); 23 | const wallet = useWallet(); 24 | const dialectCloudUrl = dialectSdk.config.dialectCloud.url; 25 | 26 | const { refreshMessages } = useNotificationThreadMessages(); 27 | 28 | const [isInitiating, setIsInitiating] = useState(false); 29 | const [isCancelling, setIsCancelling] = useState(false); 30 | 31 | const handleAction = useCallback( 32 | async (smartMessageId: string, actionHumanReadableId: string) => { 33 | try { 34 | if (!wallet.signTransaction) { 35 | return; 36 | } 37 | 38 | setIsInitiating(true); 39 | 40 | const token = await dialectSdk.tokenProvider.get(); 41 | const txResponse = await smartMessageApi.createSmartMessageTransaction( 42 | dialectCloudUrl, 43 | smartMessageId, 44 | token.rawValue, 45 | { actionHumanReadableId }, 46 | ); 47 | 48 | if (!txResponse) { 49 | return; 50 | } 51 | const versionedTransaction = VersionedTransaction.deserialize( 52 | Buffer.from(txResponse.transaction, 'base64'), 53 | ); 54 | const signed = await wallet.signTransaction(versionedTransaction); 55 | await smartMessageApi.submitSmartMessageTransaction( 56 | dialectCloudUrl, 57 | smartMessageId, 58 | token.rawValue, 59 | { 60 | actionHumanReadableId, 61 | transaction: Buffer.from(signed.serialize()).toString('base64'), 62 | }, 63 | ); 64 | 65 | await refreshMessages(); 66 | } finally { 67 | setIsInitiating(false); 68 | } 69 | }, 70 | [dialectCloudUrl, dialectSdk.tokenProvider, wallet, refreshMessages], 71 | ); 72 | 73 | const handleCancel = useCallback( 74 | async (smartMessageId: string) => { 75 | try { 76 | if (!wallet.signTransaction) { 77 | return; 78 | } 79 | 80 | setIsCancelling(true); 81 | 82 | await smartMessageApi.cancelSmartMessage( 83 | dialectCloudUrl, 84 | smartMessageId, 85 | ); 86 | } finally { 87 | setIsCancelling(false); 88 | } 89 | }, 90 | [dialectCloudUrl, wallet.signTransaction], 91 | ); 92 | 93 | return { 94 | handleSmartMessageAction: handleAction, 95 | handleSmartMessageCancel: handleCancel, 96 | isInitiatingSmartMessage: isInitiating, 97 | isCancellingSmartMessage: isCancelling, 98 | }; 99 | }; 100 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/primitives/Badge.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { ReactNode } from 'react'; 3 | import { ClassTokens } from '../../theme'; 4 | 5 | export type BadgeVariant = 'default' | 'success' | 'error'; 6 | 7 | export interface BadgeProps { 8 | children: ReactNode | ReactNode[]; 9 | variant?: BadgeVariant; 10 | className?: string; 11 | } 12 | 13 | const variantClassMap: Record = { 14 | default: clsx( 15 | ClassTokens.Text.Tertiary, 16 | ClassTokens.Background.BrandTransparent, 17 | ), 18 | success: clsx( 19 | ClassTokens.Text.Success, 20 | ClassTokens.Background.SuccessTransparent, 21 | ), 22 | error: clsx(ClassTokens.Text.Error, ClassTokens.Background.ErrorTransparent), 23 | }; 24 | 25 | export const Badge = ({ 26 | variant = 'default', 27 | children, 28 | className, 29 | }: BadgeProps) => { 30 | return ( 31 | 38 | {children} 39 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/primitives/Button.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import React from 'react'; 3 | import { ClassTokens, Icons } from '../../theme'; 4 | 5 | export enum ButtonType { 6 | Primary = 'Primary', 7 | Secondary = 'Secondary', 8 | Destructive = 'Destructive', 9 | } 10 | export interface ButtonProps { 11 | children: React.ReactNode; 12 | onClick?: () => void; 13 | disabled?: boolean; 14 | loading?: boolean; 15 | type?: ButtonType; 16 | size?: 'medium' | 'large'; 17 | stretch?: boolean; 18 | } 19 | 20 | export const Button = ({ 21 | type = ButtonType.Secondary, 22 | size = 'medium', 23 | stretch = false, 24 | ...props 25 | }: ButtonProps): JSX.Element => { 26 | const backgroundTokens = ClassTokens.Background.Button[type]; 27 | const textTokens = ClassTokens.Text.Button[type]; 28 | const styles = 29 | size === 'large' 30 | ? 'dt-px-6 dt-py-4 dt-text-text dt-font-semibold ' + 31 | ClassTokens.Radius.Medium 32 | : 'dt-px-2.5 dt-py-1.5 dt-text-subtext dt-font-semibold ' + 33 | ClassTokens.Radius.XSmall; 34 | return ( 35 | 58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/primitives/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { ClassTokens } from '../../theme'; 3 | import { CheckIcon } from '../icons'; 4 | 5 | export interface CheckboxProps { 6 | children?: string; 7 | checked?: boolean; 8 | disabled?: boolean; 9 | onChange?: (newValue: boolean) => void; 10 | } 11 | 12 | export const Checkbox = ({ 13 | children, 14 | checked = false, 15 | disabled = false, 16 | onChange, 17 | }: CheckboxProps) => { 18 | const onClick = () => { 19 | if (!disabled) { 20 | onChange?.(!checked); 21 | } 22 | }; 23 | 24 | return ( 25 | 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/primitives/IconButton.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { JSX } from 'react'; 3 | 4 | interface Props { 5 | icon: JSX.Element; 6 | className: string; 7 | onClick?: () => void; 8 | } 9 | export const IconButton = ({ icon, onClick, className }: Props) => { 10 | return ( 11 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/primitives/Input.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { 3 | DetailedHTMLProps, 4 | InputHTMLAttributes, 5 | ReactNode, 6 | useMemo, 7 | } from 'react'; 8 | import { generateIdRandom } from '../../../utils'; 9 | import { ClassTokens } from '../../theme'; 10 | 11 | export interface InputProps 12 | extends DetailedHTMLProps< 13 | InputHTMLAttributes, 14 | HTMLInputElement 15 | > { 16 | label?: string; 17 | rightAdornment?: ReactNode; 18 | error?: boolean; 19 | } 20 | 21 | export const Input = ({ 22 | id, 23 | label, 24 | rightAdornment, 25 | error = false, 26 | ...inputProps 27 | }: InputProps) => { 28 | const inputId = useMemo(() => id || `dt-input-${generateIdRandom()}`, [id]); 29 | 30 | return ( 31 |
32 | {label && ( 33 | 42 | )} 43 |
55 | 65 | {rightAdornment && ( 66 |
{rightAdornment}
67 | )} 68 |
69 |
70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/primitives/Link.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { AnchorHTMLAttributes, DetailedHTMLProps, forwardRef } from 'react'; 3 | import { ClassTokens } from '../../theme'; 4 | 5 | export interface LinkProps 6 | extends DetailedHTMLProps< 7 | AnchorHTMLAttributes, 8 | HTMLAnchorElement 9 | > { 10 | url: string; 11 | } 12 | 13 | export const Link = forwardRef(function Link( 14 | { url, children, className, ...props }, 15 | ref, 16 | ) { 17 | return ( 18 | 24 | {children} 25 | 26 | ); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/primitives/Switch.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { ClassTokens } from '../../theme'; 3 | 4 | export interface SwitchProps { 5 | children?: string; 6 | checked: boolean; 7 | onChange?: (newValue: boolean) => void; 8 | } 9 | 10 | export const Switch = ({ 11 | children, 12 | checked = false, 13 | onChange, 14 | }: SwitchProps) => { 15 | return ( 16 | 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/primitives/TextButton.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import React, { JSX } from 'react'; 3 | import { ClassTokens } from '../../theme'; 4 | 5 | export interface TextButtonProps { 6 | children: React.ReactNode; 7 | onClick?: () => void; 8 | disabled?: boolean; 9 | color?: string; 10 | } 11 | 12 | export const TextButton = ({ 13 | onClick, 14 | disabled, 15 | children, 16 | color, 17 | }: TextButtonProps): JSX.Element => { 18 | return ( 19 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/primitives/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Badge'; 2 | export * from './Button'; 3 | export * from './Checkbox'; 4 | export * from './IconButton'; 5 | export * from './Input'; 6 | export * from './Link'; 7 | export * from './Switch'; 8 | export * from './TextButton'; 9 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/wallet-state/NoWalletState.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { ClassTokens, Icons } from '../../theme'; 3 | 4 | interface NoWalletErrorProps { 5 | message?: string | JSX.Element; 6 | } 7 | 8 | const NoWalletState = ({ 9 | message = 'Wallet not connected', 10 | }: NoWalletErrorProps) => { 11 | return ( 12 |
13 |
14 | 15 |
16 | 17 |

23 | {message} 24 |

25 |
26 | ); 27 | }; 28 | 29 | export default NoWalletState; 30 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/wallet-state/NotAuthorizedState.tsx: -------------------------------------------------------------------------------- 1 | import { useDialectWallet } from '@dialectlabs/react-sdk'; 2 | import clsx from 'clsx'; 3 | import { ClassTokens } from '../../theme'; 4 | import { Button, ButtonType, Switch } from '../primitives'; 5 | 6 | const NotAuthorizedState = () => { 7 | const { 8 | hardwareWalletForcedState: { 9 | get: isHardwareWalletForced, 10 | set: setHardwareWalletForced, 11 | }, 12 | connectionInitiatedState: { set: setConnectionInitiated }, 13 | } = useDialectWallet(); 14 | 15 | return ( 16 |
17 |

23 | Verify Wallet 24 |

25 | 31 | To continue, please prove you own this wallet by signing a{' '} 32 | {isHardwareWalletForced ? 'transaction' : 'message'}. It is free and 33 | does not involve the network. 34 | 35 |
36 |
44 | Using ledger? 45 | setHardwareWalletForced(next)} 48 | /> 49 |
50 | 58 |
59 |
60 | ); 61 | }; 62 | 63 | export default NotAuthorizedState; 64 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/wallet-state/SigningMessageState.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { ClassTokens, Icons } from '../../theme'; 3 | 4 | const SigningMessageState = () => { 5 | return ( 6 |
7 |
8 | 9 |
10 | 11 |

17 | Waiting for your wallet 18 |

19 |

25 | To continue please prove you own a wallet by approving signing request. 26 | It is free and does not involve the network. 27 |

28 |
29 | ); 30 | }; 31 | 32 | export default SigningMessageState; 33 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/wallet-state/SigningTransactionState.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { ClassTokens, Icons } from '../../theme'; 3 | 4 | const SigningTransactionState = () => { 5 | return ( 6 |
7 |
8 | 9 |
10 | 11 |

17 | Waiting for your wallet 18 |

19 |

25 | To continue please prove you own this wallet by signing a transaction. 26 | This transaction will not be submitted to the blockchain, and is 27 | free. 28 |

29 |
30 | ); 31 | }; 32 | 33 | export default SigningTransactionState; 34 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/core/wallet-state/WalletStatesWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { useDialectSdk, useDialectWallet } from '@dialectlabs/react-sdk'; 2 | import React from 'react'; 3 | 4 | import NoWalletState from './NoWalletState'; 5 | import NotAuthorizedState from './NotAuthorizedState'; 6 | import SigningMessageState from './SigningMessageState'; 7 | import SigningTransactionState from './SigningTransactionState'; 8 | 9 | // Only renders children if wallet is connected, access token and encryption keys are created 10 | 11 | interface WalletStatesWrapperProps { 12 | notConnectedMessage?: string | JSX.Element; 13 | header?: JSX.Element | null; 14 | children: React.ReactNode; 15 | } 16 | 17 | function WalletStatesWrapper({ 18 | header = null, 19 | notConnectedMessage, 20 | children, 21 | }: WalletStatesWrapperProps) { 22 | const sdk = useDialectSdk(true); 23 | 24 | const { 25 | walletConnected: { get: isWalletConnected }, 26 | connectionInitiatedState: { get: isConnectionInitiated }, 27 | isSigningMessageState: { get: isSigningMessage }, 28 | isSigningFreeTransactionState: { get: isSigningFreeTransaction }, 29 | } = useDialectWallet(); 30 | 31 | if (!isWalletConnected || (!sdk && isConnectionInitiated)) { 32 | return ( 33 | <> 34 | {header} 35 | 36 | 37 | ); 38 | } 39 | 40 | if (!isConnectionInitiated) { 41 | return ( 42 | <> 43 | {header} 44 | 45 | 46 | ); 47 | } 48 | 49 | if (isSigningMessage) { 50 | return ( 51 | <> 52 | {header} 53 | 54 | 55 | ); 56 | } 57 | 58 | if (isSigningFreeTransaction) { 59 | return ( 60 | <> 61 | {header} 62 | 63 | 64 | ); 65 | } 66 | 67 | return children; 68 | } 69 | 70 | export default WalletStatesWrapper; 71 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/index.ts: -------------------------------------------------------------------------------- 1 | export * from './notifications'; 2 | export { Icons, NotificationTypeStyles } from './theme'; 3 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/Notifications.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import React, { memo, useMemo } from 'react'; 3 | import { ChannelType, ThemeType } from '../../types'; 4 | import { Header } from '../core'; 5 | import WalletStatesWrapper from '../core/wallet-state/WalletStatesWrapper'; 6 | import { ClassTokens } from '../theme'; 7 | import { NotificationsFeedScreen } from './NotificationsFeed'; 8 | import { SettingsScreen } from './Settings'; 9 | import { ExternalPropsProvider } from './internal/ExternalPropsProvider'; 10 | import { Route, Router } from './internal/Router'; 11 | 12 | const DEFAULT_CHANNELS: ChannelType[] = ['wallet', 'telegram', 'email']; 13 | 14 | export interface NotificationsProps { 15 | channels?: ChannelType[]; 16 | open?: boolean; 17 | setOpen?: (open: boolean | ((prev: boolean) => boolean)) => void; 18 | theme?: ThemeType; 19 | renderAdditionalSettingsUi?: (args: Record) => React.ReactNode; 20 | } 21 | 22 | export const NotificationsBase = ( 23 | { 24 | channels = DEFAULT_CHANNELS, 25 | open, 26 | setOpen, 27 | theme, 28 | renderAdditionalSettingsUi, 29 | }: NotificationsProps = { 30 | channels: DEFAULT_CHANNELS, 31 | }, 32 | ) => { 33 | const normalizedExtProps = useMemo( 34 | () => ({ 35 | open, 36 | setOpen, 37 | theme, 38 | channels: Array.from(new Set(channels)), 39 | }), 40 | [open, setOpen, theme, channels], 41 | ); 42 | 43 | return ( 44 | 45 |
51 | setOpen?.(false)} 59 | /> 60 | } 61 | > 62 | 63 | {(route) => ( 64 | <> 65 | {route === Route.Settings && ( 66 | 69 | )} 70 | {route === Route.Notifications && } 71 | 72 | )} 73 | 74 | 75 |
76 |
77 | ); 78 | }; 79 | 80 | export const Notifications = memo(function Notifications( 81 | props: NotificationsProps, 82 | ) { 83 | return ( 84 |
85 | 86 |
87 | ); 88 | }); 89 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/NotificationsButton.tsx: -------------------------------------------------------------------------------- 1 | import { useUnreadNotifications } from '@dialectlabs/react-sdk'; 2 | import clsx from 'clsx'; 3 | import React, { 4 | PropsWithChildren, 5 | ReactNode, 6 | RefObject, 7 | forwardRef, 8 | memo, 9 | useCallback, 10 | useEffect, 11 | useMemo, 12 | useRef, 13 | useState, 14 | } from 'react'; 15 | import { ChannelType, ThemeType } from '../../types'; 16 | import { ClassTokens, Icons } from '../theme'; 17 | import { NotificationsBase } from './Notifications'; 18 | import { useClickAway } from './internal/useClickAway'; 19 | 20 | const Modal = forwardRef< 21 | HTMLDivElement, 22 | { 23 | open: boolean; 24 | setOpen: (open: boolean | ((prev: boolean) => boolean)) => void; 25 | channels?: ChannelType[]; 26 | theme?: ThemeType; 27 | renderAdditionalSettingsUi?: ( 28 | args: Record, 29 | ) => React.ReactNode; 30 | } 31 | >(function Modal(props, modalRef) { 32 | if (!props.open) { 33 | return null; 34 | } 35 | return ( 36 |
37 | 38 |
39 | ); 40 | }); 41 | 42 | const DefaultNotificationIconButton = forwardRef< 43 | HTMLButtonElement, 44 | { 45 | open: boolean; 46 | onClick: () => void; 47 | unread?: boolean; 48 | } 49 | >(function DefaultNotificationIconButton({ open, onClick, unread }, ref) { 50 | return ( 51 | 73 | ); 74 | }); 75 | 76 | const NotificationsButtonPresentation = ({ 77 | theme, 78 | children, 79 | }: PropsWithChildren<{ theme?: ThemeType }>) => { 80 | return ( 81 |
82 |
83 | {children} 84 |
85 |
86 | ); 87 | }; 88 | 89 | interface NotificationsButtonProps { 90 | children?: (args: { 91 | open: boolean; 92 | setOpen: (open: boolean | ((prev: boolean) => boolean)) => void; 93 | unreadCount: number; 94 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 95 | ref: RefObject; 96 | }) => ReactNode | ReactNode[]; 97 | renderModalComponent?: (args: { 98 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 99 | ref: RefObject; 100 | open: boolean; 101 | setOpen: (open: boolean | ((prev: boolean) => boolean)) => void; 102 | children: ReactNode; 103 | }) => ReactNode; 104 | // tmp empty object for now 105 | renderAdditionalSettingsUi?: (args: Record) => React.ReactNode; 106 | channels?: ChannelType[]; 107 | theme?: ThemeType; 108 | } 109 | const DEFAULT_INTERVAL = 10000; 110 | 111 | NotificationsButtonPresentation.Container = 112 | function NotificationsButtonContainer({ 113 | channels, 114 | renderModalComponent, 115 | children, 116 | theme, 117 | renderAdditionalSettingsUi, 118 | }: NotificationsButtonProps) { 119 | const buttonRef = useRef(null); 120 | const modalRef = useRef(null); 121 | 122 | const [open, setOpen] = useState(false); 123 | 124 | const [refreshInterval, setRefreshInterval] = useState(DEFAULT_INTERVAL); 125 | const { unreadCount, hasNotificationsThread } = useUnreadNotifications({ 126 | refreshInterval, 127 | revalidateOnFocus: Boolean(refreshInterval), 128 | }); 129 | 130 | useEffect(() => { 131 | if (open || !hasNotificationsThread) { 132 | setRefreshInterval(0); 133 | } else { 134 | setRefreshInterval(DEFAULT_INTERVAL); 135 | } 136 | }, [hasNotificationsThread, open]); 137 | 138 | useClickAway([buttonRef, modalRef], () => setOpen(false)); 139 | 140 | const externalProps = useMemo( 141 | () => ({ open, setOpen, channels, theme, renderAdditionalSettingsUi }), 142 | [open, channels, theme, renderAdditionalSettingsUi], 143 | ); 144 | const toggle = useCallback(() => setOpen((prev) => !prev), []); 145 | 146 | return ( 147 | 148 | {/* Button Render */} 149 | {children ? ( 150 | children({ open, setOpen, unreadCount, ref: buttonRef }) 151 | ) : ( 152 | 0} 157 | /> 158 | )} 159 | {/* Modal Render */} 160 | {renderModalComponent ? ( 161 | renderModalComponent({ 162 | open, 163 | setOpen, 164 | ref: modalRef, 165 | children: , // `children` MUST BE USED 166 | }) 167 | ) : ( 168 | 169 | )} 170 | 171 | ); 172 | }; 173 | 174 | export const NotificationsButton = memo( 175 | NotificationsButtonPresentation.Container, 176 | ); 177 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/NotificationsFeed/NoNotifications.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { Button, ButtonType } from '../../core'; 3 | import { ClassTokens, Icons } from '../../theme'; 4 | import { Route, useRouter } from '../internal/Router'; 5 | 6 | export const NoNotifications = () => { 7 | const router = useRouter(); 8 | 9 | return ( 10 |
11 |
12 | 13 |
14 | 15 |

21 | You don’t have any notifications yet 22 |

23 | 24 |

30 | Enable your wallet to receive notifications. 31 |

32 | 33 | 41 |
42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/NotificationsFeed/NotificationMessage/ButtonAction.tsx: -------------------------------------------------------------------------------- 1 | import { Button, ButtonType } from '../../../core'; 2 | 3 | interface Props { 4 | disabled?: boolean; 5 | loading?: boolean; 6 | label?: string; 7 | onClick?: () => void; 8 | } 9 | 10 | export const ButtonAction = ({ onClick, disabled, label, loading }: Props) => { 11 | return ( 12 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/NotificationsFeed/NotificationMessage/LinkAction.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { NotificationStyle } from '../../../../types'; 3 | import { TextButton } from '../../../core'; 4 | import { Icons } from '../../../theme'; 5 | import { getColor, getMessageURLTarget } from './utils'; 6 | 7 | interface LinkAction { 8 | styles: NotificationStyle; 9 | url: string; 10 | label?: string; 11 | } 12 | 13 | export const LinkAction = ({ 14 | url, 15 | label = 'Open Link', 16 | styles, 17 | }: LinkAction) => { 18 | return ( 19 | 27 | 28 | {label} 29 | 30 | 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/NotificationsFeed/NotificationMessage/utils.ts: -------------------------------------------------------------------------------- 1 | import { NotificationStyleToggleColor } from '../../../../types'; 2 | 3 | export const getMessageURLTarget = (url: string) => { 4 | const urlObj = new URL(url); 5 | 6 | if (urlObj.hostname === window.location.hostname) { 7 | return '_self'; 8 | } 9 | 10 | return '_blank'; 11 | }; 12 | 13 | export const timeFormatter = new Intl.DateTimeFormat('en-US', { 14 | year: 'numeric', 15 | month: 'short', 16 | day: 'numeric', 17 | hour: '2-digit', 18 | minute: '2-digit', 19 | hour12: true, 20 | }); 21 | 22 | export const getColor = ( 23 | color?: string | NotificationStyleToggleColor, 24 | theme?: 'light' | 'dark', 25 | ): string | undefined => { 26 | if (!color) { 27 | return; 28 | } 29 | 30 | if (typeof color === 'string') { 31 | return color; 32 | } 33 | 34 | return color[theme || 'light']; 35 | }; 36 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/NotificationsFeed/NotificationsFeed.tsx: -------------------------------------------------------------------------------- 1 | import { useNotificationThreadMessages } from '@dialectlabs/react-sdk'; 2 | import { PropsWithChildren, useEffect } from 'react'; 3 | import { NoNotifications } from './NoNotifications'; 4 | import { NotificationsList } from './NotificationsList'; 5 | import { NotificationsLoading } from './NotificationsLoading'; 6 | 7 | export const NotificationsFeed = ({ 8 | children, 9 | isEmpty, 10 | isLoading, 11 | }: PropsWithChildren<{ isLoading: boolean; isEmpty: boolean }>) => { 12 | if (isLoading) { 13 | return ; 14 | } 15 | 16 | if (isEmpty) { 17 | return ; 18 | } 19 | 20 | return children; 21 | }; 22 | 23 | NotificationsFeed.Container = function NotificationsFeeContainer() { 24 | const { messages, isMessagesLoading, markAsRead } = 25 | useNotificationThreadMessages(); 26 | 27 | useEffect(() => { 28 | if (messages.length > 0) { 29 | markAsRead(); 30 | } 31 | }, [messages.length, markAsRead]); 32 | 33 | return ( 34 | 38 | 39 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/NotificationsFeed/NotificationsFeedHeader.tsx: -------------------------------------------------------------------------------- 1 | import { Header } from '../../core'; 2 | import { useExternalProps } from '../internal/ExternalPropsProvider'; 3 | import { Route, useRouter } from '../internal/Router'; 4 | 5 | export const NotificationsFeedHeader = () => { 6 | const { setOpen } = useExternalProps(); 7 | const { setRoute } = useRouter(); 8 | 9 | return ( 10 |
setRoute(Route.Settings)} 16 | onCloseClick={() => setOpen?.(false)} 17 | /> 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/NotificationsFeed/NotificationsFeedScreen.tsx: -------------------------------------------------------------------------------- 1 | import { NotificationsFeed } from './NotificationsFeed'; 2 | import { NotificationsFeedHeader } from './NotificationsFeedHeader'; 3 | 4 | export const NotificationsFeedScreen = () => { 5 | return ( 6 |
7 | 8 |
9 | 10 |
11 |
12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/NotificationsFeed/NotificationsList.tsx: -------------------------------------------------------------------------------- 1 | import { ThreadMessage } from '@dialectlabs/react-sdk'; 2 | import { ReactNode, useMemo } from 'react'; 3 | import { NotificationMessage } from './NotificationMessage'; 4 | import { 5 | NotificationsItemsContext, 6 | NotificationsItemsProviderValue, 7 | } from './context'; 8 | 9 | export const NotificationsList = ({ children }: { children?: ReactNode }) => { 10 | return
{children}
; 11 | }; 12 | 13 | NotificationsList.Container = function NotificationListContainer({ 14 | messages, 15 | }: { 16 | messages: ThreadMessage[]; 17 | }) { 18 | // potentially move to useSWR, since messages will change on every new fetch 19 | const context: NotificationsItemsProviderValue = useMemo(() => { 20 | return { 21 | list: messages.map((it) => it.id), 22 | map: Object.fromEntries(messages.map((it) => [it.id, it])), 23 | }; 24 | }, [messages]); 25 | 26 | return ( 27 | 28 | 29 | {messages.map((it) => ( 30 | 31 | ))} 32 | 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/NotificationsFeed/NotificationsLoading.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { ClassTokens, Icons } from '../../theme'; 3 | 4 | export const NotificationsLoading = () => { 5 | return ( 6 |
7 |
8 | 9 |
10 | 11 |

17 | Loading your notifications 18 |

19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/NotificationsFeed/context.ts: -------------------------------------------------------------------------------- 1 | import { ThreadMessage } from '@dialectlabs/react-sdk'; 2 | import { createContext, useContext } from 'react'; 3 | 4 | // TODO: update to ThreadMessages, once `id` is returned 5 | export interface NotificationsItemsProviderValue { 6 | list: ThreadMessage['id'][]; // list of ids, for order 7 | map: Record; 8 | } 9 | 10 | export const NotificationsItemsContext = 11 | createContext({ list: [], map: {} }); 12 | 13 | export const useNotification = (id: ThreadMessage['id']) => { 14 | const items = useContext(NotificationsItemsContext); 15 | 16 | return items.map[id]; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/NotificationsFeed/index.ts: -------------------------------------------------------------------------------- 1 | export * from './NotificationsFeedScreen'; 2 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/Settings/AppInfo.tsx: -------------------------------------------------------------------------------- 1 | import { REACT_SDK_VERSION } from '@dialectlabs/react-sdk'; 2 | import clsx from 'clsx'; 3 | import { version as REACT_UI_VERSION } from '../../../../package.json'; 4 | import { DialectLogo } from '../../core/icons'; 5 | import { ClassTokens } from '../../theme'; 6 | export const AppInfo = () => { 7 | return ( 8 |
9 |
17 | Powered By{' '} 18 | 24 | 25 | 26 |
27 | 28 |
29 | 30 | {REACT_UI_VERSION} / {REACT_SDK_VERSION} 31 | 32 |
33 |
34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/Settings/Channels/ChannelNotificationsToggle.tsx: -------------------------------------------------------------------------------- 1 | import { Switch } from '../../../core/primitives'; 2 | 3 | interface Props { 4 | enabled: boolean; 5 | onChange: (newValue: boolean) => void; 6 | } 7 | export const ChannelNotificationsToggle = ({ enabled, onChange }: Props) => ( 8 | {`Notifications ${enabled ? 'On' : 'Off'}`} 12 | ); 13 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/Settings/Channels/EmailChannel/EmailVerificationCodeInput.tsx: -------------------------------------------------------------------------------- 1 | import { AddressType } from '@dialectlabs/react-sdk'; 2 | import clsx from 'clsx'; 3 | import { useMemo } from 'react'; 4 | import { Button, Input, TextButton } from '../../../../core'; 5 | import { ClassTokens, Icons } from '../../../../theme'; 6 | import { useVerificationCode } from '../model/useVerificationCode'; 7 | 8 | const VERIFICATION_CODE_REGEX = new RegExp('^[0-9]{6}$'); 9 | export const EmailVerificationCodeInput = ({ email }: { email: string }) => { 10 | const { 11 | verificationCode, 12 | setVerificationCode, 13 | sendCode, 14 | resendCode, 15 | isSendingCode, 16 | isResendingCode, 17 | deleteAddress, 18 | currentError, 19 | } = useVerificationCode(AddressType.Email); 20 | 21 | const isCodeValid = useMemo( 22 | () => VERIFICATION_CODE_REGEX.test(verificationCode), 23 | [verificationCode], 24 | ); 25 | const isLoading = isSendingCode || isResendingCode; 26 | 27 | return ( 28 |
29 | setVerificationCode(e.target.value)} 36 | rightAdornment={ 37 | isLoading ? ( 38 |
39 | 40 |
41 | ) : ( 42 | 48 | ) 49 | } 50 | /> 51 | 52 | {currentError && ( 53 |

54 | {currentError.message} 55 |

56 | )} 57 | 58 |
59 |

60 | Check your {email} 61 | inbox for a verification code. 62 |

63 |
64 | 65 | 66 | Cancel 67 | 68 | 69 | 70 | Resend Code 71 | 72 |
73 |
74 |
75 | ); 76 | }; 77 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/Settings/Channels/EmailChannel/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AddressType, 3 | useDialectContext, 4 | useNotificationChannel, 5 | } from '@dialectlabs/react-sdk'; 6 | import { EmailInput } from './EmailInput'; 7 | import { EmailVerificationCodeInput } from './EmailVerificationCodeInput'; 8 | 9 | export const EmailChannel = () => { 10 | const { dappAddress } = useDialectContext(); 11 | const { globalAddress: emailAddress } = useNotificationChannel({ 12 | addressType: AddressType.Email, 13 | }); 14 | const isEmailSaved = Boolean(emailAddress); 15 | const isVerified = Boolean(emailAddress?.verified); 16 | 17 | const verificationNeeded = isEmailSaved && !isVerified; 18 | 19 | return ( 20 |
21 | {verificationNeeded ? ( 22 | 23 | ) : ( 24 | 25 | )} 26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/Settings/Channels/TelegramChannel/TelegramVerificationCodeInput.tsx: -------------------------------------------------------------------------------- 1 | import { AddressType, useDialectSdk } from '@dialectlabs/react-sdk'; 2 | import clsx from 'clsx'; 3 | import { useMemo } from 'react'; 4 | import { Button, Input, TextButton } from '../../../../core'; 5 | import { ClassTokens, Icons } from '../../../../theme'; 6 | import { useVerificationCode } from '../model/useVerificationCode'; 7 | 8 | const VERIFICATION_CODE_REGEX = new RegExp('^[0-9]{6}$'); 9 | export const TelegramVerificationCodeInput = ({ 10 | dappTelegramName, 11 | }: { 12 | dappTelegramName: string; 13 | }) => { 14 | const { 15 | config: { environment }, 16 | } = useDialectSdk(); 17 | 18 | const { 19 | verificationCode, 20 | setVerificationCode, 21 | sendCode, 22 | isSendingCode, 23 | deleteAddress, 24 | currentError, 25 | } = useVerificationCode(AddressType.Telegram); 26 | 27 | const buildBotUrl = (botUsername: string) => 28 | `https://t.me/${botUsername}?start=${botUsername}`; 29 | 30 | const defaultBotUrl = 31 | environment === 'production' 32 | ? buildBotUrl('DialectLabsBot') 33 | : buildBotUrl('DialectLabsDevBot'); 34 | 35 | const botURL = useMemo(() => { 36 | if (!dappTelegramName) { 37 | return defaultBotUrl; 38 | } 39 | return buildBotUrl(dappTelegramName); 40 | }, [dappTelegramName, defaultBotUrl]); 41 | 42 | const isCodeValid = useMemo( 43 | () => VERIFICATION_CODE_REGEX.test(verificationCode), 44 | [verificationCode], 45 | ); 46 | 47 | return ( 48 |
49 | setVerificationCode(e.target.value)} 56 | rightAdornment={ 57 | isSendingCode ? ( 58 |
59 | 60 |
61 | ) : ( 62 | 68 | ) 69 | } 70 | /> 71 | {currentError && ( 72 |

73 | {currentError.message} 74 |

75 | )} 76 | 91 |
92 | ); 93 | }; 94 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/Settings/Channels/TelegramChannel/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AddressType, 3 | useDialectContext, 4 | useNotificationChannel, 5 | useNotificationDapp, 6 | } from '@dialectlabs/react-sdk'; 7 | import { TelegramHandleInput } from './TelegramHandleInput'; 8 | import { TelegramVerificationCodeInput } from './TelegramVerificationCodeInput'; 9 | 10 | export const TelegramChannel = () => { 11 | const { dappAddress } = useDialectContext(); 12 | 13 | const { globalAddress: telegramAddress } = useNotificationChannel({ 14 | addressType: AddressType.Telegram, 15 | }); 16 | const isTelegramSaved = Boolean(telegramAddress); 17 | const isVerified = Boolean(telegramAddress?.verified); 18 | 19 | const { dapp } = useNotificationDapp(); 20 | 21 | const verificationNeeded = isTelegramSaved && !isVerified; 22 | return ( 23 |
24 | {verificationNeeded ? ( 25 | 28 | ) : ( 29 | 30 | )} 31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/Settings/Channels/WalletChannel.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AddressType, 3 | ThreadMemberScope, 4 | useDialectContext, 5 | useDialectSdk, 6 | useNotificationChannel, 7 | useNotificationChannelDappSubscription, 8 | useNotificationThread, 9 | useUnreadNotifications, 10 | } from '@dialectlabs/react-sdk'; 11 | import clsx from 'clsx'; 12 | import { useCallback } from 'react'; 13 | import { displayAddress } from '../../../../utils/displayAddress'; 14 | import { Button, IconButton, Input } from '../../../core'; 15 | import { ClassTokens, Icons } from '../../../theme'; 16 | import { ChannelNotificationsToggle } from './ChannelNotificationsToggle'; 17 | 18 | const ADDRESS_TYPE = AddressType.Wallet; 19 | 20 | export const WalletChannel = () => { 21 | const { dappAddress } = useDialectContext(); 22 | const { 23 | wallet: { address: walletAddress }, 24 | } = useDialectSdk(); 25 | 26 | const { 27 | thread, 28 | create: createThread, 29 | isCreatingThread, 30 | delete: deleteThread, 31 | isDeletingThread, 32 | } = useNotificationThread(); 33 | 34 | const { refresh: refreshUnreadNotifications } = useUnreadNotifications({ 35 | revalidateOnMount: false, 36 | }); 37 | 38 | const { 39 | globalAddress: walletSubscriptionAddress, 40 | create: createAddress, 41 | delete: deleteAddress, 42 | isCreatingAddress, 43 | isDeletingAddress, 44 | } = useNotificationChannel({ addressType: ADDRESS_TYPE }); 45 | 46 | const { 47 | enabled: subscriptionEnabled, 48 | toggleSubscription, 49 | isToggling, 50 | } = useNotificationChannelDappSubscription({ 51 | addressType: ADDRESS_TYPE, 52 | dappAddress, 53 | }); 54 | 55 | const deleteWalletThread = useCallback(async () => { 56 | await deleteThread(); 57 | await deleteAddress(); 58 | }, [deleteAddress, deleteThread]); 59 | 60 | const createWalletThread = useCallback(async () => { 61 | if (!dappAddress) return; 62 | return createThread({ 63 | me: { scopes: [ThreadMemberScope.ADMIN] }, 64 | otherMembers: [ 65 | { address: dappAddress, scopes: [ThreadMemberScope.WRITE] }, 66 | ], 67 | encrypted: false, 68 | }); 69 | }, [createThread, dappAddress]); 70 | 71 | const createWalletAddress = useCallback( 72 | () => createAddress({ value: walletAddress }), 73 | [createAddress, walletAddress], 74 | ); 75 | 76 | const isLoading = 77 | isDeletingThread || 78 | isCreatingThread || 79 | isDeletingAddress || 80 | isCreatingAddress || 81 | isToggling; 82 | 83 | const setUpWallet = useCallback(async () => { 84 | if (isLoading) return; 85 | const noSubscription = !walletSubscriptionAddress && !thread; 86 | let notificationsThread; 87 | if (!thread) { 88 | notificationsThread = await createWalletThread(); 89 | refreshUnreadNotifications(); 90 | } 91 | let walletAddress; 92 | if (!walletSubscriptionAddress) { 93 | walletAddress = await createWalletAddress(); 94 | } 95 | if (noSubscription && notificationsThread) { 96 | await toggleSubscription({ enabled: true, address: walletAddress }); 97 | } 98 | }, [ 99 | createWalletAddress, 100 | createWalletThread, 101 | isLoading, 102 | refreshUnreadNotifications, 103 | thread, 104 | toggleSubscription, 105 | walletSubscriptionAddress, 106 | ]); 107 | 108 | const isWalletSetUp = thread && walletSubscriptionAddress; 109 | const RightAdornment = useCallback(() => { 110 | if (isLoading) 111 | return ( 112 |
113 | 114 |
115 | ); 116 | if (isWalletSetUp) 117 | return ( 118 | } 122 | /> 123 | ); 124 | return ; 125 | }, [deleteWalletThread, isLoading, isWalletSetUp, setUpWallet]); 126 | 127 | // console.log(isWalletSetUp); 128 | 129 | return ( 130 |
131 | } 136 | /> 137 | {isWalletSetUp && ( 138 | { 141 | if (isToggling) return; 142 | return toggleSubscription({ enabled: newValue }); 143 | }} 144 | /> 145 | )} 146 |
147 | ); 148 | }; 149 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/Settings/Channels/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import { ChannelType } from '../../../../types'; 3 | import { EmailChannel } from './EmailChannel'; 4 | import { TelegramChannel } from './TelegramChannel'; 5 | import { WalletChannel } from './WalletChannel'; 6 | 7 | interface Props { 8 | channels: ChannelType[]; 9 | } 10 | 11 | const Channel = ({ type }: { type: ChannelType }) => { 12 | const ChannelRow = () => { 13 | if (type === 'email') return ; 14 | if (type === 'telegram') return ; 15 | return ; 16 | }; 17 | return ( 18 |
19 | 20 |
21 | ); 22 | }; 23 | 24 | export const Channels = memo(function Channels({ channels }: Props) { 25 | return ( 26 |
27 | {channels.map((it) => ( 28 | 29 | ))} 30 |
31 | ); 32 | }); 33 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/Settings/Channels/model/useVerificationCode.ts: -------------------------------------------------------------------------------- 1 | import { AddressType, useNotificationChannel } from '@dialectlabs/react-sdk'; 2 | import { useState } from 'react'; 3 | export const useVerificationCode = (addressType: AddressType) => { 4 | const [verificationCode, setVerificationCode] = useState(''); 5 | const [currentError, setCurrentError] = useState(null); 6 | const { 7 | verify, 8 | resend, 9 | isSendingCode, 10 | isVerifyingCode, 11 | delete: deleteAddr, 12 | } = useNotificationChannel({ 13 | addressType, 14 | }); 15 | 16 | const sendCode = async () => { 17 | try { 18 | await verify({ code: verificationCode }); 19 | setCurrentError(null); 20 | } catch (e) { 21 | setCurrentError(e as Error); 22 | } finally { 23 | setVerificationCode(''); 24 | } 25 | }; 26 | 27 | const resendCode = async () => { 28 | try { 29 | await resend(); 30 | setCurrentError(null); 31 | } catch (e) { 32 | setCurrentError(e as Error); 33 | } 34 | }; 35 | 36 | const deleteAddress = async () => { 37 | try { 38 | await deleteAddr(); 39 | setCurrentError(null); 40 | } catch (e) { 41 | setCurrentError(e as Error); 42 | } 43 | }; 44 | 45 | return { 46 | verificationCode, 47 | setVerificationCode, 48 | sendCode, 49 | isSendingCode: isVerifyingCode, 50 | resendCode, 51 | isResendingCode: isSendingCode, 52 | deleteAddress, 53 | currentError, 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/Settings/NotificationTypes.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useDialectContext, 3 | useNotificationSubscriptions, 4 | } from '@dialectlabs/react-sdk'; 5 | import clsx from 'clsx'; 6 | import { memo } from 'react'; 7 | import { Checkbox } from '../../core'; 8 | import { ClassTokens } from '../../theme'; 9 | 10 | interface Props { 11 | title: string; 12 | description?: string; 13 | enabled: boolean; 14 | onChange: (newValue: boolean) => void; 15 | } 16 | 17 | const NotificationType = ({ title, description, enabled, onChange }: Props) => { 18 | return ( 19 |
26 |
27 | 33 | {title} 34 | 35 | {description && ( 36 | 42 | {description} 43 | 44 | )} 45 |
46 | 47 |
48 | ); 49 | }; 50 | 51 | export const NotificationTypes = memo(function NotificationTypes() { 52 | const { dappAddress } = useDialectContext(); 53 | 54 | const { 55 | subscriptions: notificationSubscriptions, 56 | update: updateNotificationSubscription, 57 | isUpdating, 58 | errorUpdating: errorUpdatingNotificationSubscription, 59 | errorFetching: errorFetchingNotificationsConfigs, 60 | } = useNotificationSubscriptions({ dappAddress }); 61 | const error = 62 | errorFetchingNotificationsConfigs || errorUpdatingNotificationSubscription; 63 | 64 | return ( 65 |
66 | {error &&

{error.message}

} 67 | {Boolean(notificationSubscriptions.length) && ( 68 | <> 69 |

75 | Notification Type 76 |

77 | {notificationSubscriptions.map( 78 | ({ notificationType, subscription }) => ( 79 | { 85 | if (isUpdating) return; 86 | updateNotificationSubscription({ 87 | notificationTypeId: notificationType.id, 88 | config: { 89 | ...subscription.config, 90 | enabled: value, 91 | }, 92 | }); 93 | }} 94 | /> 95 | ), 96 | )} 97 | 98 | )} 99 |
100 | ); 101 | }); 102 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/Settings/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AddressType, 3 | useDialectContext, 4 | useNotificationChannelDappSubscription, 5 | useNotificationSubscriptions, 6 | } from '@dialectlabs/react-sdk'; 7 | import clsx from 'clsx'; 8 | import { ReactNode } from 'react'; 9 | import { ClassTokens } from '../../theme'; 10 | import { useExternalProps } from '../internal/ExternalPropsProvider'; 11 | import { AppInfo } from './AppInfo'; 12 | import { Channels } from './Channels'; 13 | import { NotificationTypes } from './NotificationTypes'; 14 | import { SettingsLoading } from './SettingsLoading'; 15 | import { TosAndPrivacy } from './TosAndPrivacy'; 16 | 17 | export const Settings = ({ 18 | renderAdditionalSettingsUi, 19 | }: { 20 | renderAdditionalSettingsUi?: (args: Record) => ReactNode; 21 | }) => { 22 | const { dappAddress } = useDialectContext(); 23 | 24 | const subscription = useNotificationChannelDappSubscription({ 25 | addressType: AddressType.Wallet, 26 | dappAddress, 27 | }); 28 | 29 | const { isFetching: isFetchingNotificationsSubscriptions } = 30 | useNotificationSubscriptions({ dappAddress }); 31 | 32 | const isLoading = 33 | subscription.isFetchingSubscriptions || 34 | isFetchingNotificationsSubscriptions; 35 | 36 | const { channels } = useExternalProps(); 37 | 38 | return isLoading ? ( 39 | 40 | ) : ( 41 |
42 |
43 | 44 |
45 |
46 | 47 |
48 | {renderAdditionalSettingsUi?.({})} 49 |
50 |
56 | 57 | 58 |
59 |
60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/Settings/SettingsHeader.tsx: -------------------------------------------------------------------------------- 1 | import { Header } from '../../core'; 2 | import { useExternalProps } from '../internal/ExternalPropsProvider'; 3 | import { Route, useRouter } from '../internal/Router'; 4 | 5 | export const SettingsHeader = () => { 6 | const { setOpen } = useExternalProps(); 7 | const { setRoute } = useRouter(); 8 | 9 | return ( 10 |
setRoute(Route.Notifications)} 16 | onCloseClick={() => setOpen?.(false)} 17 | /> 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/Settings/SettingsLoading.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { ClassTokens, Icons } from '../../theme'; 3 | 4 | export const SettingsLoading = () => { 5 | return ( 6 |
7 |
8 | 9 |
10 | {/*TODO ???*/} 11 | {/**/} 17 | {/* Loading settings*/} 18 | {/**/} 19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/Settings/SettingsScreen.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { Settings } from './Settings'; 3 | import { SettingsHeader } from './SettingsHeader'; 4 | 5 | export const SettingsScreen = ({ 6 | renderAdditionalSettingsUi, 7 | }: { 8 | renderAdditionalSettingsUi?: (args: Record) => ReactNode; 9 | }) => { 10 | return ( 11 |
12 | 13 |
14 | 15 |
16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/Settings/TosAndPrivacy.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { Link } from '../../core'; 3 | import { ClassTokens } from '../../theme'; 4 | 5 | export const TosAndPrivacy = () => ( 6 |
7 |

13 | By enabling notifications you agree to Dialect's{' '} 14 | 15 | Terms of Service 16 | {' '} 17 | and{' '} 18 | 23 | Privacy Policy 24 | 25 |

26 |
27 | ); 28 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/Settings/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SettingsScreen'; 2 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/index.ts: -------------------------------------------------------------------------------- 1 | export { Notifications } from './Notifications'; 2 | export type { NotificationsProps } from './Notifications'; 3 | export * from './NotificationsButton'; 4 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/internal/ExternalPropsProvider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, ReactNode, useContext } from 'react'; 2 | import { ChannelType } from '../../../types'; 3 | 4 | export interface ExternalProps { 5 | channels: ChannelType[]; 6 | open?: boolean; 7 | setOpen?: (open: boolean | ((prev: boolean) => boolean)) => void; 8 | } 9 | 10 | const ExternalPropsContext = createContext(null); 11 | 12 | export const ExternalPropsProvider = ({ 13 | children, 14 | props, 15 | }: { 16 | children: ReactNode; 17 | props: ExternalProps; 18 | }) => { 19 | return ( 20 | 21 | {children} 22 | 23 | ); 24 | }; 25 | 26 | export const useExternalProps = () => { 27 | const context = useContext(ExternalPropsContext); 28 | 29 | if (!context) { 30 | throw new Error( 31 | 'useExternalProps must be used within a ExternalPropsProvider', 32 | ); 33 | } 34 | 35 | return context; 36 | }; 37 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/internal/Router.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, ReactNode, useContext, useMemo, useState } from 'react'; 2 | 3 | export enum Route { 4 | Notifications = 'notifications', 5 | Settings = 'settings', 6 | } 7 | 8 | export interface RouterValue { 9 | route: Route; 10 | setRoute: (route: Route) => void; 11 | } 12 | 13 | export const RouterContext = createContext(null); 14 | 15 | export const Router = ({ 16 | initialRoute, 17 | children, 18 | }: { 19 | initialRoute?: Route; 20 | children: (route: Route) => ReactNode; 21 | }) => { 22 | const [route, setRoute] = useState(initialRoute ?? Route.Notifications); 23 | 24 | const context = useMemo(() => ({ route, setRoute }), [route]); 25 | 26 | return ( 27 | 28 | {children(route)} 29 | 30 | ); 31 | }; 32 | 33 | export const useRouter = () => { 34 | const context = useContext(RouterContext); 35 | 36 | if (!context) { 37 | throw new Error('useRouter must be used within a Router'); 38 | } 39 | 40 | return context; 41 | }; 42 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/notifications/internal/useClickAway.ts: -------------------------------------------------------------------------------- 1 | import { RefObject, useEffect, useRef } from 'react'; 2 | 3 | const defaultEvents = ['mousedown', 'touchstart']; 4 | 5 | export const useClickAway = ( 6 | refs: RefObject[], 7 | onClickAway: (event: E) => void, 8 | events: string[] = defaultEvents, 9 | ) => { 10 | const savedCallback = useRef(onClickAway); 11 | useEffect(() => { 12 | savedCallback.current = onClickAway; 13 | }, [onClickAway]); 14 | useEffect(() => { 15 | const handler = (event: Event) => { 16 | let shouldIgnore = false; 17 | refs.forEach((ref) => { 18 | const el = ref.current; 19 | shouldIgnore = 20 | shouldIgnore || Boolean(el?.contains(event.target as Node)); 21 | }); 22 | 23 | !shouldIgnore && savedCallback.current(event as E); 24 | }; 25 | 26 | for (const eventName of events) { 27 | document.addEventListener(eventName, handler); 28 | } 29 | return () => { 30 | for (const eventName of events) { 31 | document.removeEventListener(eventName, handler); 32 | } 33 | }; 34 | }, [events, refs]); 35 | }; 36 | -------------------------------------------------------------------------------- /packages/react-ui/src/ui/theme.tsx: -------------------------------------------------------------------------------- 1 | import { NotificationStyleMap } from '../types'; 2 | import { 3 | ArrowLeftIcon, 4 | ArrowRightIcon, 5 | BellButtonIcon, 6 | BellButtonIconOutline, 7 | BellIcon, 8 | CloseIcon, 9 | ResendIcon, 10 | SettingsIcon, 11 | SpinnerDots, 12 | TrashIcon, 13 | WalletIcon, 14 | XmarkIcon, 15 | } from './core/icons'; 16 | 17 | export const Icons = { 18 | Loader: SpinnerDots, 19 | Settings: SettingsIcon, 20 | ArrowLeft: ArrowLeftIcon, 21 | ArrowRight: ArrowRightIcon, 22 | Close: CloseIcon, 23 | Bell: BellIcon, 24 | BellButton: BellButtonIcon, 25 | BellButtonOutline: BellButtonIconOutline, 26 | Trash: TrashIcon, 27 | Xmark: XmarkIcon, 28 | Resend: ResendIcon, 29 | Wallet: WalletIcon, 30 | }; 31 | 32 | export const NotificationTypeStyles: NotificationStyleMap = {}; 33 | 34 | export const ClassTokens = { 35 | Text: { 36 | Primary: 'dt-text-[--dt-text-primary]', 37 | Secondary: 'dt-text-[--dt-text-secondary]', 38 | Tertiary: 'dt-text-[--dt-text-tertiary]', 39 | Accent: 'dt-text-[--dt-text-accent]', 40 | Success: 'dt-text-[--dt-accent-success]', 41 | Error: 'dt-text-[--dt-accent-error]', 42 | Inverse: 'dt-text-[--dt-text-inverse]', 43 | Button: { 44 | Primary: { 45 | Default: 'dt-text-[--dt-text-inverse]', 46 | Disabled: 'disabled:dt-text-[--dt-text-tertiary]', 47 | }, 48 | Secondary: { 49 | Default: 'dt-text-[--dt-text-primary]', 50 | Disabled: 'disabled:dt-text-[--dt-text-tertiary]', 51 | }, 52 | Destructive: { 53 | Default: 'dt-text-[--dt-accent-error]', 54 | Disabled: 'disabled:dt-text-[--dt-accent-error]', 55 | }, 56 | }, 57 | Input: { 58 | Placeholder: 'placeholder:dt-text-[--dt-text-quaternary]', 59 | }, 60 | }, 61 | Icon: { 62 | Primary: 'dt-text-[--dt-icon-primary]', 63 | Secondary: 'dt-text-[--dt-icon-secondary]', 64 | Tertiary: 'dt-text-[--dt-icon-tertiary]', 65 | }, 66 | Background: { 67 | Button: { 68 | Primary: { 69 | Default: 'dt-bg-[--dt-button-primary]', 70 | Hover: 'hover:dt-bg-[--dt-button-primary-hover]', 71 | Pressed: 'active:dt-bg-[--dt-button-primary]', 72 | Disabled: 'disabled:dt-bg-[--dt-button-primary-disabled]', 73 | }, 74 | Secondary: { 75 | Default: 'dt-bg-[--dt-button-secondary]', 76 | Hover: 'hover:dt-bg-[--dt-button-secondary-hover]', 77 | Pressed: 'active:dt-bg-[--dt-button-secondary]', 78 | Disabled: 'disabled:dt-bg-[--dt-button-secondary-disabled]', 79 | }, 80 | Destructive: { 81 | Default: 'dt-bg-[--dt-button-secondary]', 82 | Hover: 'hover:dt-bg-[--dt-button-secondary-hover]', 83 | Pressed: 'active:dt-bg-[--dt-button-secondary]', 84 | Disabled: 'disabled:dt-bg-[--dt-button-secondary-disabled]', 85 | }, 86 | }, 87 | Input: { 88 | Secondary: 'dt-bg-[--dt-input-secondary]', 89 | Checked: 'dt-bg-[--dt-input-checked]', 90 | Unchecked: 'dt-bg-[--dt-input-unchecked]', 91 | }, 92 | Primary: 'dt-bg-[--dt-bg-primary]', 93 | Secondary: 'dt-bg-[--dt-bg-secondary]', 94 | Tertiary: 'dt-bg-[--dt-bg-tertiary]', 95 | BrandTransparent: 'dt-bg-[--dt-brand-transparent]', 96 | SuccessTransparent: 'dt-bg-[--dt-success-transparent]', 97 | ErrorTransparent: 'dt-bg-[--dt-error-transparent]', 98 | AccentBrand: 'dt-bg-[--dt-accent-brand]', 99 | }, 100 | Stroke: { 101 | Input: { 102 | Primary: 'dt-border-[--dt-input-primary]', 103 | Checked: 'dt-border-[--dt-input-checked]', 104 | Error: 'dt-border-[--dt-accent-error]', 105 | Focused: 'focus-within:dt-border-[--dt-input-checked]', 106 | }, 107 | Primary: 'dt-border-[--dt-stroke-primary]', 108 | Error: 'dt-border-[--dt-accent-error]', 109 | }, 110 | Radius: { 111 | XSmall: 'dt-rounded-[--dt-border-radius-xs]', 112 | Small: 'dt-rounded-[--dt-border-radius-s]', 113 | Medium: 'dt-rounded-[--dt-border-radius-m]', 114 | Large: 'dt-rounded-[--dt-border-radius-l]', 115 | }, 116 | }; 117 | -------------------------------------------------------------------------------- /packages/react-ui/src/utils/displayAddress.ts: -------------------------------------------------------------------------------- 1 | import type { AccountAddress } from '@dialectlabs/sdk'; 2 | 3 | export function displayAddress(address: AccountAddress, chars = 4): string { 4 | const addr = address.toString(); 5 | return `${addr.substring(0, chars)}...${addr.substring(addr.length - chars)}`; 6 | } 7 | -------------------------------------------------------------------------------- /packages/react-ui/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './objects'; 2 | export * from './random'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /packages/react-ui/src/utils/objects.ts: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/questions/27936772/how-to-deep-merge-instead-of-shallow-merge 2 | 3 | /** 4 | * Simple object check. 5 | * @param item 6 | * @returns {boolean} 7 | */ 8 | export function isObject(item: unknown) { 9 | return item && typeof item === 'object' && !Array.isArray(item); 10 | } 11 | 12 | /** 13 | * Deep merge two objects. 14 | * @param target - initial object 15 | * @param sources - objects to merge 16 | * 17 | */ 18 | export function deepMerge< 19 | T extends Record, 20 | S extends Record, 21 | >(target: T, ...sources: Partial[]): T & S { 22 | if (!sources.length) return target; 23 | const source = sources.shift(); 24 | 25 | if (isObject(target) && isObject(source)) { 26 | for (const key in source) { 27 | if (isObject(source[key])) { 28 | if (!target[key]) Object.assign(target, { [key]: {} }); 29 | deepMerge(target[key], source[key]!); 30 | } else { 31 | Object.assign(target, { [key]: source[key] }); 32 | } 33 | } 34 | } 35 | 36 | return deepMerge(target, ...sources); 37 | } 38 | -------------------------------------------------------------------------------- /packages/react-ui/src/utils/random.ts: -------------------------------------------------------------------------------- 1 | export const generateIdRandom = () => { 2 | return Math.random().toString(36).slice(2, 9); 3 | }; 4 | -------------------------------------------------------------------------------- /packages/react-ui/src/utils/types.ts: -------------------------------------------------------------------------------- 1 | export type DeepPartial = T extends object 2 | ? { [K in keyof T]?: DeepPartial } 3 | : Partial; 4 | -------------------------------------------------------------------------------- /packages/react-ui/stories/Button.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, StoryObj } from '@storybook/react'; 2 | import { Button, ButtonType } from '../src/ui/core'; 3 | 4 | export const Main: StoryObj = { 5 | args: { 6 | children: 'Press me', 7 | type: ButtonType.Secondary, 8 | // label: 'Input', 9 | // value: undefined, 10 | // placeholder: 'Type here...', 11 | }, 12 | }; 13 | 14 | export const Primary: StoryObj = { 15 | args: { 16 | children: 'Press me', 17 | type: ButtonType.Primary, 18 | size: 'large', 19 | // label: 'Input', 20 | // value: undefined, 21 | // placeholder: 'Type here...', 22 | }, 23 | }; 24 | 25 | export default { 26 | component: Button, 27 | decorators: [ 28 | (Story) => ( 29 |
30 | 31 |
32 | ), 33 | ], 34 | } satisfies Meta; 35 | -------------------------------------------------------------------------------- /packages/react-ui/stories/Checkbox.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, StoryObj } from '@storybook/react'; 2 | import { Checkbox } from '../src/ui/core/primitives'; 3 | 4 | export const Main: StoryObj = { 5 | args: { 6 | children: 'Notifications', 7 | checked: true, 8 | }, 9 | }; 10 | 11 | export default { 12 | component: Checkbox, 13 | decorators: [(Story) => ], 14 | } satisfies Meta; 15 | -------------------------------------------------------------------------------- /packages/react-ui/stories/CustomIcons.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, StoryObj } from '@storybook/react'; 2 | import { SVGProps } from 'react'; 3 | import { Button } from '../src/ui/core/primitives'; 4 | import { Icons } from '../src/ui/theme'; 5 | 6 | const MyLoaderIcon = (props: SVGProps) => ( 7 | 8 | 17 | 25 | 33 | 34 | 42 | 50 | 51 | 52 | ); 53 | 54 | Icons.Loader = MyLoaderIcon; 55 | 56 | export const Main: StoryObj = { 57 | args: { 58 | children: 'Press me', 59 | loading: true, 60 | }, 61 | }; 62 | 63 | export default { 64 | component: Button, 65 | decorators: [ 66 | (Story) => ( 67 |
68 | 69 |
70 | ), 71 | ], 72 | } satisfies Meta; 73 | -------------------------------------------------------------------------------- /packages/react-ui/stories/Input.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, StoryObj } from '@storybook/react'; 2 | import { Input } from '../src/ui/core/primitives'; 3 | 4 | export const Main: StoryObj = { 5 | args: { 6 | label: 'Input', 7 | value: undefined, 8 | placeholder: 'Type here...', 9 | }, 10 | argTypes: { 11 | value: { 12 | type: 'string', 13 | }, 14 | }, 15 | }; 16 | 17 | export const WithRightAdornment: StoryObj = { 18 | args: { 19 | label: 'Input', 20 | value: undefined, 21 | placeholder: 'Type here...', 22 | rightAdornment: 👋, 23 | }, 24 | argTypes: { 25 | value: { 26 | type: 'string', 27 | }, 28 | }, 29 | }; 30 | 31 | export default { 32 | component: Input, 33 | decorators: [ 34 | (Story) => ( 35 |
36 | 37 |
38 | ), 39 | ], 40 | } satisfies Meta; 41 | -------------------------------------------------------------------------------- /packages/react-ui/stories/NotificationsFeed.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, StoryObj } from '@storybook/react'; 2 | import { NotificationsFeed } from '../src/ui/notifications/NotificationsFeed/NotificationsFeed'; 3 | 4 | export const Main: StoryObj = { 5 | args: {}, 6 | }; 7 | 8 | export default { 9 | component: NotificationsFeed, 10 | } satisfies Meta; 11 | -------------------------------------------------------------------------------- /packages/react-ui/stories/Switch.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, StoryObj } from '@storybook/react'; 2 | import { fn } from '@storybook/test'; 3 | import { Switch } from '../src/ui/core/primitives'; 4 | 5 | export const Main: StoryObj = { 6 | args: { 7 | children: 'Notifications', 8 | checked: false, 9 | onChange: fn(), 10 | }, 11 | }; 12 | 13 | export default { 14 | component: Switch, 15 | decorators: [(Story) => ], 16 | } satisfies Meta; 17 | -------------------------------------------------------------------------------- /packages/react-ui/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ['./src/**/*.{js,ts,jsx,tsx}'], 4 | theme: { 5 | colors: { 6 | transparent: 'transparent', 7 | current: 'currentColor', 8 | }, 9 | extend: { 10 | fontSize: { 11 | h2: [ 12 | '1.0625rem', 13 | { 14 | fontWeight: 600, 15 | lineHeight: '1.25rem', 16 | }, 17 | ], 18 | text: ['0.9375rem', '1.125rem'], 19 | subtext: ['0.8125rem', '1rem'], 20 | caption: ['0.6875rem', '0.875rem'], 21 | }, 22 | backgroundImage: { 23 | 'gradient-button': 24 | 'linear-gradient(95deg, #2B2D2D 4.07%, #414445 51.31%, #2B2D2D 95.93%)', 25 | }, 26 | }, 27 | }, 28 | plugins: [], 29 | prefix: 'dt-', 30 | }; 31 | -------------------------------------------------------------------------------- /packages/react-ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src/*.ts", "src/*.tsx"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/react-ui/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts', 'src/index.css'], 5 | splitting: false, 6 | sourcemap: true, 7 | clean: true, 8 | dts: { 9 | entry: 'src/index.ts', 10 | }, 11 | format: ['cjs', 'esm'], 12 | target: ['esnext'], 13 | }); 14 | -------------------------------------------------------------------------------- /packages/react-ui/vite.config.js: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react'; 2 | 3 | export default { 4 | plugins: [react({ jsxRuntime: 'automatic' })], 5 | }; 6 | -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import("prettier").Config} */ 2 | const config = { 3 | plugins: ['prettier-plugin-organize-imports', 'prettier-plugin-tailwindcss'], 4 | trailingComma: 'all', 5 | tabWidth: 2, 6 | semi: true, 7 | singleQuote: true, 8 | tailwindFunctions: ['clsx', 'cn'], 9 | }; 10 | 11 | export default config; 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // target 4 | "lib": ["dom", "ESNext"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleResolution": "node", 8 | "jsx": "react-jsx", 9 | // build 10 | "types": [], 11 | "esModuleInterop": true, 12 | "preserveConstEnums": true, 13 | "skipLibCheck": true, 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | // linting 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noImplicitOverride": true, 21 | "noUncheckedIndexedAccess": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "forceConsistentCasingInFileNames": true, 24 | "noErrorTruncation": true, 25 | "strictNullChecks": true, 26 | "verbatimModuleSyntax": false 27 | } 28 | } 29 | --------------------------------------------------------------------------------