├── .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 |   
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 |
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 |
36 | *]:bg-[var(--background)]'
39 | }
40 | >
41 |
47 | {icon}
48 |
49 |
50 |
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 |
50 | {props.loading ? (
51 | <>
52 | Loading
53 | >
54 | ) : (
55 | props.children
56 | )}
57 |
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 |
29 |
30 |
41 | {checked && }
42 |
43 | {children}
44 |
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 |
15 | {icon}
16 |
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 |
40 | {label}
41 |
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 | onChange?.(!checked)}
19 | >
20 | onChange?.(!checked)}
24 | />
25 |
33 |
40 |
41 | {children}
42 |
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 |
29 | {children}
30 |
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 |
setConnectionInitiated(true)}
55 | >
56 | {isHardwareWalletForced ? 'Sign transaction' : 'Sign message'}
57 |
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 |
61 |
62 | {unread && (
63 |
69 | )}
70 | {open ? : }
71 |
72 |
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 |
router.setRoute(Route.Settings)}
38 | >
39 | Set up notifications
40 |
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 |
19 | {label}
20 |
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 |
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 |
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 |
46 | Submit
47 |
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 |
66 | Submit
67 |
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 Enable ;
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 |
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 |