diff --git a/.changeset/smooth-bushes-wear.md b/.changeset/smooth-bushes-wear.md new file mode 100644 index 000000000..22c49a6d1 --- /dev/null +++ b/.changeset/smooth-bushes-wear.md @@ -0,0 +1,21 @@ +--- +'@ton/appkit-react': patch +'@ton/walletkit': patch +'@ton/appkit': patch +--- + +- `@ton/appkit`: + - added `getSwapProvider` and `watchSwapProviders` actions + - added swap-related events and types to `AppKit` core + - added `calcFiatValue` and `formatLargeValue` amount utilities + - added `debounce` utility function +- `@ton/walletkit`: + - added `SwapProviderMetadata` interface + - added `getMetadata()` method to `SwapProvider` + - added metadata support to `DeDustSwapProvider` and `OmnistonSwapProvider` +- `@ton/appkit-react`: + - added `SwapWidget` and related UI components (`SwapField`, `SwapSettings`, `TokenSelector`, etc.) + - added `SwapWidgetProvider` for swap state management + - added hooks for swap: `useSwapProvider`, `useSwapQuote`, `useBuildSwapTransaction` + - added `useDebounceCallback`, `useDebounceValue`, and `useUnmount` utility hooks + - added English localizations for swap features diff --git a/apps/appkit-minter/src/app.tsx b/apps/appkit-minter/src/app.tsx index 90b232ce5..8e6f33d5a 100644 --- a/apps/appkit-minter/src/app.tsx +++ b/apps/appkit-minter/src/app.tsx @@ -12,8 +12,7 @@ import { AppKitProvider } from '@ton/appkit-react'; import { appKit } from '@/core/configs/app-kit'; import { AppRouter, ThemeProvider, ToasterProvider } from '@/core/components'; -import './core/styles/app.css'; -import '@ton/appkit-react/styles.css'; +import './core/styles/index.css'; const queryClient = new QueryClient(); diff --git a/apps/appkit-minter/src/core/components/common/button.tsx b/apps/appkit-minter/src/core/components/common/button.tsx deleted file mode 100644 index b30647602..000000000 --- a/apps/appkit-minter/src/core/components/common/button.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright (c) TonTech. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import type React from 'react'; -import { Loader2 } from 'lucide-react'; - -import { cn } from '@/core/lib/utils'; - -interface ButtonProps extends React.ButtonHTMLAttributes { - variant?: 'primary' | 'secondary' | 'danger' | 'ghost'; - size?: 'sm' | 'md' | 'lg'; - isLoading?: boolean; - children: React.ReactNode; -} - -export const Button: React.FC = ({ - variant = 'primary', - size = 'md', - isLoading = false, - children, - disabled, - className = '', - ...props -}) => { - const baseClasses = - 'font-medium rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center'; - - const variantClasses = { - primary: 'bg-primary text-primary-foreground hover:bg-primary/90 focus:ring-ring shadow-md hover:shadow-lg', - secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 focus:ring-ring', - danger: 'bg-destructive text-destructive-foreground hover:bg-destructive/90 focus:ring-destructive', - ghost: 'hover:bg-accent hover:text-accent-foreground focus:ring-ring', - }; - - const sizeClasses = { - sm: 'px-3 py-2 text-sm', - md: 'px-4 py-2.5 text-base', - lg: 'px-6 py-3 text-lg', - }; - - return ( - - ); -}; diff --git a/apps/appkit-minter/src/core/components/common/index.ts b/apps/appkit-minter/src/core/components/common/index.ts index a19bc8d08..3689cf03a 100644 --- a/apps/appkit-minter/src/core/components/common/index.ts +++ b/apps/appkit-minter/src/core/components/common/index.ts @@ -6,5 +6,4 @@ * */ -export { Button } from './button'; export { Card } from './card'; diff --git a/apps/appkit-minter/src/core/components/index.ts b/apps/appkit-minter/src/core/components/index.ts index e32131718..f296014b8 100644 --- a/apps/appkit-minter/src/core/components/index.ts +++ b/apps/appkit-minter/src/core/components/index.ts @@ -7,7 +7,7 @@ */ // Common components -export { Button, Card } from './common'; +export { Card } from './common'; export { ToasterProvider } from './common/toaster-provider'; // Layout components diff --git a/apps/appkit-minter/src/core/components/layout/app-router.tsx b/apps/appkit-minter/src/core/components/layout/app-router.tsx index 26babde0f..2fe49ef25 100644 --- a/apps/appkit-minter/src/core/components/layout/app-router.tsx +++ b/apps/appkit-minter/src/core/components/layout/app-router.tsx @@ -11,7 +11,7 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { useWatchBalance, useWatchTransactions, useWatchJettons } from '@ton/appkit-react'; import { toast } from 'sonner'; -import { MinterPage } from '@/pages'; +import { MinterPage, SwapPage } from '@/pages'; export const AppRouter: React.FC = () => { // Enable global real-time balance updates @@ -50,6 +50,7 @@ export const AppRouter: React.FC = () => { } /> + } /> } /> diff --git a/apps/appkit-minter/src/core/components/layout/layout.tsx b/apps/appkit-minter/src/core/components/layout/layout.tsx index 22a88bcd6..9b45d3ce3 100644 --- a/apps/appkit-minter/src/core/components/layout/layout.tsx +++ b/apps/appkit-minter/src/core/components/layout/layout.tsx @@ -9,6 +9,7 @@ import type React from 'react'; import { TonConnectButton } from '@ton/appkit-react'; import { Layers } from 'lucide-react'; +import { Link } from 'react-router-dom'; import { ThemeSwitcher } from './theme-switcher'; @@ -16,20 +17,19 @@ import { NetworkPicker } from '@/features/network'; interface LayoutProps { children: React.ReactNode; - title?: string; } -export const Layout: React.FC = ({ children, title = 'NFT Minter' }) => { +export const Layout: React.FC = ({ children }) => { return (
-
-
+ +
-

{title}

-
+

NFT Minter

+
diff --git a/apps/appkit-minter/src/core/components/layout/theme-switcher.tsx b/apps/appkit-minter/src/core/components/layout/theme-switcher.tsx index 30d315453..d4cc987be 100644 --- a/apps/appkit-minter/src/core/components/layout/theme-switcher.tsx +++ b/apps/appkit-minter/src/core/components/layout/theme-switcher.tsx @@ -7,6 +7,7 @@ */ import { Moon, Sun } from 'lucide-react'; +import { Button } from '@ton/appkit-react'; import { useTheme } from '@/core/hooks'; @@ -14,12 +15,9 @@ export function ThemeSwitcher() { const { theme, setTheme } = useTheme(); return ( - + ); } diff --git a/apps/appkit-minter/src/core/hooks/use-theme.ts b/apps/appkit-minter/src/core/hooks/use-theme.ts index 32d634aef..e6d9a98ff 100644 --- a/apps/appkit-minter/src/core/hooks/use-theme.ts +++ b/apps/appkit-minter/src/core/hooks/use-theme.ts @@ -8,17 +8,24 @@ import { useContext, useEffect } from 'react'; import { useAppKitTheme } from '@ton/appkit-react'; +import { useTonConnectUI, THEME } from '@tonconnect/ui-react'; import { ThemeProviderContext } from '@/core/components/layout/theme-provider'; export const useTheme = () => { const context = useContext(ThemeProviderContext); const [, setTheme] = useAppKitTheme(); + const [tonconnect] = useTonConnectUI(); if (context === undefined) throw new Error('useTheme must be used within a ThemeProvider'); useEffect(() => { setTheme(context.theme); + tonconnect.uiOptions = { + uiPreferences: { + theme: context.theme === 'dark' ? THEME.DARK : THEME.LIGHT, + }, + }; }, [context.theme]); return context; diff --git a/apps/appkit-minter/src/core/styles/app.css b/apps/appkit-minter/src/core/styles/app.css deleted file mode 100644 index d631e3f75..000000000 --- a/apps/appkit-minter/src/core/styles/app.css +++ /dev/null @@ -1,189 +0,0 @@ -@import "tailwindcss"; - -@import "tw-animate-css"; - -@custom-variant dark (&:is(.dark *)); - -#root { - width: 100%; -} - -/* Rarity glow effects */ -.rarity-common { - --rarity-color: #9ca3af; - --rarity-glow: rgba(156, 163, 175, 0.3); -} - -.rarity-rare { - --rarity-color: #3b82f6; - --rarity-glow: rgba(59, 130, 246, 0.4); -} - -.rarity-epic { - --rarity-color: #8b5cf6; - --rarity-glow: rgba(139, 92, 246, 0.5); -} - -.rarity-legendary { - --rarity-color: #f59e0b; - --rarity-glow: rgba(245, 158, 11, 0.6); -} - -/* Card glow animation */ -.card-glow { - box-shadow: 0 0 20px var(--rarity-glow), 0 0 40px var(--rarity-glow); -} - -.card-glow-legendary { - animation: legendary-glow 2s ease-in-out infinite; -} - -@keyframes legendary-glow { - 0%, 100% { - box-shadow: 0 0 20px rgba(245, 158, 11, 0.6), 0 0 40px rgba(245, 158, 11, 0.4), 0 0 60px rgba(245, 158, 11, 0.2); - } - 50% { - box-shadow: 0 0 30px rgba(245, 158, 11, 0.8), 0 0 60px rgba(245, 158, 11, 0.6), 0 0 90px rgba(245, 158, 11, 0.4); - } -} - -/* Shimmer effect for legendary cards */ -.shimmer-overlay { - background: linear-gradient( - 90deg, - transparent 0%, - rgba(255, 255, 255, 0.2) 50%, - transparent 100% - ); - background-size: 200% 100%; - animation: shimmer 2s linear infinite; -} - -@keyframes shimmer { - 0% { - background-position: -200% 0; - } - 100% { - background-position: 200% 0; - } -} - -@theme inline { - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --font-sans: 'Inter Variable', sans-serif; - --color-sidebar-ring: var(--sidebar-ring); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar: var(--sidebar); - --color-chart-5: var(--chart-5); - --color-chart-4: var(--chart-4); - --color-chart-3: var(--chart-3); - --color-chart-2: var(--chart-2); - --color-chart-1: var(--chart-1); - --radius-2xl: calc(var(--radius) + 8px); - --radius-3xl: calc(var(--radius) + 12px); - --radius-4xl: calc(var(--radius) + 16px); -} - -:root { - --radius: 0.625rem; - --card: oklch(1 0 0); - --card-foreground: oklch(0.147 0.004 49.25); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.147 0.004 49.25); - --primary: oklch(0.6543 0.1605 243.75); - --primary-foreground: oklch(0.98 0.02 201); - --secondary: oklch(0.967 0.001 286.375); - --secondary-foreground: oklch(0.21 0.006 285.885); - --muted: oklch(0.97 0.001 106.424); - --muted-foreground: oklch(0.553 0.013 58.071); - --accent: oklch(0.6543 0.1605 243.75); - --accent-foreground: oklch(0.98 0.02 201); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.923 0.003 48.717); - --input: oklch(0.923 0.003 48.717); - --ring: oklch(0.709 0.01 56.259); - --background: oklch(1 0 0); - --foreground: oklch(0.147 0.004 49.25); - --chart-1: oklch(0.87 0.12 207); - --chart-2: oklch(0.80 0.13 212); - --chart-3: oklch(0.71 0.13 215); - --chart-4: oklch(0.6543 0.1605 243.75); - --chart-5: oklch(0.52 0.09 223); - --sidebar: oklch(0.985 0.001 106.423); - --sidebar-foreground: oklch(0.147 0.004 49.25); - --sidebar-primary: oklch(0.6543 0.1605 243.75); - --sidebar-primary-foreground: oklch(0.98 0.02 201); - --sidebar-accent: oklch(0.6543 0.1605 243.75); - --sidebar-accent-foreground: oklch(0.98 0.02 201); - --sidebar-border: oklch(0.923 0.003 48.717); - --sidebar-ring: oklch(0.709 0.01 56.259); -} - -.dark { - --background: oklch(0.147 0.004 49.25); - --foreground: oklch(0.985 0.001 106.423); - --card: oklch(0.216 0.006 56.043); - --card-foreground: oklch(0.985 0.001 106.423); - --popover: oklch(0.216 0.006 56.043); - --popover-foreground: oklch(0.985 0.001 106.423); - --primary: oklch(0.71 0.13 215); - --primary-foreground: oklch(0.30 0.05 230); - --secondary: oklch(0.274 0.006 286.033); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.268 0.007 34.298); - --muted-foreground: oklch(0.709 0.01 56.259); - --accent: oklch(0.71 0.13 215); - --accent-foreground: oklch(0.30 0.05 230); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.553 0.013 58.071); - --chart-1: oklch(0.87 0.12 207); - --chart-2: oklch(0.80 0.13 212); - --chart-3: oklch(0.71 0.13 215); - --chart-4: oklch(0.6543 0.1605 243.75); - --chart-5: oklch(0.52 0.09 223); - --sidebar: oklch(0.216 0.006 56.043); - --sidebar-foreground: oklch(0.985 0.001 106.423); - --sidebar-primary: oklch(0.80 0.13 212); - --sidebar-primary-foreground: oklch(0.30 0.05 230); - --sidebar-accent: oklch(0.71 0.13 215); - --sidebar-accent-foreground: oklch(0.30 0.05 230); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.553 0.013 58.071); -} - -@layer base { - * { - @apply border-border outline-ring/50; - } - body { - @apply bg-background text-foreground; - } -} diff --git a/apps/appkit-minter/src/core/styles/index.css b/apps/appkit-minter/src/core/styles/index.css index 509ca420e..18d2ff5e9 100644 --- a/apps/appkit-minter/src/core/styles/index.css +++ b/apps/appkit-minter/src/core/styles/index.css @@ -1,5 +1,12 @@ +@import '@ton/appkit-react/styles.css'; + +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + :root { - font-family: 'Inter', system-ui, Avenir, Helvetica, Arial, sans-serif; + font-family: var(--font-sans); line-height: 1.5; font-weight: 400; @@ -9,13 +16,166 @@ text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + + --font-sans: "Inter", system-ui, Avenir, Helvetica, Arial, sans-serif; + --radius: 0.625rem; + + --card: oklch(1 0 0); + --card-foreground: oklch(0.147 0.004 49.25); + --primary: oklch(0.71 0.13 215); + --primary-foreground: oklch(0.98 0.02 201); + --secondary: oklch(97.893% 0.00298 264.796); + --secondary-foreground: oklch(57.568% 0.01372 285.899); + --muted: oklch(93.014% 0.00759 260.883); + --muted-foreground: oklch(66.646% 0.0147 285.926); + --destructive: oklch(0.577 0.245 27.325); + --success: oklch(24.818% 0.0376 145.737); + --success-foreground: oklch(1 0 0); + --border: oklch(0.923 0.003 48.717); + --background: oklch(1 0 0); + --foreground: oklch(0.147 0.004 49.25); } -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; +.dark { + --background: oklch(0.147 0.004 49.25); + --foreground: oklch(0.985 0.001 106.423); + --card: oklch(0.216 0.006 56.043); + --card-foreground: oklch(0.985 0.001 106.423); + --primary: oklch(0.71 0.13 215); + --primary-foreground: oklch(0.30 0.05 230); + --secondary: oklch(23.503% 0.00003 271.152); + --secondary-foreground: oklch(98.511% 0.00011 271.152); + --muted: oklch(0.2931 0 0); + --muted-foreground: oklch(57.568% 0.01372 285.899); + --destructive: oklch(0.704 0.191 22.216); + --success: oklch(0.75 0.18 144); + --success-foreground: oklch(0 0 0); + --border: oklch(1 0 0 / 10%); +} + +#root { width: 100%; - min-height: 100vh; +} + +* { + --ta-color-primary: var(--primary); + --ta-color-primary-foreground: var(--primary-foreground); + --ta-color-error: var(--destructive); + --ta-color-success: var(--success); + --ta-color-text: var(--foreground); + --ta-color-text-secondary: var(--muted-foreground); + --ta-color-background: var(--background); + --ta-color-background-secondary: var(--secondary); + --ta-color-background-tertiary: var(--muted); + + --ta-font-family: var(--font-sans); + --ta-border-radius-s: var(--radius-sm); + --ta-border-radius-m: var(--radius-md); + --ta-border-radius-l: var(--radius-lg); + --ta-border-radius-xl: var(--radius-xl); + --ta-border-radius-2xl: var(--radius-2xl); +} + +/* Rarity glow effects */ +.rarity-common { + --rarity-color: #9ca3af; + --rarity-glow: rgba(156, 163, 175, 0.3); +} + +.rarity-rare { + --rarity-color: #3b82f6; + --rarity-glow: rgba(59, 130, 246, 0.4); +} + +.rarity-epic { + --rarity-color: #8b5cf6; + --rarity-glow: rgba(139, 92, 246, 0.5); +} + +.rarity-legendary { + --rarity-color: #f59e0b; + --rarity-glow: rgba(245, 158, 11, 0.6); +} + +/* Card glow animation */ +.card-glow { + box-shadow: 0 0 20px var(--rarity-glow), 0 0 40px var(--rarity-glow); +} + +.card-glow-legendary { + animation: legendary-glow 2s ease-in-out infinite; +} + +@keyframes legendary-glow { + 0%, + 100% { + box-shadow: 0 0 20px rgba(245, 158, 11, 0.6), 0 0 40px rgba(245, 158, 11, 0.4), + 0 0 60px rgba(245, 158, 11, 0.2); + } + 50% { + box-shadow: 0 0 30px rgba(245, 158, 11, 0.8), 0 0 60px rgba(245, 158, 11, 0.6), + 0 0 90px rgba(245, 158, 11, 0.4); + } +} + +/* Shimmer effect for legendary cards */ +.shimmer-overlay { + background: linear-gradient( + 90deg, + transparent 0%, + rgba(255, 255, 255, 0.2) 50%, + transparent 100% + ); + background-size: 200% 100%; + animation: shimmer 2s linear infinite; +} + +@keyframes shimmer { + 0% { + background-position: -200% 0; + } + 100% { + background-position: 200% 0; + } +} + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-destructive: var(--destructive); + --color-success: var(--success); + --color-success-foreground: var(--success-foreground); + --color-border: var(--border); + --font-sans: "Inter", sans-serif; + --radius-2xl: calc(var(--radius) + 8px); + --radius-3xl: calc(var(--radius) + 12px); + --radius-4xl: calc(var(--radius) + 16px); +} + +@layer base { + * { + @apply border-border outline-none; + } + + body { + @apply bg-background text-foreground; + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + width: 100%; + min-height: 100vh; + } } diff --git a/apps/appkit-minter/src/features/balances/components/token-transfer-modal.tsx b/apps/appkit-minter/src/features/balances/components/token-transfer-modal.tsx index 1b69cef37..21fe3fbaa 100644 --- a/apps/appkit-minter/src/features/balances/components/token-transfer-modal.tsx +++ b/apps/appkit-minter/src/features/balances/components/token-transfer-modal.tsx @@ -9,10 +9,9 @@ import React, { useMemo, useState } from 'react'; import type { Jetton } from '@ton/appkit'; import { getFormattedJettonInfo, getErrorMessage } from '@ton/appkit'; -import { SendTonButton, SendJettonButton } from '@ton/appkit-react'; -import { Gem, X } from 'lucide-react'; +import { SendTonButton, SendJettonButton, Button, Input, Modal } from '@ton/appkit-react'; +import { Gem } from 'lucide-react'; -import { Button } from '@/core/components'; import { TransactionStatus } from '@/features/transaction'; interface TokenTransferModalProps { @@ -73,155 +72,143 @@ export const TokenTransferModal: React.FC = ({ onClose(); }; - if (!isOpen || !tokenInfo.decimals) return null; + if (!tokenInfo.decimals) return null; return ( -
-
-
-
-
-
- {tokenInfo.image ? ( - {tokenInfo.name} - ) : tokenType === 'TON' ? ( - - ) : ( - - {tokenInfo.symbol?.slice(0, 2)} - - )} -
-
-

Transfer {tokenInfo.name}

-

- Balance: {tokenInfo.balance} {tokenInfo.symbol} -

-
-
- -
- - {txBoc ? ( -
- - -
+ !open && handleClose()}> +
+
+ {tokenInfo.image ? ( + {tokenInfo.name} + ) : tokenType === 'TON' ? ( + ) : ( - <> -
-
- - setRecipientAddress(e.target.value)} - placeholder="Enter TON address" - className="w-full px-3 py-2 bg-input border border-border rounded-lg focus:ring-2 focus:ring-ring focus:border-ring text-sm text-foreground placeholder:text-muted-foreground" - /> -
- -
- - setAmount(e.target.value)} - placeholder="0.00" - step="any" - min="0" - className="w-full px-3 py-2 bg-input border border-border rounded-lg focus:ring-2 focus:ring-ring focus:border-ring text-sm text-foreground placeholder:text-muted-foreground" - /> -
- -
- - setComment(e.target.value)} - placeholder="Add a comment" - className="w-full px-3 py-2 bg-input border border-border rounded-lg focus:ring-2 focus:ring-ring focus:border-ring text-sm text-foreground placeholder:text-muted-foreground" - /> -
- - {transferError && ( -
-

{transferError}

-
- )} + {tokenInfo.symbol?.slice(0, 2)} + )} +
+
+

Available Balance

+

+ {tokenInfo.balance} {tokenInfo.symbol} +

+
+
+ + {txBoc ? ( +
+ + +
+ ) : ( + <> +
+ + + Recipient Address + + + setRecipientAddress(e.target.value)} + placeholder="Enter TON address" + /> + + + + + + Amount + + + setAmount(e.target.value)} + placeholder="0.00" + step="any" + min="0" + /> + + + + + + Comment (optional) + + + setComment(e.target.value)} + placeholder="Add a comment" + /> + + + + {transferError && ( +
+

{transferError}

+ )} +
-
- {tokenType === 'TON' && ( - setTransferError(getErrorMessage(error))} - onSuccess={(data) => setTxBoc(data.boc)} +
+ {tokenType === 'TON' && ( + setTransferError(getErrorMessage(error))} + onSuccess={(data) => setTxBoc(data.boc)} + > + {({ isLoading, onSubmit, disabled, text }) => ( + - )} - + {text} + )} - - {tokenType === 'JETTON' && jetton?.address && ( - setTransferError(getErrorMessage(error))} - onSuccess={(data) => setTxBoc(data.boc)} + + )} + + {tokenType === 'JETTON' && jetton?.address && ( + setTransferError(getErrorMessage(error))} + onSuccess={(data) => setTxBoc(data.boc)} + > + {({ isLoading, onSubmit, disabled, text }) => ( + - )} - + {text} + )} + + )} - -
- - )} -
-
-
+ +
+ + )} + ); }; diff --git a/apps/appkit-minter/src/features/balances/components/tokens-card.tsx b/apps/appkit-minter/src/features/balances/components/tokens-card.tsx index c64afec63..577f1e394 100644 --- a/apps/appkit-minter/src/features/balances/components/tokens-card.tsx +++ b/apps/appkit-minter/src/features/balances/components/tokens-card.tsx @@ -12,10 +12,12 @@ import type { Jetton } from '@ton/appkit'; import { getFormattedJettonInfo } from '@ton/appkit'; import { CurrencyItem, useJettons, useBalance } from '@ton/appkit-react'; import { AlertCircle } from 'lucide-react'; +import { Button } from '@ton/appkit-react'; +import { Link } from 'react-router-dom'; import { TokenTransferModal } from './token-transfer-modal'; -import { Card, Button } from '@/core/components'; +import { Card } from '@/core/components'; interface SelectedToken { type: 'TON' | 'JETTON'; @@ -55,7 +57,7 @@ export const TokensCard: FC> = (props) => {

Failed to load balances

-
@@ -74,26 +76,32 @@ export const TokensCard: FC> = (props) => { ) : (
{/* Summary */} -
-

+

+

{totalTokens} {totalTokens === 1 ? 'Asset' : 'Assets'}

- + + +
{/* Token List */}
- setSelectedToken({ type: 'TON' })} - icon="./ton.png" - isVerified - /> +
+ setSelectedToken({ type: 'TON' })} + icon="./ton.png" + isVerified + /> +
{/* Jettons */} {jettons.map((jetton) => { @@ -104,16 +112,17 @@ export const TokensCard: FC> = (props) => { } return ( - setSelectedToken({ type: 'JETTON', jetton })} - /> +
+ setSelectedToken({ type: 'JETTON', jetton })} + /> +
); })}
diff --git a/apps/appkit-minter/src/features/mint/components/card-generator.tsx b/apps/appkit-minter/src/features/mint/components/card-generator.tsx index 652d26443..90280e948 100644 --- a/apps/appkit-minter/src/features/mint/components/card-generator.tsx +++ b/apps/appkit-minter/src/features/mint/components/card-generator.tsx @@ -12,6 +12,7 @@ import { Sparkles, Coins, AlertCircle } from 'lucide-react'; import { useSelectedWallet, Send } from '@ton/appkit-react'; import { getErrorMessage } from '@ton/appkit'; import { toast } from 'sonner'; +import { Button } from '@ton/appkit-react'; import { CardPreview } from './card-preview'; import { useCardGenerator } from '../hooks/use-card-generator'; @@ -19,7 +20,7 @@ import { useNftMintTransaction } from '../hooks/use-nft-mint-transaction'; import { mintCard } from '../store/actions/mint-card'; import { setMintError } from '../store/actions/set-mint-error'; -import { Button, Card } from '@/core/components'; +import { Card } from '@/core/components'; interface CardGeneratorProps { className?: string; @@ -86,8 +87,12 @@ export const CardGenerator: React.FC = ({ className }) => { {/* Action buttons */}
- @@ -111,11 +116,11 @@ export const CardGenerator: React.FC = ({ className }) => { )} diff --git a/apps/appkit-minter/src/features/nft/components/nft-transfer-modal.tsx b/apps/appkit-minter/src/features/nft/components/nft-transfer-modal.tsx index cab3cb848..d925371e0 100644 --- a/apps/appkit-minter/src/features/nft/components/nft-transfer-modal.tsx +++ b/apps/appkit-minter/src/features/nft/components/nft-transfer-modal.tsx @@ -9,11 +9,9 @@ import React, { useCallback, useMemo, useState } from 'react'; import type { NFT } from '@ton/appkit'; import { getFormattedNftInfo, createTransferNftTransaction, getErrorMessage } from '@ton/appkit'; -import { Send, useAppKit } from '@ton/appkit-react'; +import { Send, useAppKit, Button, Input, Modal } from '@ton/appkit-react'; import { toast } from 'sonner'; -import { X, Image as ImageIcon } from 'lucide-react'; - -import { Button } from '@/core/components'; +import { Image as ImageIcon } from 'lucide-react'; interface NftTransferModalProps { nft: NFT; @@ -45,94 +43,81 @@ export const NftTransferModal: React.FC = ({ nft, isOpen, onClose(); }; - if (!isOpen) return null; - return ( -
-
-
-
-

Transfer NFT

- -
- - {/* NFT Preview */} -
-
- {nftInfo.image ? ( - {nftInfo.name} - ) : ( - - )} -
-

{nftInfo.name}

-

{nftInfo.collectionName}

- {nftInfo.description && ( -

{nftInfo.description}

- )} -
- -
-
- - setRecipientAddress(e.target.value)} - placeholder="Enter TON address" - className="w-full px-3 py-2 bg-input border border-border rounded-lg focus:ring-2 focus:ring-ring focus:border-ring text-sm text-foreground placeholder:text-muted-foreground" - /> -
- -
- - setComment(e.target.value)} - placeholder="Add a comment" - className="w-full px-3 py-2 bg-input border border-border rounded-lg focus:ring-2 focus:ring-ring focus:border-ring text-sm text-foreground placeholder:text-muted-foreground" - /> -
+ !open && handleClose()}> + {/* NFT Preview */} +
+
+ {nftInfo.image ? ( + {nftInfo.name} + ) : ( + + )} +
+

{nftInfo.name}

+

{nftInfo.collectionName}

+ {nftInfo.description &&

{nftInfo.description}

} +
- {transferError && ( -
-

{transferError}

-
- )} +
+ + + Recipient Address + + + setRecipientAddress(e.target.value)} + placeholder="Enter TON address" + /> + + + + + + Comment (optional) + + + setComment(e.target.value)} + placeholder="Add a comment" + /> + + + + {transferError && ( +
+

{transferError}

+ )} +
-
- { - handleClose(); - toast.success('NFT transferred successfully'); - }} - onError={(error: Error) => { - setTransferError(getErrorMessage(error)); - }} - disabled={!recipientAddress} - > - {({ isLoading, onSubmit, disabled, text }) => ( - - )} - - - -
-
+ )} + + +
-
+ ); }; diff --git a/apps/appkit-minter/src/features/nft/components/nfts-card.tsx b/apps/appkit-minter/src/features/nft/components/nfts-card.tsx index 7534dda2b..8bb1fdd7a 100644 --- a/apps/appkit-minter/src/features/nft/components/nfts-card.tsx +++ b/apps/appkit-minter/src/features/nft/components/nfts-card.tsx @@ -11,10 +11,11 @@ import type { FC, ComponentProps } from 'react'; import type { NFT } from '@ton/appkit'; import { NftItem, useNfts } from '@ton/appkit-react'; import { AlertCircle, Image as ImageIcon } from 'lucide-react'; +import { Button } from '@ton/appkit-react'; import { NftTransferModal } from './nft-transfer-modal'; -import { Card, Button } from '@/core/components'; +import { Card } from '@/core/components'; export const NftsCard: FC> = (props) => { const [selectedNft, setSelectedNft] = useState(null); @@ -38,7 +39,7 @@ export const NftsCard: FC> = (props) => {

Failed to load NFTs

-
@@ -69,7 +70,7 @@ export const NftsCard: FC> = (props) => {

{nfts.length} {nfts.length === 1 ? 'NFT' : 'NFTs'}

-
diff --git a/apps/appkit-minter/src/features/signing/components/sign-message-card.tsx b/apps/appkit-minter/src/features/signing/components/sign-message-card.tsx index fb9a29eed..2672756e1 100644 --- a/apps/appkit-minter/src/features/signing/components/sign-message-card.tsx +++ b/apps/appkit-minter/src/features/signing/components/sign-message-card.tsx @@ -10,8 +10,9 @@ import { useState } from 'react'; import type { FC, ComponentProps } from 'react'; import { useSignText, useSelectedWallet } from '@ton/appkit-react'; import { toast } from 'sonner'; +import { Button } from '@ton/appkit-react'; -import { Card, Button } from '@/core/components'; +import { Card } from '@/core/components'; export const SignMessageCard: FC> = (props) => { const [message, setMessage] = useState(''); @@ -64,10 +65,11 @@ export const SignMessageCard: FC> = (props) => { {/* Sign Button */} @@ -79,7 +81,7 @@ export const SignMessageCard: FC> = (props) => {
{signature}
-
diff --git a/apps/appkit-minter/src/features/staking/components/stake-button.tsx b/apps/appkit-minter/src/features/staking/components/stake-button.tsx index 0a0baa5e0..d23e13fcc 100644 --- a/apps/appkit-minter/src/features/staking/components/stake-button.tsx +++ b/apps/appkit-minter/src/features/staking/components/stake-button.tsx @@ -81,10 +81,12 @@ export const StakeButton: FC = ({ return ( ); }; diff --git a/apps/appkit-minter/src/features/staking/components/staking-card.tsx b/apps/appkit-minter/src/features/staking/components/staking-card.tsx index dfb9f5b2f..57efeb247 100644 --- a/apps/appkit-minter/src/features/staking/components/staking-card.tsx +++ b/apps/appkit-minter/src/features/staking/components/staking-card.tsx @@ -8,13 +8,12 @@ import { useMemo, useState } from 'react'; import type { FC } from 'react'; -import { UnstakeMode, useAddress, useBalance, useStakedBalance } from '@ton/appkit-react'; +import { UnstakeMode, useAddress, useBalance, useStakedBalance, Input, Button } from '@ton/appkit-react'; import type { UnstakeModes } from '@ton/appkit-react'; import { StakeButton } from './stake-button'; import { Card } from '@/core/components'; -import { cn } from '@/core/lib/utils'; const balanceAmountFormatter = new Intl.NumberFormat(undefined, { maximumFractionDigits: 20, @@ -113,26 +112,25 @@ export const StakingCard: FC = () => {
-
- - setAmountInput(e.target.value)} - placeholder="Default: 1" - step="any" - min="0" - className="w-full rounded-lg border border-border bg-input px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-ring focus:ring-2 focus:ring-ring" - /> + + + Amount (optional) + + + setAmountInput(e.target.value)} + placeholder="Default: 1" + step="any" + min="0" + /> + {amountInvalid ? ( -

- Enter a positive number or leave empty to use 1. -

+ Enter a positive number or leave empty to use 1. ) : null} -
+
Tonstakers:
@@ -141,30 +139,28 @@ export const StakingCard: FC = () => { Unstake mode
{UNSTAKE_MODE_OPTIONS.map(({ mode, label }) => ( - + ))}
+

{UNSTAKE_MODE_OPTIONS.find((o) => o.mode === unstakeMode)?.hint}

+
diff --git a/apps/appkit-minter/src/features/swap/components/swap-button.tsx b/apps/appkit-minter/src/features/swap/components/swap-button.tsx deleted file mode 100644 index 58afd4145..000000000 --- a/apps/appkit-minter/src/features/swap/components/swap-button.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Copyright (c) TonTech. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import { useMemo } from 'react'; -import type { FC } from 'react'; -import { Send, useSwapQuote, useNetwork, useAddress, useBuildSwapTransaction } from '@ton/appkit-react'; - -const USDT_ADDRESS = 'EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs'; -const TON = { address: 'ton', decimals: 9, symbol: 'TON' }; -const USDT = { address: USDT_ADDRESS, decimals: 6, symbol: 'USDT' }; - -interface SwapButtonProps { - amount: string; - direction: 'from' | 'to'; - providerId?: string; -} - -export const SwapButton: FC = ({ amount, direction, providerId }) => { - const network = useNetwork(); - const address = useAddress(); - const from = direction === 'from' ? TON : USDT; - const to = direction === 'to' ? TON : USDT; - const { - data: quote, - isError, - isLoading, - } = useSwapQuote({ - amount, - from, - to, - network, - slippageBps: 100, - providerId, - }); - - const { mutateAsync: buildSwapTransaction } = useBuildSwapTransaction(); - - const handleBuildSwapTransaction = () => { - if (!quote || !address) { - return Promise.reject(new Error('Missing quote or address')); - } - - return buildSwapTransaction({ - quote, - userAddress: address, - }); - }; - - const buttonText = useMemo(() => { - if (isLoading) { - return 'Fetching quote...'; - } - - if (isError || !quote) { - return 'Swap Unavailable'; - } - - return `Swap ${quote.fromAmount} ${from.symbol} -> ${quote.toAmount} ${to.symbol}`; - }, [isLoading, isError, quote]); - - return ; -}; diff --git a/apps/appkit-minter/src/features/wallet/components/wallet-info.tsx b/apps/appkit-minter/src/features/wallet/components/wallet-info.tsx index 4f2928385..49cc443c9 100644 --- a/apps/appkit-minter/src/features/wallet/components/wallet-info.tsx +++ b/apps/appkit-minter/src/features/wallet/components/wallet-info.tsx @@ -8,7 +8,7 @@ import { useState, useCallback } from 'react'; import type { ComponentProps, FC } from 'react'; -import { useSelectedWallet, Network } from '@ton/appkit-react'; +import { useSelectedWallet, Network, Button } from '@ton/appkit-react'; import { Wallet, Check, Copy } from 'lucide-react'; import { Card } from '@/core/components'; @@ -67,12 +67,7 @@ export const WalletInfo: FC> = (props) => {
{wallet && ( - + )} diff --git a/apps/appkit-minter/src/main.tsx b/apps/appkit-minter/src/main.tsx index 420ea6f7f..04bb2bcfc 100644 --- a/apps/appkit-minter/src/main.tsx +++ b/apps/appkit-minter/src/main.tsx @@ -13,8 +13,6 @@ import { createRoot } from 'react-dom/client'; import { App } from './app'; -import './core/styles/index.css'; - createRoot(document.getElementById('root')!).render( diff --git a/apps/appkit-minter/src/pages/index.ts b/apps/appkit-minter/src/pages/index.ts index d55767204..ba95a7d75 100644 --- a/apps/appkit-minter/src/pages/index.ts +++ b/apps/appkit-minter/src/pages/index.ts @@ -7,3 +7,4 @@ */ export { MinterPage } from './minter-page'; +export { SwapPage } from './swap-page'; diff --git a/apps/appkit-minter/src/pages/minter-page.tsx b/apps/appkit-minter/src/pages/minter-page.tsx index 429683429..bc6b18e4b 100644 --- a/apps/appkit-minter/src/pages/minter-page.tsx +++ b/apps/appkit-minter/src/pages/minter-page.tsx @@ -13,8 +13,7 @@ import { TokensCard } from '@/features/balances'; import { CardGenerator } from '@/features/mint'; import { NftsCard } from '@/features/nft'; import { WalletInfo } from '@/features/wallet'; -import { Card, Layout } from '@/core/components'; -import { SwapButton } from '@/features/swap'; +import { Layout } from '@/core/components'; import { StakingCard } from '@/features/staking'; import { SignMessageCard } from '@/features/signing'; @@ -23,7 +22,7 @@ export const MinterPage: React.FC = () => { const isConnected = !!wallet; return ( - +
@@ -36,22 +35,6 @@ export const MinterPage: React.FC = () => { - -
-
Default provider:
- - - -
StonFi provider:
- - - -
DeDust provider:
- - -
-
-
)} diff --git a/apps/appkit-minter/src/pages/swap-page.tsx b/apps/appkit-minter/src/pages/swap-page.tsx new file mode 100644 index 000000000..0f680b0b1 --- /dev/null +++ b/apps/appkit-minter/src/pages/swap-page.tsx @@ -0,0 +1,98 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type React from 'react'; +import { Network } from '@ton/appkit'; +import { SwapWidget } from '@ton/appkit-react'; +import type { AppkitUIToken } from '@ton/appkit-react'; + +import { Card, Layout } from '@/core/components'; + +const TOKENS: AppkitUIToken[] = [ + { + symbol: 'TON', + name: 'Toncoin', + decimals: 9, + address: 'ton', + logo: 'https://asset.ston.fi/img/EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c/c8d21a3d93f9b574381e0a8d8f16d48b325dd8f54ce172f599c1e9d6c62f03f7', + }, + { + symbol: 'USDâ‚®', + name: 'Tether USD', + decimals: 6, + address: 'UQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_p0p', + rate: '1', + logo: 'https://asset.ston.fi/img/EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs/1a87edfee9a28b05578853952e5effb8cc30af1e0fb90043aa2ce19dce490849', + }, + { + symbol: 'STON', + name: 'STON', + decimals: 9, + address: 'UQA2kCVNwVsil2EM2mB0SkXytxCqQjS4mttjDpnXmwG9T_sL', + logo: 'https://asset.ston.fi/img/EQA2kCVNwVsil2EM2mB0SkXytxCqQjS4mttjDpnXmwG9T6bO/7c9798ce1e64707fb4cb8f025d4060f66b386ed381b50498e3b88731cedeffe8', + }, + { + symbol: 'XAUt0', + name: 'Tether Gold', + decimals: 6, + address: 'UQA1R_LuQCLHlMgOo1S4G7Y7W1cd0FrAkbA10Zq7rddKxnKh', + logo: 'https://asset.ston.fi/img/EQA1R_LuQCLHlMgOo1S4G7Y7W1cd0FrAkbA10Zq7rddKxi9k/4aaaa7c30d7811bced81ded6bc116dcc82a78c6aea53d6012fd586a5826963ad', + }, + { + symbol: 'USDe', + name: 'Ethena USDe', + decimals: 6, + address: 'UQAIb6KmdfdDR7CN1GBqVJuP25iCnLKCvBlJ07Evuu2dzKOa', + rate: '1', + logo: 'https://asset.ston.fi/img/EQAIb6KmdfdDR7CN1GBqVJuP25iCnLKCvBlJ07Evuu2dzP5f/dbcc67993cd4aad4845a97a4a9722c6cb618123997c8112c29d4932b2739c4cd', + }, + { + symbol: 'tsTON', + name: 'Tonstakers TON', + decimals: 9, + address: 'UQC98_qAmNEptUtPc7W6xdHh_ZHrBUFpw5Ft_IzNU20QAMtq', + logo: 'https://asset.ston.fi/img/EQC98_qAmNEptUtPc7W6xdHh_ZHrBUFpw5Ft_IzNU20QAJav/38f530facb209e4696b8aef17af51df94d16bd879926c517b07d25841da287b7', + }, + { + symbol: 'GEMSTON', + name: 'GEMSTON', + decimals: 9, + address: 'UQBX6K9aXVl3nXINCyPPL86C4ONVmQ8vK360u6dykFKXpC1f', + logo: 'https://asset.ston.fi/img/EQBX6K9aXVl3nXINCyPPL86C4ONVmQ8vK360u6dykFKXpHCa/c6ab1e58e3b9b58a7429d38b7feab731afae2f66dc301a6c42041fdf7e9d7c9c', + }, + { + symbol: 'UTYA', + name: 'Utya', + decimals: 9, + address: 'UQBaCgUwOoc6gHCNln_oJzb0mVs79YG7wYoavh-o1Itanb8F', + logo: 'https://asset.ston.fi/img/EQBaCgUwOoc6gHCNln_oJzb0mVs79YG7wYoavh-o1ItaneLA/727e6cc971afdfa8ed9c698d0909eee9de344a0b6766ff5e4ddcc3323449d6f6', + }, + { + symbol: 'WETH', + name: 'Wrapped Ether', + decimals: 18, + address: 'UQBTkLAhEteZCRgRe_xMs5ZE0bMrduYxKbyzGCpXXW8dRT5W', + logo: 'https://asset.ston.fi/img/EQBTkLAhEteZCRgRe_xMs5ZE0bMrduYxKbyzGCpXXW8dRWOT/6267787665c30c2500dbde048e2f8a6a6d7ec58633ea038723f4ce1fab337ccb', + }, +]; + +export const SwapPage: React.FC = () => { + return ( + + + + + + ); +}; diff --git a/demo/examples/package.json b/demo/examples/package.json index 8d9eba234..dd02f0aed 100644 --- a/demo/examples/package.json +++ b/demo/examples/package.json @@ -10,6 +10,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@tanstack/react-query": "catalog:", "@ton/appkit": "workspace:*", "@ton/appkit-react": "workspace:*", "@ton/walletkit": "workspace:*", diff --git a/demo/examples/src/appkit/actions/swap/swap-actions.ts b/demo/examples/src/appkit/actions/swap/swap-actions.ts index ace40c84c..cd07d0b66 100644 --- a/demo/examples/src/appkit/actions/swap/swap-actions.ts +++ b/demo/examples/src/appkit/actions/swap/swap-actions.ts @@ -8,13 +8,31 @@ import type { AppKit } from '@ton/appkit'; import { Network } from '@ton/appkit'; -import { getSwapManager, getSwapQuote, buildSwapTransaction, sendTransaction } from '@ton/appkit'; +import { + getSwapManager, + getSwapProvider, + watchSwapProviders, + getSwapQuote, + buildSwapTransaction, + sendTransaction, +} from '@ton/appkit'; export const swapExample = async (appKit: AppKit) => { // SAMPLE_START: GET_SWAP_MANAGER const swapManager = getSwapManager(appKit); // SAMPLE_END: GET_SWAP_MANAGER + // SAMPLE_START: GET_SWAP_PROVIDER + const swapProvider = getSwapProvider(appKit, { id: 'stonfi' }); + // SAMPLE_END: GET_SWAP_PROVIDER + + // SAMPLE_START: WATCH_SWAP_PROVIDERS + const unsubscribe = watchSwapProviders(appKit, { + onChange: () => console.log('Swap providers updated'), + }); + unsubscribe(); + // SAMPLE_END: WATCH_SWAP_PROVIDERS + // SAMPLE_START: GET_SWAP_QUOTE const quote = await getSwapQuote(appKit, { from: { address: 'EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs', decimals: 6 }, @@ -35,5 +53,5 @@ export const swapExample = async (appKit: AppKit) => { console.log('Swap Transaction:', transactionResponse); // SAMPLE_END: BUILD_SWAP_TRANSACTION - return { swapManager, quote, transactionRequest }; + return { swapManager, swapProvider, quote, transactionRequest }; }; diff --git a/demo/examples/src/appkit/actions/swap/swap.test.ts b/demo/examples/src/appkit/actions/swap/swap.test.ts index b4a367d76..b0d5693b0 100644 --- a/demo/examples/src/appkit/actions/swap/swap.test.ts +++ b/demo/examples/src/appkit/actions/swap/swap.test.ts @@ -30,11 +30,15 @@ describe('Swap Actions Examples', () => { }, }); + // Mock SwapManager mockGetQuote = vi.fn(); mockBuildSwapTransaction = vi.fn(); mockSendTransaction = vi.fn(); - // Mock SwapManager + // @ts-expect-error - internal access + vi.spyOn(appKit.swapManager, 'getProvider').mockImplementation((id) => ({ + providerId: id || 'default', + })); // @ts-expect-error - internal access vi.spyOn(appKit.swapManager, 'getQuote').mockImplementation(mockGetQuote); // @ts-expect-error - internal access diff --git a/demo/examples/src/appkit/hooks/swap/swap.test.tsx b/demo/examples/src/appkit/hooks/swap/swap.test.tsx index 6a1e5ff47..b8f151623 100644 --- a/demo/examples/src/appkit/hooks/swap/swap.test.tsx +++ b/demo/examples/src/appkit/hooks/swap/swap.test.tsx @@ -12,6 +12,7 @@ import * as AppKitReact from '@ton/appkit-react'; import { UseSwapQuoteExample } from './use-swap-quote'; import { UseBuildSwapTransactionExample } from './use-build-swap-transaction'; +import { UseSwapProviderExample } from './use-swap-provider'; // Mock the whole module vi.mock('@ton/appkit-react', async () => { @@ -21,6 +22,7 @@ vi.mock('@ton/appkit-react', async () => { useSwapQuote: vi.fn(), useBuildSwapTransaction: vi.fn(), useSendTransaction: vi.fn(), + useSwapProvider: vi.fn(), }; }); @@ -71,6 +73,17 @@ describe('Swap Hooks Examples', () => { }); }); + describe('UseSwapProviderExample', () => { + it('should render swap provider', () => { + vi.mocked(AppKitReact.useSwapProvider).mockReturnValue({ + providerId: 'stonfi', + } as unknown as AppKitReact.UseSwapProviderReturnType); + + render(); + expect(screen.getByText('Result: stonfi')).toBeDefined(); + }); + }); + describe('UseBuildSwapTransactionExample', () => { it('should call buildTx and sendTx on button click', async () => { const mockQuote = { toAmount: '0.99' }; diff --git a/demo/examples/src/appkit/hooks/swap/use-swap-provider.tsx b/demo/examples/src/appkit/hooks/swap/use-swap-provider.tsx new file mode 100644 index 000000000..957bf97de --- /dev/null +++ b/demo/examples/src/appkit/hooks/swap/use-swap-provider.tsx @@ -0,0 +1,16 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useSwapProvider } from '@ton/appkit-react'; + +export const UseSwapProviderExample = () => { + // SAMPLE_START: USE_SWAP_PROVIDER + const provider = useSwapProvider({ id: 'stonfi' }); + return
Result: {provider ? provider.providerId : 'null'}
; + // SAMPLE_END: USE_SWAP_PROVIDER +}; diff --git a/package.json b/package.json index 9f3d166c7..fd9b2393e 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "quality:bridge": "turbo quality:bridge", "typecheck": "turbo typecheck", "e2e": "turbo e2e", - "clean": "git clean -xdf node_modules", + "clean": "rimraf node_modules", "clean:workspaces": "turbo run clean", "changeset": "changeset", "version-packages": "changeset version", diff --git a/packages/appkit-react/.storybook/app-kit.ts b/packages/appkit-react/.storybook/app-kit.ts new file mode 100644 index 000000000..c80dd823e --- /dev/null +++ b/packages/appkit-react/.storybook/app-kit.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { AppKit, Network } from '@ton/appkit'; +import { createTonConnectConnector } from '@ton/appkit'; +import { OmnistonSwapProvider } from '@ton/appkit/swap/omniston'; + +export const appKit = new AppKit({ + networks: { + [Network.mainnet().chainId]: { + apiClient: { + url: 'https://toncenter.com', + key: '25a9b2326a34b39a5fa4b264fb78fb4709e1bd576fc5e6b176639f5b71e94b0d', + }, + }, + [Network.testnet().chainId]: { + apiClient: { + url: 'https://testnet.toncenter.com', + key: 'd852b54d062f631565761042cccea87fa6337c41eb19b075e6c7fb88898a3992', + }, + }, + }, + connectors: [ + createTonConnectConnector({ + tonConnectOptions: { + manifestUrl: + 'https://raw.githubusercontent.com/ton-connect/demo-dapp-with-react-ui/master/public/tonconnect-manifest.json', + }, + }), + ], + providers: [new OmnistonSwapProvider()], +}); diff --git a/packages/appkit-react/.storybook/main.ts b/packages/appkit-react/.storybook/main.ts index 32c21bf0a..555fa6d21 100644 --- a/packages/appkit-react/.storybook/main.ts +++ b/packages/appkit-react/.storybook/main.ts @@ -6,14 +6,25 @@ * */ +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + import type { StorybookConfig } from '@storybook/react-vite'; import { nodePolyfills } from 'vite-plugin-node-polyfills'; const config: StorybookConfig = { stories: ['../src/**/*.stories.@(ts|tsx)'], - addons: ['@storybook/addon-docs'], + addons: [getAbsolutePath('@storybook/addon-docs')], + staticDirs: ['./public'], framework: { - name: '@storybook/react-vite', + name: getAbsolutePath('@storybook/react-vite'), options: {}, }, core: { @@ -55,3 +66,7 @@ const config: StorybookConfig = { }; export default config; + +function getAbsolutePath(value: string) { + return dirname(fileURLToPath(import.meta.resolve(`${value}/package.json`))); +} diff --git a/packages/appkit-react/.storybook/manager.ts b/packages/appkit-react/.storybook/manager.ts new file mode 100644 index 000000000..a658e7a81 --- /dev/null +++ b/packages/appkit-react/.storybook/manager.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { addons } from 'storybook/manager-api'; + +import theme from './theme'; + +addons.setConfig({ + theme, +}); diff --git a/packages/appkit-react/.storybook/preview.tsx b/packages/appkit-react/.storybook/preview.tsx index 4d45f3f92..8a870129a 100644 --- a/packages/appkit-react/.storybook/preview.tsx +++ b/packages/appkit-react/.storybook/preview.tsx @@ -6,13 +6,28 @@ * */ -import type { Preview } from '@storybook/react'; -import type { Decorator } from '@storybook/react'; +import type { Preview } from '@storybook/react-vite'; +import type { Decorator } from '@storybook/react-vite'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import React from 'react'; +import { AppKitProvider } from '../src/providers/app-kit-provider'; import { I18nProvider } from '../src/providers/i18n-provider'; +import { appKit } from './app-kit'; +import theme from './theme'; + import '../src/styles/index.css'; +const queryClient = new QueryClient(); + +const withAppKit: Decorator = (Story) => ( + + + + + +); + const withI18n: Decorator = (Story) => ( @@ -21,8 +36,24 @@ const withI18n: Decorator = (Story) => ( const withTheme: Decorator = (Story, context) => { const theme = context.globals.theme; + + React.useEffect(() => { + document.documentElement.setAttribute('data-ta-theme', theme); + }, [theme]); + return ( -
+
); @@ -45,6 +76,10 @@ const preview: Preview = { }, }, parameters: { + docs: { + theme, + }, + layout: 'fullscreen', actions: { argTypesRegex: '^on[A-Z].*' }, controls: { matchers: { @@ -52,15 +87,8 @@ const preview: Preview = { date: /Date$/, }, }, - backgrounds: { - default: 'dark', - values: [ - { name: 'dark', value: '#000000' }, - { name: 'light', value: '#ffffff' }, - ], - }, }, - decorators: [withTheme, withI18n], + decorators: [withTheme, withI18n, withAppKit], }; export default preview; diff --git a/packages/appkit-react/.storybook/public/ton.svg b/packages/appkit-react/.storybook/public/ton.svg new file mode 100644 index 000000000..2eba092f4 --- /dev/null +++ b/packages/appkit-react/.storybook/public/ton.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/appkit-react/.storybook/theme.ts b/packages/appkit-react/.storybook/theme.ts new file mode 100644 index 000000000..4b7fa51a1 --- /dev/null +++ b/packages/appkit-react/.storybook/theme.ts @@ -0,0 +1,51 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { create } from 'storybook/theming'; + +export default create({ + base: 'dark', + + // Branding + brandTitle: 'TON AppKit', + brandUrl: 'https://github.com/ton-connect/kit', + brandImage: 'ton.svg', + brandTarget: '_self', + + // Colors + colorPrimary: '#007AFF', + colorSecondary: '#007AFF', + + // UI + appBg: '#141416', + appContentBg: '#1E1E1E', + appPreviewBg: '#141416', + appBorderColor: '#2C2C2C', + appBorderRadius: 8, + + // Typography + fontBase: + '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"', + fontCode: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', + + // Text colors + textColor: '#FFFFFF', + textInverseColor: '#141416', + + // Toolbar default and active colors + barTextColor: '#93939D', + barSelectedColor: '#007AFF', + barHoverColor: '#FFFFFF', + barBg: '#141416', + + // Form colors + inputBg: '#1E1E1E', + inputBorder: '#2C2C2C', + inputTextColor: '#FFFFFF', + inputBorderRadius: 8, +}); diff --git a/packages/appkit-react/docs/hooks.md b/packages/appkit-react/docs/hooks.md index b01154a4a..7b0c5265b 100644 --- a/packages/appkit-react/docs/hooks.md +++ b/packages/appkit-react/docs/hooks.md @@ -695,6 +695,15 @@ return ( ); ``` +### `useSwapProvider` + +Hook to get a specific swap provider. Returns the provider instance directly or `null` if not found. + +```tsx +const provider = useSwapProvider({ id: 'stonfi' }); +return
Result: {provider ? provider.providerId : 'null'}
; +``` + ## Transaction ### `useSendTransaction` diff --git a/packages/appkit-react/package.json b/packages/appkit-react/package.json index 4aeecc53e..0dc7c7679 100644 --- a/packages/appkit-react/package.json +++ b/packages/appkit-react/package.json @@ -49,10 +49,8 @@ "rosetta": "1.1.0" }, "devDependencies": { - "@storybook/addon-docs": "10.2.8", - "@storybook/react": "10.2.8", - "@storybook/react-vite": "10.2.8", - "@storybook/test": "^8.6.15", + "@storybook/addon-docs": "10.3.3", + "@storybook/react-vite": "10.3.3", "@tanstack/react-query": "catalog:", "@tonconnect/ui-react": "catalog:", "@types/react": "catalog:", @@ -60,7 +58,7 @@ "copyfiles": "2.4.1", "react": "catalog:", "react-dom": "catalog:", - "storybook": "10.2.8", + "storybook": "10.3.3", "typescript": "^5.9.2", "vite": "^7.3.1", "vite-plugin-node-polyfills": "^0.25.0" diff --git a/packages/appkit-react/src/components/block/block.module.css b/packages/appkit-react/src/components/block/block.module.css index 5de8b960d..102bbef8c 100644 --- a/packages/appkit-react/src/components/block/block.module.css +++ b/packages/appkit-react/src/components/block/block.module.css @@ -4,7 +4,7 @@ box-sizing: border-box; padding: 16px; border-radius: var(--ta-border-radius-l); - background-color: var(--ta-color-block); + background-color: var(--ta-color-background-secondary); } .row { diff --git a/packages/appkit-react/src/components/block/block.stories.tsx b/packages/appkit-react/src/components/block/block.stories.tsx index a14b28716..e4af9983e 100644 --- a/packages/appkit-react/src/components/block/block.stories.tsx +++ b/packages/appkit-react/src/components/block/block.stories.tsx @@ -6,7 +6,7 @@ * */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Block } from './block'; @@ -31,9 +31,15 @@ export const Column: Story = { direction: 'column', children: ( <> -
Item 1
-
Item 2
-
Item 3
+
+ Item 1 +
+
+ Item 2 +
+
+ Item 3 +
), }, @@ -44,9 +50,15 @@ export const Row: Story = { direction: 'row', children: ( <> -
Item 1
-
Item 2
-
Item 3
+
+ Item 1 +
+
+ Item 2 +
+
+ Item 3 +
), }, diff --git a/packages/appkit-react/src/components/button/button.module.css b/packages/appkit-react/src/components/button/button.module.css index 9482cf6e8..b4eae82d0 100644 --- a/packages/appkit-react/src/components/button/button.module.css +++ b/packages/appkit-react/src/components/button/button.module.css @@ -1,38 +1,109 @@ .button { - composes: bodyMedium from "../../styles/typography.module.css"; appearance: none; border: none; outline: none; cursor: pointer; box-sizing: border-box; - padding: 8px 16px; - border-radius: 20px; - - display: flex; + display: inline-flex; align-items: center; justify-content: center; gap: 8px; - - background-color: var(--ta-color-primary); - color: var(--ta-color-primary-foreground); - transition: transform 0.125s ease-in-out, background-color 0.2s ease; - will-change: transform; + transition: opacity 0.15s ease-in-out; + text-decoration: none; + width: fit-content; } -.button:hover { - transform: scale(1.02); +.button:hover:not(:disabled):not(.loading) { + opacity: 0.85; } -.button:active { - transform: scale(0.98); +.button:active:not(:disabled):not(.loading) { + opacity: 0.65; } .button:disabled { - opacity: 0.5; + opacity: 0.35; cursor: not-allowed; } -.button:disabled:hover, -.button:disabled:active { - transform: none; +.loading { + cursor: wait; +} + +/* Sizes */ +.l { + composes: bodySemibold from "../../styles/typography.module.css"; + padding: 12px 16px; + border-radius: var(--ta-border-radius-xl); +} + +.m { + composes: labelSemibold from "../../styles/typography.module.css"; + padding: 12px 16px; + border-radius: var(--ta-border-radius-l); +} + +.s { + composes: labelSemibold from "../../styles/typography.module.css"; + padding: 9px 12px; + border-radius: var(--ta-border-radius-2xl); +} + +/* Variants */ +.fill { + background-color: var(--ta-color-primary); + color: var(--ta-color-primary-foreground); +} + +.secondary { + background-color: var(--ta-color-background-secondary); + color: var(--ta-color-text); +} + +.bezeled { + background-color: var(--ta-color-background-bezeled); + color: var(--ta-color-primary); +} + +.gray { + background-color: var(--ta-color-background-tertiary); + color: var(--ta-color-text); +} + +.ghost { + background-color: transparent; + color: var(--ta-color-text); +} + +.ghost:hover:not(:disabled):not(.loading) { + background-color: var(--ta-color-background-tertiary); +} + +.fullWidth { + width: 100%; +} + +.icon { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.spinner { + width: 18px; + height: 18px; + border: 2.5px solid currentColor; + border-top-color: transparent; + border-radius: 50%; + animation: button-spin 0.6s linear infinite; +} + +@keyframes button-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } } diff --git a/packages/appkit-react/src/components/button/button.stories.tsx b/packages/appkit-react/src/components/button/button.stories.tsx index a268465b5..1682ac0df 100644 --- a/packages/appkit-react/src/components/button/button.stories.tsx +++ b/packages/appkit-react/src/components/button/button.stories.tsx @@ -6,7 +6,7 @@ * */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Button } from './button'; @@ -15,9 +15,23 @@ const meta: Meta = { component: Button, tags: ['autodocs'], argTypes: { + size: { + control: 'select', + options: ['s', 'm', 'l'], + }, + variant: { + control: 'select', + options: ['fill', 'bezeled', 'gray'], + }, disabled: { control: 'boolean', }, + loading: { + control: 'boolean', + }, + fullWidth: { + control: 'boolean', + }, }, }; @@ -25,34 +39,71 @@ export default meta; type Story = StoryObj; -export const Default: Story = { +export const Fill: Story = { + args: { + children: 'Action', + variant: 'fill', + size: 'l', + }, +}; + +export const Bezeled: Story = { + args: { + children: 'Action', + variant: 'bezeled', + size: 'l', + }, +}; + +export const Gray: Story = { + args: { + children: 'Action', + variant: 'gray', + size: 'l', + }, +}; + +export const Sizes: Story = { + render: (args) => ( +
+ + + +
+ ), args: { - children: 'Click me', + variant: 'fill', }, }; -export const Disabled: Story = { +export const Variants: Story = { + render: (args) => ( +
+ + + +
+ ), args: { - children: 'Disabled Button', - disabled: true, + size: 'l', }, }; -export const WithIcon: Story = { +export const Loading: Story = { args: { - children: ( - <> - - - - Add Item - - ), + children: 'Loading Button', + loading: true, }, }; diff --git a/packages/appkit-react/src/components/button/button.tsx b/packages/appkit-react/src/components/button/button.tsx index a9f9dce44..1b70c9e42 100644 --- a/packages/appkit-react/src/components/button/button.tsx +++ b/packages/appkit-react/src/components/button/button.tsx @@ -6,11 +6,60 @@ * */ -import type { FC, ComponentProps } from 'react'; +import { forwardRef } from 'react'; +import type { ComponentProps, ReactNode } from 'react'; import clsx from 'clsx'; import styles from './button.module.css'; -export const Button: FC> = ({ className, ...props }) => { - return + ); + }, +); + +Button.displayName = 'Button'; diff --git a/packages/appkit-react/src/components/button/index.ts b/packages/appkit-react/src/components/button/index.ts index 4d25fa681..628283402 100644 --- a/packages/appkit-react/src/components/button/index.ts +++ b/packages/appkit-react/src/components/button/index.ts @@ -7,3 +7,4 @@ */ export { Button } from './button'; +export type { ButtonProps } from './button'; diff --git a/packages/appkit-react/src/components/circle-icon/circle-icon.tsx b/packages/appkit-react/src/components/circle-icon/circle-icon.tsx deleted file mode 100644 index 5ed387403..000000000 --- a/packages/appkit-react/src/components/circle-icon/circle-icon.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) TonTech. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import type { FC, ComponentProps } from 'react'; -import clsx from 'clsx'; -import { Avatar } from 'radix-ui'; - -import styles from './circle-icon.module.css'; - -export interface CircleIconProps extends ComponentProps<'div'> { - size?: number; - src?: string; - alt?: string; - fallback?: string; -} - -export const CircleIcon: FC = ({ className, size = 30, src, alt, fallback, ...props }) => { - return ( - - - - {(fallback || alt) && ( - - {fallback ? fallback : alt?.[0]} - - )} - - ); -}; diff --git a/packages/appkit-react/src/components/dialog/dialog.tsx b/packages/appkit-react/src/components/dialog/dialog.tsx new file mode 100644 index 000000000..dbf4fefdd --- /dev/null +++ b/packages/appkit-react/src/components/dialog/dialog.tsx @@ -0,0 +1,105 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { forwardRef, useCallback, useEffect, useId } from 'react'; +import { createPortal } from 'react-dom'; +import type { ComponentPropsWithoutRef, ComponentRef, FC, ReactNode } from 'react'; + +import { DialogContext, useDialogContext } from './use-dialog-context'; + +interface DialogRootProps { + children: ReactNode; + open?: boolean; + onOpenChange?: (open: boolean) => void; +} + +const DialogRoot: FC = ({ children, open = false, onOpenChange }) => { + const titleId = useId(); + const handleOpenChange = useCallback((value: boolean) => onOpenChange?.(value), [onOpenChange]); + + return ( + + {children} + + ); +}; + +interface DialogPortalProps { + children: ReactNode; + container?: Element | null; +} + +const DialogPortal: FC = ({ children, container }) => { + const { open } = useDialogContext(); + if (!open || typeof document === 'undefined') return null; + return createPortal(children, container ?? document.body); +}; + +const DialogOverlay = forwardRef, ComponentPropsWithoutRef<'div'>>((props, ref) => { + useEffect(() => { + document.body.style.overflow = 'hidden'; + return () => { + document.body.style.overflow = ''; + }; + }, []); + + return
; +}); + +DialogOverlay.displayName = 'DialogOverlay'; + +const DialogContent = forwardRef, ComponentPropsWithoutRef<'div'>>((props, ref) => { + const { onOpenChange, titleId } = useDialogContext(); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') onOpenChange(false); + }; + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [onOpenChange]); + + return
; +}); + +DialogContent.displayName = 'DialogContent'; + +const DialogTitle = forwardRef, ComponentPropsWithoutRef<'h2'>>((props, ref) => { + const { titleId } = useDialogContext(); + return

; +}); + +DialogTitle.displayName = 'DialogTitle'; + +const DialogClose = forwardRef, ComponentPropsWithoutRef<'button'>>( + ({ onClick, ...props }, ref) => { + const { onOpenChange } = useDialogContext(); + return ( + + +

This is a simple modal window content.

+
+ + ); + }, +}; + +export const LargeContent: Story = { + render: () => { + const [open, setOpen] = useState(false); + return ( + <> + + +
+ {Array.from({ length: 20 }).map((_, i) => ( +

+ This is paragraph {i + 1} of a very long text. Modals should be scrollable when the + content exceeds the screen height. +

+ ))} +
+
+ + ); + }, +}; diff --git a/packages/appkit-react/src/components/modal/modal.tsx b/packages/appkit-react/src/components/modal/modal.tsx new file mode 100644 index 000000000..c6fdb290d --- /dev/null +++ b/packages/appkit-react/src/components/modal/modal.tsx @@ -0,0 +1,68 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { FC, ReactNode } from 'react'; +import clsx from 'clsx'; + +import { Dialog } from '../dialog'; +import styles from './modal.module.css'; + +export interface ModalProps { + /** + * Controlled open state. + */ + open?: boolean; + /** + * Event handler called when the open state changes. + */ + onOpenChange?: (open: boolean) => void; + /** + * Modal title. + */ + title?: string; + /** + * Modal content. + */ + children?: ReactNode; + /** + * Additional class name for the content container. + */ + className?: string; +} + +const CloseIcon = () => ( + + + +); + +export const Modal: FC = ({ open, onOpenChange, title, children, className }) => { + return ( + + + onOpenChange?.(false)}> + e.stopPropagation()}> +
+ {title && {title}} + + + +
+
{children}
+
+
+
+
+ ); +}; diff --git a/packages/appkit-react/src/components/search-icon/index.ts b/packages/appkit-react/src/components/search-icon/index.ts new file mode 100644 index 000000000..3ebae8f92 --- /dev/null +++ b/packages/appkit-react/src/components/search-icon/index.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { SearchIcon } from './search-icon'; diff --git a/packages/appkit-react/src/components/search-icon/search-icon.tsx b/packages/appkit-react/src/components/search-icon/search-icon.tsx new file mode 100644 index 000000000..2dc54cf67 --- /dev/null +++ b/packages/appkit-react/src/components/search-icon/search-icon.tsx @@ -0,0 +1,24 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { ComponentProps, FC } from 'react'; + +export interface SearchIconProps extends Omit, 'width' | 'height'> { + size?: number; +} + +export const SearchIcon: FC = ({ size = 16, ...props }) => { + return ( + + + + ); +}; diff --git a/packages/appkit-react/src/components/skeleton/index.ts b/packages/appkit-react/src/components/skeleton/index.ts new file mode 100644 index 000000000..586f0d232 --- /dev/null +++ b/packages/appkit-react/src/components/skeleton/index.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { Skeleton } from './skeleton'; +export type { SkeletonProps } from './skeleton'; diff --git a/packages/appkit-react/src/components/skeleton/skeleton.module.css b/packages/appkit-react/src/components/skeleton/skeleton.module.css new file mode 100644 index 000000000..ab06da3f5 --- /dev/null +++ b/packages/appkit-react/src/components/skeleton/skeleton.module.css @@ -0,0 +1,25 @@ +.skeleton { + display: inline-block; + background-color: var(--ta-color-background-tertiary); + border-radius: var(--ta-border-radius-l); + position: relative; + overflow: hidden; +} + +.skeleton::after { + content: ''; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + transform: translateX(-100%); + background: var(--ta-color-skeleton-shimmer); + animation: skeleton-shimmer 1.5s infinite; +} + +@keyframes skeleton-shimmer { + 100% { + transform: translateX(100%); + } +} diff --git a/packages/appkit-react/src/components/skeleton/skeleton.stories.tsx b/packages/appkit-react/src/components/skeleton/skeleton.stories.tsx new file mode 100644 index 000000000..e28cd0f7a --- /dev/null +++ b/packages/appkit-react/src/components/skeleton/skeleton.stories.tsx @@ -0,0 +1,62 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { Skeleton } from './skeleton'; + +const meta: Meta = { + title: 'Public/Components/Skeleton', + component: Skeleton, + tags: ['autodocs'], + argTypes: { + width: { + control: 'text', + }, + height: { + control: 'text', + }, + }, + args: { + width: 100, + height: 20, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const CustomSize: Story = { + args: { + width: '100%', + height: 100, + }, +}; + +export const Circular: Story = { + args: { + width: 48, + height: 48, + style: { borderRadius: '50%' }, + }, +}; + +export const ParagraphPlaceholder: Story = { + render: (args) => ( +
+ + + + +
+ ), + args: {}, +}; diff --git a/packages/appkit-react/src/components/skeleton/skeleton.tsx b/packages/appkit-react/src/components/skeleton/skeleton.tsx new file mode 100644 index 000000000..7a27f5926 --- /dev/null +++ b/packages/appkit-react/src/components/skeleton/skeleton.tsx @@ -0,0 +1,33 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { forwardRef } from 'react'; +import type { ComponentProps } from 'react'; +import clsx from 'clsx'; + +import styles from './skeleton.module.css'; + +export interface SkeletonProps extends ComponentProps<'div'> { + width?: string | number; + height?: string | number; +} + +export const Skeleton = forwardRef( + ({ className, width, height, style, ...props }, ref) => { + return ( +
+ ); + }, +); + +Skeleton.displayName = 'Skeleton'; diff --git a/packages/appkit-react/src/components/token-select-modal/index.ts b/packages/appkit-react/src/components/token-select-modal/index.ts new file mode 100644 index 000000000..8188dd7ec --- /dev/null +++ b/packages/appkit-react/src/components/token-select-modal/index.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { TokenSelectModal } from './token-select-modal'; +export type { TokenSelectModalProps } from './token-select-modal'; diff --git a/packages/appkit-react/src/components/token-select-modal/token-select-modal.module.css b/packages/appkit-react/src/components/token-select-modal/token-select-modal.module.css new file mode 100644 index 000000000..8f8e2d427 --- /dev/null +++ b/packages/appkit-react/src/components/token-select-modal/token-select-modal.module.css @@ -0,0 +1,27 @@ +.searchWrapper { + margin-bottom: 16px; +} + +.list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 16px; + height: 400px; + max-height: 100%; + overflow-y: auto; + flex: 1; +} + +.empty { + padding: 32px 0; +} + +.emptyText { + composes: bodyRegular from "../../styles/typography.module.css"; + margin: 0; + color: var(--ta-color-text-secondary); + text-align: center; +} diff --git a/packages/appkit-react/src/components/token-select-modal/token-select-modal.stories.tsx b/packages/appkit-react/src/components/token-select-modal/token-select-modal.stories.tsx new file mode 100644 index 000000000..a16bc0f92 --- /dev/null +++ b/packages/appkit-react/src/components/token-select-modal/token-select-modal.stories.tsx @@ -0,0 +1,68 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { STORY_TOKENS } from '../../storybook/fixtures/tokens'; +import { Button } from '../button'; +import { TokenSelectModal } from './token-select-modal'; +import type { AppkitUIToken } from '../../types/appkit-ui-token'; + +const meta: Meta = { + title: 'Public/Components/TokenSelectModal', + component: TokenSelectModal, + tags: ['autodocs'], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + render: () => { + const [open, setOpen] = useState(false); + const [selected, setSelected] = useState(null); + + return ( + <> + + setOpen(false)} + tokens={STORY_TOKENS} + onSelect={setSelected} + title="Select Token" + searchPlaceholder="Search by name or symbol" + /> + + ); + }, +}; + +export const Empty: Story = { + render: () => { + const [open, setOpen] = useState(false); + + return ( + <> + + setOpen(false)} + tokens={[]} + onSelect={() => {}} + title="Select Token" + searchPlaceholder="Search by name or symbol" + /> + + ); + }, +}; diff --git a/packages/appkit-react/src/components/token-select-modal/token-select-modal.tsx b/packages/appkit-react/src/components/token-select-modal/token-select-modal.tsx new file mode 100644 index 000000000..395b042b5 --- /dev/null +++ b/packages/appkit-react/src/components/token-select-modal/token-select-modal.tsx @@ -0,0 +1,97 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useState } from 'react'; +import type { FC } from 'react'; +import { compareAddress } from '@ton/appkit'; + +import { Input } from '../input/input'; +import { Modal } from '../modal/modal'; +import { SearchIcon } from '../search-icon'; +import { CurrencyItem } from '../../features/balances'; +import type { AppkitUIToken } from '../../types/appkit-ui-token'; +import styles from './token-select-modal.module.css'; + +export interface TokenSelectModalProps { + open: boolean; + onClose: () => void; + tokens: AppkitUIToken[]; + onSelect: (token: AppkitUIToken) => void; + title: string; + searchPlaceholder?: string; +} + +export const TokenSelectModal: FC = ({ + open, + onClose, + tokens, + onSelect, + title, + searchPlaceholder, +}) => { + const [search, setSearch] = useState(''); + + const filtered = tokens.filter( + (token) => + token.symbol.toLowerCase().includes(search.toLowerCase()) || + token.name.toLowerCase().includes(search.toLowerCase()) || + compareAddress(token.address, search), + ); + + const handleSelect = (token: AppkitUIToken) => () => { + onSelect(token); + onClose(); + setSearch(''); + }; + + const handleOpenChange = (isOpen: boolean) => { + if (!isOpen) { + onClose(); + setSearch(''); + } + }; + + return ( + + + + + + + setSearch(e.target.value)} + autoFocus + /> + + + +
+ {filtered.length === 0 ? ( +
+

We didn't find any tokens.

+

Try searching by address.

+
+ ) : ( +
    + {filtered.map((token) => ( + + ))} +
+ )} +
+
+ ); +}; diff --git a/packages/appkit-react/src/components/ton-icon/ton-icon.stories.tsx b/packages/appkit-react/src/components/ton-icon/ton-icon.stories.tsx index 068f195be..d894f6218 100644 --- a/packages/appkit-react/src/components/ton-icon/ton-icon.stories.tsx +++ b/packages/appkit-react/src/components/ton-icon/ton-icon.stories.tsx @@ -6,7 +6,7 @@ * */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { TonIcon, TonIconCircle } from './ton-icon'; @@ -53,7 +53,7 @@ export const Large: Story = { export const CustomColor: Story = { args: { size: 32, - style: { color: '#0098EB' }, + style: { color: 'var(--ta-color-primary)' }, }, }; diff --git a/packages/appkit-react/src/components/ton-icon/ton-icon.tsx b/packages/appkit-react/src/components/ton-icon/ton-icon.tsx index d4179ba27..e3982be58 100644 --- a/packages/appkit-react/src/components/ton-icon/ton-icon.tsx +++ b/packages/appkit-react/src/components/ton-icon/ton-icon.tsx @@ -29,16 +29,16 @@ export const TonIconCircle: FC = ({ size = 16, ...props }) => { - + diff --git a/packages/appkit-react/src/features/balances/components/balance-badge/balance-badge.module.css b/packages/appkit-react/src/features/balances/components/balance-badge/balance-badge.module.css index 5bebfd093..f942d9b42 100644 --- a/packages/appkit-react/src/features/balances/components/balance-badge/balance-badge.module.css +++ b/packages/appkit-react/src/features/balances/components/balance-badge/balance-badge.module.css @@ -1,5 +1,5 @@ .balance { - composes: bodyMedium from "../../../../styles/typography.module.css"; + composes: labelMedium from "../../../../styles/typography.module.css"; width: fit-content; gap: 12px; @@ -15,7 +15,7 @@ } .ticker { - composes: bodyBold from "../../../../styles/typography.module.css"; + composes: labelSemibold from "../../../../styles/typography.module.css"; color: var(--ta-color-text); line-height: 1; } diff --git a/packages/appkit-react/src/features/balances/components/balance-badge/balance-badge.stories.tsx b/packages/appkit-react/src/features/balances/components/balance-badge/balance-badge.stories.tsx index eb0a784dd..1d4dda479 100644 --- a/packages/appkit-react/src/features/balances/components/balance-badge/balance-badge.stories.tsx +++ b/packages/appkit-react/src/features/balances/components/balance-badge/balance-badge.stories.tsx @@ -6,16 +6,13 @@ * */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { BalanceBadge } from './balance-badge'; const meta: Meta = { title: 'Public/Features/Balances/BalanceBadge', tags: ['autodocs'], - parameters: { - layout: 'centered', - }, }; export default meta; diff --git a/packages/appkit-react/src/features/balances/components/balance-badge/balance-badge.tsx b/packages/appkit-react/src/features/balances/components/balance-badge/balance-badge.tsx index 662d9250d..0e42a999a 100644 --- a/packages/appkit-react/src/features/balances/components/balance-badge/balance-badge.tsx +++ b/packages/appkit-react/src/features/balances/components/balance-badge/balance-badge.tsx @@ -12,7 +12,7 @@ import clsx from 'clsx'; import { Block } from '../../../../components/block'; import styles from './balance-badge.module.css'; -import { CircleIcon } from '../../../../components/circle-icon'; +import { Logo } from '../../../../components/logo'; const BalanceBadgeContainer: FC> = ({ className, ...props }) => { return ; @@ -40,7 +40,7 @@ const BalanceSymbol: FC & { symbol: string }> = ({ classN export const BalanceBadge = { Container: BalanceBadgeContainer, - Icon: CircleIcon, + Icon: Logo, BalanceBlock: BalanceBlock, Symbol: BalanceSymbol, Balance: Balance, diff --git a/packages/appkit-react/src/features/balances/components/currency-item/currency-item.module.css b/packages/appkit-react/src/features/balances/components/currency-item/currency-item.module.css index d8dc0d179..ac05553b9 100644 --- a/packages/appkit-react/src/features/balances/components/currency-item/currency-item.module.css +++ b/packages/appkit-react/src/features/balances/components/currency-item/currency-item.module.css @@ -1,23 +1,17 @@ .currencyItem { box-sizing: border-box; - border-radius: var(--ta-border-radius-l); width: 100%; display: flex; flex-direction: row; align-items: center; gap: 12px; - padding: 12px; - background-color: var(--ta-color-block); - transition: border-color 150ms cubic-bezier(0.4, 0, 0.2, 1); + padding: 0; + background-color: transparent; cursor: pointer; - border: 1px solid transparent; + border: none; outline: none; } -.currencyItem:hover { - border-color: var(--ta-color-primary); -} - .icon { width: 40px; height: 40px; @@ -45,7 +39,7 @@ } .name { - composes: bodyBold from "../../../../styles/typography.module.css"; + composes: bodySemibold from "../../../../styles/typography.module.css"; color: var(--ta-color-text); white-space: nowrap; @@ -62,7 +56,7 @@ } .ticker { - composes: bodyMedium from "../../../../styles/typography.module.css"; + composes: labelMedium from "../../../../styles/typography.module.css"; color: var(--ta-color-text-secondary); margin: 0; diff --git a/packages/appkit-react/src/features/balances/components/currency-item/currency-item.stories.tsx b/packages/appkit-react/src/features/balances/components/currency-item/currency-item.stories.tsx index 2ac1f7c2c..1f59fbdc0 100644 --- a/packages/appkit-react/src/features/balances/components/currency-item/currency-item.stories.tsx +++ b/packages/appkit-react/src/features/balances/components/currency-item/currency-item.stories.tsx @@ -6,8 +6,8 @@ * */ -import type { Meta, StoryObj } from '@storybook/react'; -import { fn } from '@storybook/test'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { fn } from 'storybook/test'; import { CurrencyItem } from './currency-item'; @@ -15,9 +15,6 @@ const meta: Meta = { title: 'Public/Features/Balances/CurrencyItem', component: CurrencyItem, tags: ['autodocs'], - parameters: { - layout: 'centered', - }, args: { onClick: fn(), }, diff --git a/packages/appkit-react/src/features/balances/components/currency-item/currency-item.tsx b/packages/appkit-react/src/features/balances/components/currency-item/currency-item.tsx index 16840eefa..bffc5207a 100644 --- a/packages/appkit-react/src/features/balances/components/currency-item/currency-item.tsx +++ b/packages/appkit-react/src/features/balances/components/currency-item/currency-item.tsx @@ -9,7 +9,7 @@ import type { FC, ComponentProps } from 'react'; import clsx from 'clsx'; -import { CircleIcon } from '../../../../components/circle-icon'; +import { Logo } from '../../../../components/logo'; import styles from './currency-item.module.css'; export interface CurrencyItemProps extends ComponentProps<'button'> { @@ -31,7 +31,7 @@ export const CurrencyItem: FC = ({ }) => { return ( ); }; diff --git a/packages/appkit-react/src/features/balances/components/send-jetton-button/send-jetton-button.stories.tsx b/packages/appkit-react/src/features/balances/components/send-jetton-button/send-jetton-button.stories.tsx index 5de010829..e63e8bfba 100644 --- a/packages/appkit-react/src/features/balances/components/send-jetton-button/send-jetton-button.stories.tsx +++ b/packages/appkit-react/src/features/balances/components/send-jetton-button/send-jetton-button.stories.tsx @@ -6,7 +6,7 @@ * */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Button } from '../../../../components/button'; @@ -26,9 +26,6 @@ const meta: Meta = { title: 'Public/Features/Balances/SendJettonButton', component: SendJettonButtonPreview, tags: ['autodocs'], - parameters: { - layout: 'centered', - }, }; export default meta; diff --git a/packages/appkit-react/src/features/balances/components/send-jetton-button/send-jetton-button.tsx b/packages/appkit-react/src/features/balances/components/send-jetton-button/send-jetton-button.tsx index 6aade283e..d9d8cd399 100644 --- a/packages/appkit-react/src/features/balances/components/send-jetton-button/send-jetton-button.tsx +++ b/packages/appkit-react/src/features/balances/components/send-jetton-button/send-jetton-button.tsx @@ -10,8 +10,7 @@ import { useCallback, useMemo } from 'react'; import type { FC } from 'react'; import { createTransferJettonTransaction, formatUnits, parseUnits } from '@ton/appkit'; -import { useI18n } from '../../../../hooks/use-i18n'; -import { useAppKit } from '../../../../hooks/use-app-kit'; +import { useI18n, useAppKit } from '../../../settings'; import type { SendProps } from '../../../transaction'; import { Send } from '../../../transaction'; diff --git a/packages/appkit-react/src/features/balances/components/send-ton-button/send-ton-button.stories.tsx b/packages/appkit-react/src/features/balances/components/send-ton-button/send-ton-button.stories.tsx index ea68ef1f4..20d16faa6 100644 --- a/packages/appkit-react/src/features/balances/components/send-ton-button/send-ton-button.stories.tsx +++ b/packages/appkit-react/src/features/balances/components/send-ton-button/send-ton-button.stories.tsx @@ -6,7 +6,7 @@ * */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Button } from '../../../../components/button'; @@ -26,9 +26,6 @@ const meta: Meta = { title: 'Public/Features/Balances/SendTonButton', component: SendTonButtonPreview, tags: ['autodocs'], - parameters: { - layout: 'centered', - }, }; export default meta; diff --git a/packages/appkit-react/src/features/balances/components/send-ton-button/send-ton-button.tsx b/packages/appkit-react/src/features/balances/components/send-ton-button/send-ton-button.tsx index 7005cd59f..30201ae49 100644 --- a/packages/appkit-react/src/features/balances/components/send-ton-button/send-ton-button.tsx +++ b/packages/appkit-react/src/features/balances/components/send-ton-button/send-ton-button.tsx @@ -10,8 +10,7 @@ import { useCallback } from 'react'; import type { FC } from 'react'; import { createTransferTonTransaction } from '@ton/appkit'; -import { useI18n } from '../../../../hooks/use-i18n'; -import { useAppKit } from '../../../../hooks/use-app-kit'; +import { useI18n, useAppKit } from '../../../settings'; import type { SendProps } from '../../../transaction'; import { Send } from '../../../transaction'; diff --git a/packages/appkit-react/src/features/balances/hooks/use-balance-by-address.ts b/packages/appkit-react/src/features/balances/hooks/use-balance-by-address.ts index ceddb9706..e01443bd4 100644 --- a/packages/appkit-react/src/features/balances/hooks/use-balance-by-address.ts +++ b/packages/appkit-react/src/features/balances/hooks/use-balance-by-address.ts @@ -9,7 +9,7 @@ import { getBalanceByAddressQueryOptions } from '@ton/appkit/queries'; import type { GetBalanceByAddressData, GetBalanceErrorType, GetBalanceByAddressQueryConfig } from '@ton/appkit/queries'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; import { useQuery } from '../../../libs/query'; import type { UseQueryReturnType } from '../../../libs/query'; diff --git a/packages/appkit-react/src/features/balances/hooks/use-watch-balance-by-address.ts b/packages/appkit-react/src/features/balances/hooks/use-watch-balance-by-address.ts index fe4f3cb89..d03d8ec42 100644 --- a/packages/appkit-react/src/features/balances/hooks/use-watch-balance-by-address.ts +++ b/packages/appkit-react/src/features/balances/hooks/use-watch-balance-by-address.ts @@ -12,7 +12,7 @@ import { watchBalanceByAddress, hasStreamingProvider, resolveNetwork } from '@to import type { WatchBalanceByAddressOptions } from '@ton/appkit'; import { handleBalanceUpdate } from '@ton/appkit/queries'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; export type UseWatchBalanceByAddressParameters = Partial; diff --git a/packages/appkit-react/src/features/jettons/hooks/use-jetton-balance-by-address.ts b/packages/appkit-react/src/features/jettons/hooks/use-jetton-balance-by-address.ts index 2b60743ef..c8aa7c162 100644 --- a/packages/appkit-react/src/features/jettons/hooks/use-jetton-balance-by-address.ts +++ b/packages/appkit-react/src/features/jettons/hooks/use-jetton-balance-by-address.ts @@ -13,7 +13,7 @@ import type { GetJettonBalanceByAddressQueryConfig, } from '@ton/appkit/queries'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; import { useQuery } from '../../../libs/query'; import type { UseQueryReturnType } from '../../../libs/query'; diff --git a/packages/appkit-react/src/features/jettons/hooks/use-jetton-info.ts b/packages/appkit-react/src/features/jettons/hooks/use-jetton-info.ts index 494d0c4e6..62373412f 100644 --- a/packages/appkit-react/src/features/jettons/hooks/use-jetton-info.ts +++ b/packages/appkit-react/src/features/jettons/hooks/use-jetton-info.ts @@ -9,7 +9,7 @@ import { getJettonInfoQueryOptions } from '@ton/appkit/queries'; import type { GetJettonInfoData, GetJettonInfoErrorType, GetJettonInfoQueryConfig } from '@ton/appkit/queries'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; import { useQuery } from '../../../libs/query'; import type { UseQueryReturnType } from '../../../libs/query'; diff --git a/packages/appkit-react/src/features/jettons/hooks/use-jetton-wallet-address.ts b/packages/appkit-react/src/features/jettons/hooks/use-jetton-wallet-address.ts index 7bf11c6c3..f45847b01 100644 --- a/packages/appkit-react/src/features/jettons/hooks/use-jetton-wallet-address.ts +++ b/packages/appkit-react/src/features/jettons/hooks/use-jetton-wallet-address.ts @@ -13,7 +13,7 @@ import type { GetJettonWalletAddressQueryConfig, } from '@ton/appkit/queries'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; import { useQuery } from '../../../libs/query'; import type { UseQueryReturnType } from '../../../libs/query'; diff --git a/packages/appkit-react/src/features/jettons/hooks/use-jettons-by-address.ts b/packages/appkit-react/src/features/jettons/hooks/use-jettons-by-address.ts index 2dd119d92..cd5828f5d 100644 --- a/packages/appkit-react/src/features/jettons/hooks/use-jettons-by-address.ts +++ b/packages/appkit-react/src/features/jettons/hooks/use-jettons-by-address.ts @@ -9,7 +9,7 @@ import { getJettonsByAddressQueryOptions } from '@ton/appkit/queries'; import type { GetJettonsByAddressData, GetJettonsErrorType, GetJettonsByAddressQueryConfig } from '@ton/appkit/queries'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; import { useQuery } from '../../../libs/query'; import type { UseQueryReturnType } from '../../../libs/query'; diff --git a/packages/appkit-react/src/features/jettons/hooks/use-transfer-jetton.ts b/packages/appkit-react/src/features/jettons/hooks/use-transfer-jetton.ts index 26d06237a..fb797cb94 100644 --- a/packages/appkit-react/src/features/jettons/hooks/use-transfer-jetton.ts +++ b/packages/appkit-react/src/features/jettons/hooks/use-transfer-jetton.ts @@ -19,7 +19,7 @@ import { transferJettonMutationOptions } from '@ton/appkit/queries'; import { useMutation } from '../../../libs/query'; import type { UseMutationReturnType } from '../../../libs/query'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; export type UseTransferJettonParameters = TransferJettonOptions; diff --git a/packages/appkit-react/src/features/jettons/hooks/use-watch-jettons-by-address.ts b/packages/appkit-react/src/features/jettons/hooks/use-watch-jettons-by-address.ts index bbdc990b6..333f9bdc0 100644 --- a/packages/appkit-react/src/features/jettons/hooks/use-watch-jettons-by-address.ts +++ b/packages/appkit-react/src/features/jettons/hooks/use-watch-jettons-by-address.ts @@ -12,7 +12,7 @@ import { watchJettonsByAddress, hasStreamingProvider, resolveNetwork } from '@to import type { WatchJettonsByAddressOptions, JettonUpdate } from '@ton/appkit'; import { handleJettonBalanceUpdate, handleJettonsUpdate } from '@ton/appkit/queries'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; export type UseWatchJettonsByAddressParameters = Partial; diff --git a/packages/appkit-react/src/features/network/hooks/use-block-number.ts b/packages/appkit-react/src/features/network/hooks/use-block-number.ts index 2c2d3365e..8c4114920 100644 --- a/packages/appkit-react/src/features/network/hooks/use-block-number.ts +++ b/packages/appkit-react/src/features/network/hooks/use-block-number.ts @@ -9,7 +9,7 @@ import { getBlockNumberQueryOptions } from '@ton/appkit/queries'; import type { GetBlockNumberData, GetBlockNumberErrorType, GetBlockNumberQueryConfig } from '@ton/appkit/queries'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; import { useQuery } from '../../../libs/query'; import type { UseQueryReturnType } from '../../../libs/query'; diff --git a/packages/appkit-react/src/features/network/hooks/use-default-network.ts b/packages/appkit-react/src/features/network/hooks/use-default-network.ts index 43f78ebc9..694b99aed 100644 --- a/packages/appkit-react/src/features/network/hooks/use-default-network.ts +++ b/packages/appkit-react/src/features/network/hooks/use-default-network.ts @@ -10,7 +10,7 @@ import { useSyncExternalStore, useCallback } from 'react'; import { getDefaultNetwork, setDefaultNetwork, watchDefaultNetwork } from '@ton/appkit'; import type { GetDefaultNetworkReturnType, Network } from '@ton/appkit'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; export type UseDefaultNetworkReturnType = [ network: GetDefaultNetworkReturnType, diff --git a/packages/appkit-react/src/features/network/hooks/use-networks.ts b/packages/appkit-react/src/features/network/hooks/use-networks.ts index 788c74883..775da117e 100644 --- a/packages/appkit-react/src/features/network/hooks/use-networks.ts +++ b/packages/appkit-react/src/features/network/hooks/use-networks.ts @@ -10,7 +10,7 @@ import { useSyncExternalStore, useCallback, useRef } from 'react'; import { getNetworks, watchNetworks } from '@ton/appkit'; import type { GetNetworksReturnType } from '@ton/appkit'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; export type UseNetworksReturnType = GetNetworksReturnType; diff --git a/packages/appkit-react/src/features/nft/components/nft-item/nft-item.module.css b/packages/appkit-react/src/features/nft/components/nft-item/nft-item.module.css index 38704049f..c43871e54 100644 --- a/packages/appkit-react/src/features/nft/components/nft-item/nft-item.module.css +++ b/packages/appkit-react/src/features/nft/components/nft-item/nft-item.module.css @@ -38,10 +38,9 @@ } .name { - composes: bodySmall from "../../../../styles/typography.module.css"; + composes: labelSemibold from "../../../../styles/typography.module.css"; color: var(--ta-color-text); - font-weight: var(--ta-body-medium-font-weight); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -49,7 +48,7 @@ } .collectionName { - composes: bodySmall from "../../../../styles/typography.module.css"; + composes: footnoteRegular from "../../../../styles/typography.module.css"; color: var(--ta-color-text-secondary); white-space: nowrap; @@ -59,7 +58,7 @@ } .saleBadge { - composes: caption2 from "../../../../styles/typography.module.css"; + composes: captionSemibold from "../../../../styles/typography.module.css"; position: absolute; top: 8px; @@ -68,7 +67,6 @@ align-items: center; padding: 2px 6px; border-radius: var(--ta-border-radius-s); - font-weight: var(--ta-body-medium-font-weight); background-color: var(--ta-color-primary); - color: var(--ta-color-text-on-primary, #fff); + color: var(--ta-color-primary-foreground); } diff --git a/packages/appkit-react/src/features/nft/components/nft-item/nft-item.stories.tsx b/packages/appkit-react/src/features/nft/components/nft-item/nft-item.stories.tsx index 4b4012a82..d96ccb920 100644 --- a/packages/appkit-react/src/features/nft/components/nft-item/nft-item.stories.tsx +++ b/packages/appkit-react/src/features/nft/components/nft-item/nft-item.stories.tsx @@ -6,8 +6,8 @@ * */ -import type { Meta, StoryObj } from '@storybook/react'; -import { fn } from '@storybook/test'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { fn } from 'storybook/test'; import clsx from 'clsx'; import styles from './nft-item.module.css'; diff --git a/packages/appkit-react/src/features/nft/components/nft-item/nft-item.tsx b/packages/appkit-react/src/features/nft/components/nft-item/nft-item.tsx index e47418501..2fd8a81b2 100644 --- a/packages/appkit-react/src/features/nft/components/nft-item/nft-item.tsx +++ b/packages/appkit-react/src/features/nft/components/nft-item/nft-item.tsx @@ -12,7 +12,7 @@ import type { FC, ComponentProps } from 'react'; import { getFormattedNftInfo } from '@ton/appkit'; import clsx from 'clsx'; -import { useI18n } from '../../../../hooks/use-i18n'; +import { useI18n } from '../../../settings/hooks/use-i18n'; import styles from './nft-item.module.css'; const PlaceholderIcon: FC = () => ( diff --git a/packages/appkit-react/src/features/nft/hooks/use-nft.ts b/packages/appkit-react/src/features/nft/hooks/use-nft.ts index 99681de7a..00dce6e1b 100644 --- a/packages/appkit-react/src/features/nft/hooks/use-nft.ts +++ b/packages/appkit-react/src/features/nft/hooks/use-nft.ts @@ -9,7 +9,7 @@ import { getNftQueryOptions } from '@ton/appkit/queries'; import type { GetNftData, GetNftErrorType, GetNftQueryConfig } from '@ton/appkit/queries'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; import { useQuery } from '../../../libs/query'; import type { UseQueryReturnType } from '../../../libs/query'; diff --git a/packages/appkit-react/src/features/nft/hooks/use-nfts-by-address.ts b/packages/appkit-react/src/features/nft/hooks/use-nfts-by-address.ts index 18cacb6a8..6cafde854 100644 --- a/packages/appkit-react/src/features/nft/hooks/use-nfts-by-address.ts +++ b/packages/appkit-react/src/features/nft/hooks/use-nfts-by-address.ts @@ -9,7 +9,7 @@ import { getNFTsQueryOptions } from '@ton/appkit/queries'; import type { GetNFTsData, GetNFTsErrorType, GetNFTsQueryConfig } from '@ton/appkit/queries'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; import { useQuery } from '../../../libs/query'; import type { UseQueryReturnType } from '../../../libs/query'; diff --git a/packages/appkit-react/src/features/nft/hooks/use-transfer-nft.ts b/packages/appkit-react/src/features/nft/hooks/use-transfer-nft.ts index 8d2097f1b..4ebe67a98 100644 --- a/packages/appkit-react/src/features/nft/hooks/use-transfer-nft.ts +++ b/packages/appkit-react/src/features/nft/hooks/use-transfer-nft.ts @@ -19,7 +19,7 @@ import { transferNftMutationOptions } from '@ton/appkit/queries'; import { useMutation } from '../../../libs/query'; import type { UseMutationReturnType } from '../../../libs/query'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; export type UseTransferNftParameters = TransferNftOptions; diff --git a/packages/appkit-react/src/hooks/use-app-kit-theme.ts b/packages/appkit-react/src/features/settings/hooks/use-app-kit-theme.ts similarity index 100% rename from packages/appkit-react/src/hooks/use-app-kit-theme.ts rename to packages/appkit-react/src/features/settings/hooks/use-app-kit-theme.ts diff --git a/packages/appkit-react/src/hooks/use-app-kit.ts b/packages/appkit-react/src/features/settings/hooks/use-app-kit.ts similarity index 85% rename from packages/appkit-react/src/hooks/use-app-kit.ts rename to packages/appkit-react/src/features/settings/hooks/use-app-kit.ts index 2dfa5a34b..2a56599a7 100644 --- a/packages/appkit-react/src/hooks/use-app-kit.ts +++ b/packages/appkit-react/src/features/settings/hooks/use-app-kit.ts @@ -8,7 +8,7 @@ import { useContext } from 'react'; -import { AppKitContext } from '../providers/app-kit-provider'; +import { AppKitContext } from '../../../providers/app-kit-provider'; export function useAppKit() { const context = useContext(AppKitContext); diff --git a/packages/appkit-react/src/hooks/use-i18n.ts b/packages/appkit-react/src/features/settings/hooks/use-i18n.ts similarity index 86% rename from packages/appkit-react/src/hooks/use-i18n.ts rename to packages/appkit-react/src/features/settings/hooks/use-i18n.ts index 015e18f7d..f808e3bb1 100644 --- a/packages/appkit-react/src/hooks/use-i18n.ts +++ b/packages/appkit-react/src/features/settings/hooks/use-i18n.ts @@ -8,7 +8,7 @@ import { useContext } from 'react'; -import { I18nContext } from '../providers/i18n-provider'; +import { I18nContext } from '../../../providers/i18n-provider'; export const useI18n = () => { const i18n = useContext(I18nContext); diff --git a/packages/appkit-react/src/features/settings/index.ts b/packages/appkit-react/src/features/settings/index.ts new file mode 100644 index 000000000..91e69da26 --- /dev/null +++ b/packages/appkit-react/src/features/settings/index.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { useAppKit } from './hooks/use-app-kit'; +export { useAppKitTheme, type AppKitTheme } from './hooks/use-app-kit-theme'; +export { useI18n } from './hooks/use-i18n'; diff --git a/packages/appkit-react/src/features/signing/hooks/use-sign-binary.ts b/packages/appkit-react/src/features/signing/hooks/use-sign-binary.ts index 4b6f41d32..b13a6d30c 100644 --- a/packages/appkit-react/src/features/signing/hooks/use-sign-binary.ts +++ b/packages/appkit-react/src/features/signing/hooks/use-sign-binary.ts @@ -10,7 +10,7 @@ import type { UseMutationResult } from '@tanstack/react-query'; import { signBinaryMutationOptions } from '@ton/appkit/queries'; import type { SignBinaryData, SignBinaryErrorType, SignBinaryOptions, SignBinaryVariables } from '@ton/appkit/queries'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; import { useMutation } from '../../../libs/query'; export type UseSignBinaryParameters = SignBinaryOptions; diff --git a/packages/appkit-react/src/features/signing/hooks/use-sign-cell.ts b/packages/appkit-react/src/features/signing/hooks/use-sign-cell.ts index 0d93b0534..265985a27 100644 --- a/packages/appkit-react/src/features/signing/hooks/use-sign-cell.ts +++ b/packages/appkit-react/src/features/signing/hooks/use-sign-cell.ts @@ -10,7 +10,7 @@ import type { UseMutationResult } from '@tanstack/react-query'; import { signCellMutationOptions } from '@ton/appkit/queries'; import type { SignCellData, SignCellErrorType, SignCellOptions, SignCellVariables } from '@ton/appkit/queries'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; import { useMutation } from '../../../libs/query'; export type UseSignCellParameters = SignCellOptions; diff --git a/packages/appkit-react/src/features/signing/hooks/use-sign-text.ts b/packages/appkit-react/src/features/signing/hooks/use-sign-text.ts index f88f71c53..0845e8f59 100644 --- a/packages/appkit-react/src/features/signing/hooks/use-sign-text.ts +++ b/packages/appkit-react/src/features/signing/hooks/use-sign-text.ts @@ -10,7 +10,7 @@ import type { UseMutationResult } from '@tanstack/react-query'; import { signTextMutationOptions } from '@ton/appkit/queries'; import type { SignTextData, SignTextErrorType, SignTextOptions, SignTextVariables } from '@ton/appkit/queries'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; import { useMutation } from '../../../libs/query'; export type UseSignTextParameters = SignTextOptions; diff --git a/packages/appkit-react/src/features/staking/hooks/use-build-stake-transaction.ts b/packages/appkit-react/src/features/staking/hooks/use-build-stake-transaction.ts index fb77ccb81..c0b5e7250 100644 --- a/packages/appkit-react/src/features/staking/hooks/use-build-stake-transaction.ts +++ b/packages/appkit-react/src/features/staking/hooks/use-build-stake-transaction.ts @@ -14,7 +14,7 @@ import type { BuildStakeTransactionVariables, } from '@ton/appkit/queries'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; import { useMutation } from '../../../libs/query'; export type UseBuildStakeTransactionReturnType = UseMutationResult< diff --git a/packages/appkit-react/src/features/staking/hooks/use-staked-balance.ts b/packages/appkit-react/src/features/staking/hooks/use-staked-balance.ts index afb01b6a5..fb93a8ed0 100644 --- a/packages/appkit-react/src/features/staking/hooks/use-staked-balance.ts +++ b/packages/appkit-react/src/features/staking/hooks/use-staked-balance.ts @@ -9,7 +9,7 @@ import { getStakedBalanceQueryOptions } from '@ton/appkit/queries'; import type { GetStakedBalanceData, GetStakedBalanceErrorType, GetStakedBalanceQueryConfig } from '@ton/appkit/queries'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; import { useQuery } from '../../../libs/query'; import type { UseQueryReturnType } from '../../../libs/query'; diff --git a/packages/appkit-react/src/features/staking/hooks/use-staking-provider-info.ts b/packages/appkit-react/src/features/staking/hooks/use-staking-provider-info.ts index ed528d9b7..92fbd8e32 100644 --- a/packages/appkit-react/src/features/staking/hooks/use-staking-provider-info.ts +++ b/packages/appkit-react/src/features/staking/hooks/use-staking-provider-info.ts @@ -13,7 +13,7 @@ import type { GetStakingProviderInfoQueryConfig, } from '@ton/appkit/queries'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; import { useQuery } from '../../../libs/query'; import type { UseQueryReturnType } from '../../../libs/query'; diff --git a/packages/appkit-react/src/features/staking/hooks/use-staking-providers.ts b/packages/appkit-react/src/features/staking/hooks/use-staking-providers.ts index fc97cfdee..d5f0573c5 100644 --- a/packages/appkit-react/src/features/staking/hooks/use-staking-providers.ts +++ b/packages/appkit-react/src/features/staking/hooks/use-staking-providers.ts @@ -13,7 +13,7 @@ import type { GetStakingProvidersQueryConfig, } from '@ton/appkit/queries'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; import { useQuery } from '../../../libs/query'; import type { UseQueryReturnType } from '../../../libs/query'; diff --git a/packages/appkit-react/src/features/staking/hooks/use-staking-quote.ts b/packages/appkit-react/src/features/staking/hooks/use-staking-quote.ts index c011c67a4..194765672 100644 --- a/packages/appkit-react/src/features/staking/hooks/use-staking-quote.ts +++ b/packages/appkit-react/src/features/staking/hooks/use-staking-quote.ts @@ -9,7 +9,7 @@ import { getStakingQuoteQueryOptions } from '@ton/appkit/queries'; import type { GetStakingQuoteData, GetStakingQuoteErrorType, GetStakingQuoteQueryConfig } from '@ton/appkit/queries'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; import { useQuery } from '../../../libs/query'; import type { UseQueryReturnType } from '../../../libs/query'; diff --git a/packages/appkit-react/src/features/swap/components/swap-field/index.ts b/packages/appkit-react/src/features/swap/components/swap-field/index.ts new file mode 100644 index 000000000..cd1af50f3 --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/swap-field/index.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export * from './swap-field'; diff --git a/packages/appkit-react/src/features/swap/components/swap-field/swap-field.module.css b/packages/appkit-react/src/features/swap/components/swap-field/swap-field.module.css new file mode 100644 index 000000000..e975e7cb0 --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/swap-field/swap-field.module.css @@ -0,0 +1,51 @@ +.container { + composes: block from "../../../../components/block/block.module.css"; + + width: 100%; + border: var(--ta-border-width-m) solid transparent; + transition: border-color 0.2s; + gap: 8px; +} + +.container .header, +.container .caption { + padding-left: 0; + padding-right: 0; +} + +.container:focus-within { + border-color: var(--ta-color-primary); +} + +.balanceLine { + composes: bodyRegular from "../../../../styles/typography.module.css"; + + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; +} + +.maxButton { + composes: bodyRegular from "../../../../styles/typography.module.css"; + + background: none; + border: none; + cursor: pointer; + padding: 0; + color: var(--ta-color-primary); + font-weight: 600; +} + +.balanceWrapper { + display: inline-flex; + align-items: center; + gap: 4px; + height: 1lh; +} + +.skeletonText { + width: 5ch; + height: 1em; + height: 0.8lh; +} diff --git a/packages/appkit-react/src/features/swap/components/swap-field/swap-field.tsx b/packages/appkit-react/src/features/swap/components/swap-field/swap-field.tsx new file mode 100644 index 000000000..ef7fc7400 --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/swap-field/swap-field.tsx @@ -0,0 +1,101 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { FC } from 'react'; +import { calcFiatValue, formatLargeValue } from '@ton/appkit'; + +import { useI18n } from '../../../settings/hooks/use-i18n'; +import { Input } from '../../../../components/input/input'; +import { Skeleton } from '../../../../components/skeleton'; +import { TokenSelector } from '../token-selector/token-selector'; +import type { AppkitUIToken } from '../../../../types/appkit-ui-token'; +import { useSwapContext } from '../swap-widget-provider/swap-widget-provider'; +import styles from './swap-field.module.css'; + +export interface SwapFieldProps { + type: 'pay' | 'receive'; + amount: string; + token?: AppkitUIToken; + onAmountChange?: (value: string) => void; + balance?: string; + loading?: boolean; + onMaxClick?: () => void; + onTokenSelectorClick?: () => void; + isWalletConnected?: boolean; +} + +export const SwapField: FC = ({ + type, + token, + amount, + onAmountChange, + balance, + loading, + onMaxClick, + onTokenSelectorClick, + isWalletConnected, +}) => { + const { t } = useI18n(); + const { fiatSymbol } = useSwapContext(); + + const tokenSymbol = token?.symbol || ''; + const displayDecimals = token ? Math.min(token.decimals, 5) : 5; + + return ( + + + {type === 'pay' ? t('swap.pay') : t('swap.receive')} + + + + onAmountChange(e.target.value))} + disabled={type === 'receive'} + /> + + + + + + +
+ + {token?.rate && + `${fiatSymbol} ${formatLargeValue(calcFiatValue(amount || '0', token.rate), 2)}`} + + {type === 'pay' && ( + + {balance || !isWalletConnected ? ( + <> + {t('swap.max')} + + + ) : ( + + )} + + )} + + {type === 'receive' && ( + + {balance || !isWalletConnected ? ( + `${formatLargeValue(balance || '0', displayDecimals)} ${tokenSymbol}` + ) : ( + + )} + + )} +
+
+
+ ); +}; diff --git a/packages/appkit-react/src/features/swap/components/swap-flip-button/index.ts b/packages/appkit-react/src/features/swap/components/swap-flip-button/index.ts new file mode 100644 index 000000000..1f0e1ea03 --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/swap-flip-button/index.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export * from './swap-flip-button'; diff --git a/packages/appkit-react/src/features/swap/components/swap-flip-button/swap-flip-button.module.css b/packages/appkit-react/src/features/swap/components/swap-flip-button/swap-flip-button.module.css new file mode 100644 index 000000000..598812834 --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/swap-flip-button/swap-flip-button.module.css @@ -0,0 +1,42 @@ +.container { + background-color: var(--ta-color-background-secondary); + border-radius: 50%; + width: 44px; + height: 44px; + display: flex; + align-items: center; + justify-content: center; +} + +.flipButton { + width: 44px; + height: 44px; + padding: 0; + border-radius: 50%; + cursor: pointer; + transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.2s; + box-shadow: var(--ta-shadow-m); + appearance: none; + border: none; + outline: none; + cursor: pointer; + box-sizing: border-box; + display: inline-flex; + align-items: center; + justify-content: center; + text-decoration: none; + background-color: var(--ta-color-background-tertiary); + color: var(--ta-color-text); +} + +.flipButton:hover { + opacity: 1; +} + +.flipButton:focus { + opacity: 1; +} + +.flipButton.rotated { + transform: rotate(180deg); +} diff --git a/packages/appkit-react/src/features/swap/components/swap-flip-button/swap-flip-button.tsx b/packages/appkit-react/src/features/swap/components/swap-flip-button/swap-flip-button.tsx new file mode 100644 index 000000000..f7f73d79d --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/swap-flip-button/swap-flip-button.tsx @@ -0,0 +1,43 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { ComponentProps, FC } from 'react'; +import clsx from 'clsx'; + +import styles from './swap-flip-button.module.css'; +import { Button } from '../../../../components/button'; + +export interface SwapFlipButtonProps extends ComponentProps<'div'> { + onClick?: () => void; + rotated?: boolean; +} + +export const SwapFlipButton: FC = ({ onClick, rotated, ...props }) => { + return ( +
+ +
+ ); +}; diff --git a/packages/appkit-react/src/features/swap/components/swap-info/index.ts b/packages/appkit-react/src/features/swap/components/swap-info/index.ts new file mode 100644 index 000000000..706f1854b --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/swap-info/index.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export * from './swap-info'; diff --git a/packages/appkit-react/src/features/swap/components/swap-info/swap-info.tsx b/packages/appkit-react/src/features/swap/components/swap-info/swap-info.tsx new file mode 100644 index 000000000..3f53b2260 --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/swap-info/swap-info.tsx @@ -0,0 +1,41 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { FC } from 'react'; + +import { InfoBlock } from '../../../../components/info-block'; + +export interface SwapInfoRowProps { + label: string; + value: string; +} + +export interface SwapInfoProps { + rows: SwapInfoRowProps[]; + isLoading?: boolean; +} + +export const SwapInfo: FC = ({ rows, isLoading }) => { + return ( + + {isLoading + ? Array.from({ length: 3 }).map((_, idx) => ( + + + + + )) + : rows.map((row, idx) => ( + + {row.label} + {row.value} + + ))} + + ); +}; diff --git a/packages/appkit-react/src/features/swap/components/swap-settings-button/index.ts b/packages/appkit-react/src/features/swap/components/swap-settings-button/index.ts new file mode 100644 index 000000000..7f9a68ad8 --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/swap-settings-button/index.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export * from './swap-settings-button'; diff --git a/packages/appkit-react/src/features/swap/components/swap-settings-button/swap-settings-button.module.css b/packages/appkit-react/src/features/swap/components/swap-settings-button/swap-settings-button.module.css new file mode 100644 index 000000000..7544b841b --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/swap-settings-button/swap-settings-button.module.css @@ -0,0 +1,11 @@ +.settingsButton { + padding: 4px; + width: 32px; + height: 32px; + border-radius: var(--ta-border-radius-m); +} + +.settingsButton svg { + width: 24px; + height: 24px; +} diff --git a/packages/appkit-react/src/features/swap/components/swap-settings-button/swap-settings-button.tsx b/packages/appkit-react/src/features/swap/components/swap-settings-button/swap-settings-button.tsx new file mode 100644 index 000000000..ed7900dd2 --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/swap-settings-button/swap-settings-button.tsx @@ -0,0 +1,43 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { FC } from 'react'; + +import { Button } from '../../../../components/button'; +import styles from './swap-settings-button.module.css'; + +export interface SwapSettingsButtonProps { + onClick?: () => void; +} + +export const SwapSettingsButton: FC = ({ onClick }) => { + return ( + + ); +}; diff --git a/packages/appkit-react/src/features/swap/components/swap-settings-modal/index.ts b/packages/appkit-react/src/features/swap/components/swap-settings-modal/index.ts new file mode 100644 index 000000000..932fe83c8 --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/swap-settings-modal/index.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { SwapSettingsModal } from './swap-settings-modal'; +export type { SwapSettingsModalProps } from './swap-settings-modal'; diff --git a/packages/appkit-react/src/features/swap/components/swap-settings-modal/swap-settings-modal.module.css b/packages/appkit-react/src/features/swap/components/swap-settings-modal/swap-settings-modal.module.css new file mode 100644 index 000000000..8b3d3c7dc --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/swap-settings-modal/swap-settings-modal.module.css @@ -0,0 +1,32 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +.label { + margin: 0 0 8px; + padding: 0 4px; + color: var(--ta-color-text-secondary); + font-size: 14px; +} + +.row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.presetBtn { + flex: 1; +} + +.warning { + margin: 8px 0 0; + padding: 0 4px; + font-size: 12px; + line-height: 1.4; +} diff --git a/packages/appkit-react/src/features/swap/components/swap-settings-modal/swap-settings-modal.tsx b/packages/appkit-react/src/features/swap/components/swap-settings-modal/swap-settings-modal.tsx new file mode 100644 index 000000000..ab8379427 --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/swap-settings-modal/swap-settings-modal.tsx @@ -0,0 +1,57 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { FC } from 'react'; +import clsx from 'clsx'; + +import { Modal } from '../../../../components/modal/modal'; +import { useI18n } from '../../../settings/hooks/use-i18n'; +import styles from './swap-settings-modal.module.css'; +import { Button } from '../../../../components/button'; + +/** Preset slippage values in basis points */ +const SLIPPAGE_PRESETS = [50, 100, 200] as const; + +const WARNING_BPS = 500; + +const formatSlippage = (bps: number): string => { + return `${(bps / 100).toFixed(2)}%`; +}; + +export interface SwapSettingsModalProps { + open: boolean; + onClose: () => void; + slippage: number; + onSlippageChange: (bps: number) => void; +} + +export const SwapSettingsModal: FC = ({ open, onClose, slippage, onSlippageChange }) => { + const { t } = useI18n(); + + return ( + !isOpen && onClose()} title={t('swap.settings')}> +

{t('swap.slippage')}

+ +
+ {SLIPPAGE_PRESETS.map((preset) => ( + + ))} +
+ + {slippage > WARNING_BPS &&

{t('swap.slippageWarning')}

} +
+ ); +}; diff --git a/packages/appkit-react/src/features/swap/components/swap-token-select-modal/index.ts b/packages/appkit-react/src/features/swap/components/swap-token-select-modal/index.ts new file mode 100644 index 000000000..042df4c7d --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/swap-token-select-modal/index.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { SwapTokenSelectModal } from './swap-token-select-modal'; +export type { SwapTokenSelectModalProps } from './swap-token-select-modal'; diff --git a/packages/appkit-react/src/features/swap/components/swap-token-select-modal/swap-token-select-modal.module.css b/packages/appkit-react/src/features/swap/components/swap-token-select-modal/swap-token-select-modal.module.css new file mode 100644 index 000000000..fd1da9e6d --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/swap-token-select-modal/swap-token-select-modal.module.css @@ -0,0 +1,67 @@ +.searchWrapper { + margin-bottom: 16px; +} + +.list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 16px; + max-height: 400px; + overflow-y: auto; +} + +.item { + display: flex; + align-items: center; + gap: 12px; + width: 100%; + padding: 10px 12px; + border: none; + background: none; + border-radius: var(--ta-border-radius-l); + cursor: pointer; + transition: background-color 0.15s; + text-align: left; +} + +.item:hover { + background: var(--ta-color-background-secondary); +} + +.tokenIcon { + width: 36px; + height: 36px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + flex-shrink: 0; +} + +.tokenInfo { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.tokenSymbol { + composes: bodySemibold from "../../../../styles/typography.module.css"; + color: var(--ta-color-text); +} + +.tokenName { + composes: footnoteRegular from "../../../../styles/typography.module.css"; + color: var(--ta-color-text-secondary); +} + +.tokenBalance { + composes: bodyRegular from "../../../../styles/typography.module.css"; + color: var(--ta-color-text-secondary); + flex-shrink: 0; +} diff --git a/packages/appkit-react/src/features/swap/components/swap-token-select-modal/swap-token-select-modal.tsx b/packages/appkit-react/src/features/swap/components/swap-token-select-modal/swap-token-select-modal.tsx new file mode 100644 index 000000000..f4195b990 --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/swap-token-select-modal/swap-token-select-modal.tsx @@ -0,0 +1,21 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { FC } from 'react'; + +import { TokenSelectModal } from '../../../../components/token-select-modal'; +import type { TokenSelectModalProps } from '../../../../components/token-select-modal'; +import { useI18n } from '../../../settings/hooks/use-i18n'; + +export type SwapTokenSelectModalProps = Omit; + +export const SwapTokenSelectModal: FC = (props) => { + const { t } = useI18n(); + + return ; +}; diff --git a/packages/appkit-react/src/features/swap/components/swap-widget-provider/index.ts b/packages/appkit-react/src/features/swap/components/swap-widget-provider/index.ts new file mode 100644 index 000000000..33620204f --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/swap-widget-provider/index.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export * from './swap-widget-provider'; diff --git a/packages/appkit-react/src/features/swap/components/swap-widget-provider/swap-widget-provider.tsx b/packages/appkit-react/src/features/swap/components/swap-widget-provider/swap-widget-provider.tsx new file mode 100644 index 000000000..22be6fbdf --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/swap-widget-provider/swap-widget-provider.tsx @@ -0,0 +1,256 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { createContext, useCallback, useContext, useMemo, useState } from 'react'; +import type { FC, PropsWithChildren } from 'react'; +import type { Network } from '@ton/appkit'; +import type { GetSwapQuoteData } from '@ton/appkit/queries'; + +import { useSwapQuote } from '../../hooks/use-swap-quote'; +import { useBuildSwapTransaction } from '../../hooks/use-build-swap-transaction'; +import { useSelectedWallet, useAddress } from '../../../wallets'; +import { useSendTransaction } from '../../../transaction/hooks/use-send-transaction'; +import { useDebounceValue } from '../../../../hooks/use-debounce-value'; +import type { AppkitUIToken } from '../../../../types/appkit-ui-token'; +import { mapSwapWidgetTokens } from '../../utils/map-swap-widget-tokens'; +import { useSwapTokenState } from './use-swap-token-state'; +import { useSwapBalances } from './use-swap-balances'; +import { useSwapValidation } from './use-swap-validation'; + +export type { AppkitUIToken }; + +export type SwapWidgetError = 'insufficientBalance' | 'tooManyDecimals' | 'quoteError' | null; + +export interface SwapContextType { + /** Full list of available tokens */ + tokens: AppkitUIToken[]; + /** Currently selected "from" token */ + fromToken: AppkitUIToken | null; + /** Currently selected "to" token */ + toToken: AppkitUIToken | null; + /** Amount the user wants to swap (string to preserve input UX) */ + fromAmount: string; + /** Calculated receive amount from the quote */ + toAmount: string; + /** Fiat currency symbol, e.g. "$" */ + fiatSymbol: string; + /** Balance of the "from" token for the connected wallet */ + fromBalance: string | undefined; + /** Balance of the "to" token for the connected wallet */ + toBalance: string | undefined; + /** Whether the user can proceed with the swap */ + canSubmit: boolean; + /** Whether a wallet is currently connected */ + isWalletConnected: boolean; + /** Raw swap quote from the provider */ + quote: GetSwapQuoteData | undefined; + /** True while the quote is being fetched */ + isQuoteLoading: boolean; + /** Current validation/fetch error, null when everything is ok */ + error: SwapWidgetError; + /** Slippage tolerance in basis points (100 = 1%) */ + slippage: number; + setFromToken: (token: AppkitUIToken) => void; + setToToken: (token: AppkitUIToken) => void; + setFromAmount: (amount: string) => void; + setSlippage: (slippage: number) => void; + onFlip: () => void; + onMaxClick: () => void; + sendSwapTransaction: () => Promise; + isSendingTransaction: boolean; +} + +export const SwapContext = createContext({ + tokens: [], + fromToken: null, + toToken: null, + fromAmount: '', + toAmount: '', + fiatSymbol: '$', + fromBalance: undefined, + toBalance: undefined, + canSubmit: false, + isWalletConnected: false, + quote: undefined, + isQuoteLoading: false, + error: null, + slippage: 50, + setFromToken: () => {}, + setToToken: () => {}, + setFromAmount: () => {}, + setSlippage: () => {}, + onFlip: () => {}, + onMaxClick: () => {}, + sendSwapTransaction: () => Promise.resolve(), + isSendingTransaction: false, +}); + +export function useSwapContext() { + return useContext(SwapContext); +} + +export interface SwapProviderProps extends PropsWithChildren { + /** Full list of tokens available for swapping */ + tokens: AppkitUIToken[]; + /** Network to use for quote fetching, defaults to mainnet */ + network: Network; + /** Fiat currency symbol shown next to amounts, defaults to "$" */ + fiatSymbol?: string; + /** Symbol of the token pre-selected in the "from" field */ + defaultFromSymbol?: string; + /** Symbol of the token pre-selected in the "to" field */ + defaultToSymbol?: string; + /** Initial slippage in basis points (100 = 1%), defaults to 50 (0.5%) */ + defaultSlippage?: number; +} + +export const SwapWidgetProvider: FC = ({ + children, + tokens, + network, + fiatSymbol = '$', + defaultFromSymbol, + defaultToSymbol, + defaultSlippage = 100, +}) => { + const mappedTokens = useMemo(() => mapSwapWidgetTokens(tokens), [tokens]); + + const { fromToken, toToken, fromAmount, setFromToken, setToToken, setFromAmount, onFlip } = useSwapTokenState({ + mappedTokens, + defaultFromSymbol, + defaultToSymbol, + }); + + const [slippage, setSlippage] = useState(defaultSlippage); + + const fromTokenParam = useMemo( + () => + fromToken + ? { + address: fromToken.address, + decimals: fromToken.decimals, + symbol: fromToken.symbol, + name: fromToken.name, + } + : undefined, + [fromToken], + ); + + const toTokenParam = useMemo( + () => + toToken + ? { address: toToken.address, decimals: toToken.decimals, symbol: toToken.symbol, name: toToken.name } + : undefined, + [toToken], + ); + + const [fromAmountDebounced] = useDebounceValue(fromAmount, 500); + + const { + data: quote, + isFetching: isQuoteLoading, + error: quoteError, + } = useSwapQuote({ + from: fromTokenParam, + to: toTokenParam, + amount: fromAmountDebounced, + network, + slippageBps: slippage, + }); + + const toAmount = quote?.toAmount ?? ''; + + const [wallet] = useSelectedWallet(); + const isWalletConnected = wallet !== null; + const address = useAddress(); + + const { fromBalance, toBalance } = useSwapBalances({ + fromToken, + toToken, + ownerAddress: address ?? undefined, + }); + + const handleMaxClick = useCallback(() => { + if (fromBalance) { + setFromAmount(fromBalance.replace(/\s/g, '')); + } + }, [fromBalance, setFromAmount]); + + const { mutateAsync: buildTransaction } = useBuildSwapTransaction(); + const { mutateAsync: sendTransaction, isPending: isSendingTransaction } = useSendTransaction(); + + const sendSwapTransaction = useCallback(async () => { + if (!quote || !address) return; + + const transactionParams = await buildTransaction({ quote, userAddress: address }); + + await sendTransaction(transactionParams); + }, [quote, address, buildTransaction, sendTransaction]); + + const { error, canSubmit } = useSwapValidation({ + fromAmount, + fromAmountDebounced, + fromToken, + toToken, + fromBalance, + quoteError, + }); + + const value = useMemo( + () => ({ + tokens: mappedTokens, + fromToken, + toToken, + fromAmount, + toAmount, + fiatSymbol, + fromBalance, + toBalance, + canSubmit, + isWalletConnected, + quote, + isQuoteLoading, + error, + slippage, + setFromToken, + setToToken, + setFromAmount, + setSlippage, + onFlip, + onMaxClick: handleMaxClick, + sendSwapTransaction, + isSendingTransaction, + }), + [ + mappedTokens, + fromToken, + toToken, + fromAmount, + toAmount, + fiatSymbol, + fromBalance, + toBalance, + canSubmit, + isWalletConnected, + quote, + isQuoteLoading, + error, + slippage, + setFromToken, + setToToken, + setFromAmount, + setSlippage, + onFlip, + handleMaxClick, + sendSwapTransaction, + isSendingTransaction, + ], + ); + + return {children}; +}; diff --git a/packages/appkit-react/src/features/swap/components/swap-widget-provider/use-swap-balances.ts b/packages/appkit-react/src/features/swap/components/swap-widget-provider/use-swap-balances.ts new file mode 100644 index 000000000..2ed23d645 --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/swap-widget-provider/use-swap-balances.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useBalance } from '../../../balances/hooks/use-balance'; +import { useJettonBalanceByAddress } from '../../../jettons/hooks/use-jetton-balance-by-address'; +import type { AppkitUIToken } from '../../../../types/appkit-ui-token'; + +interface UseSwapBalancesOptions { + fromToken: AppkitUIToken | null; + toToken: AppkitUIToken | null; + ownerAddress: string | undefined; +} + +export function useSwapBalances({ fromToken, toToken, ownerAddress }: UseSwapBalancesOptions) { + const isFromNative = fromToken?.address === 'ton'; + const isToNative = toToken?.address === 'ton'; + + const { data: tonBalance } = useBalance(); + + const { data: fromJettonBalance } = useJettonBalanceByAddress({ + jettonAddress: fromToken?.address, + ownerAddress, + jettonDecimals: fromToken?.decimals, + query: { enabled: !isFromNative && !!fromToken }, + }); + + const { data: toJettonBalance } = useJettonBalanceByAddress({ + jettonAddress: toToken?.address, + ownerAddress, + jettonDecimals: toToken?.decimals, + query: { enabled: !isToNative && !!toToken }, + }); + + return { + fromBalance: isFromNative ? tonBalance : fromJettonBalance, + toBalance: isToNative ? tonBalance : toJettonBalance, + }; +} diff --git a/packages/appkit-react/src/features/swap/components/swap-widget-provider/use-swap-token-state.ts b/packages/appkit-react/src/features/swap/components/swap-widget-provider/use-swap-token-state.ts new file mode 100644 index 000000000..0ade8e0d1 --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/swap-widget-provider/use-swap-token-state.ts @@ -0,0 +1,79 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useCallback, useState } from 'react'; +import { validateNumericString } from '@ton/appkit'; + +import type { AppkitUIToken } from '../../../../types/appkit-ui-token'; +import { truncateDecimals } from '../../utils/truncate-decimals'; + +interface UseSwapTokenStateOptions { + mappedTokens: AppkitUIToken[]; + defaultFromSymbol?: string; + defaultToSymbol?: string; +} + +export function useSwapTokenState({ mappedTokens, defaultFromSymbol, defaultToSymbol }: UseSwapTokenStateOptions) { + const [fromToken, setFromToken] = useState( + mappedTokens.find((t) => t.symbol.toLowerCase() === defaultFromSymbol?.toLowerCase()) ?? + mappedTokens[0] ?? + null, + ); + const [toToken, setToToken] = useState( + mappedTokens.find((t) => t.symbol.toLowerCase() === defaultToSymbol?.toLowerCase()) ?? mappedTokens[1] ?? null, + ); + const [fromAmount, setFromAmountRaw] = useState(''); + + const setFromAmount = useCallback( + (value: string) => { + if (value === '' || validateNumericString(value, fromToken?.decimals)) { + setFromAmountRaw(value); + } + }, + [fromToken?.decimals], + ); + + const handleSetFromToken = useCallback( + (token: AppkitUIToken) => { + if (toToken && token.address === toToken.address) { + setToToken(fromToken); + } + setFromToken(token); + setFromAmountRaw((prev) => truncateDecimals(prev, token.decimals)); + }, + [fromToken, toToken], + ); + + const handleSetToToken = useCallback( + (token: AppkitUIToken) => { + if (fromToken && token.address === fromToken.address) { + setFromToken(toToken); + } + setToToken(token); + }, + [fromToken, toToken], + ); + + const onFlip = useCallback(() => { + setFromToken(toToken); + setToToken(fromToken); + if (toToken) { + setFromAmountRaw((prev) => truncateDecimals(prev, toToken.decimals)); + } + }, [fromToken, toToken]); + + return { + fromToken, + toToken, + fromAmount, + setFromToken: handleSetFromToken, + setToToken: handleSetToToken, + setFromAmount, + onFlip, + }; +} diff --git a/packages/appkit-react/src/features/swap/components/swap-widget-provider/use-swap-validation.ts b/packages/appkit-react/src/features/swap/components/swap-widget-provider/use-swap-validation.ts new file mode 100644 index 000000000..afae46282 --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/swap-widget-provider/use-swap-validation.ts @@ -0,0 +1,54 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useMemo } from 'react'; + +import type { AppkitUIToken } from '../../../../types/appkit-ui-token'; +import type { SwapWidgetError } from './swap-widget-provider'; + +interface UseSwapValidationOptions { + fromAmount: string; + fromAmountDebounced: string; + fromToken: AppkitUIToken | null; + toToken: AppkitUIToken | null; + fromBalance: string | undefined; + quoteError: Error | null; +} + +export function useSwapValidation({ + fromAmount, + fromAmountDebounced, + fromToken, + toToken, + fromBalance, + quoteError, +}: UseSwapValidationOptions) { + const error: SwapWidgetError = useMemo(() => { + const amount = parseFloat(fromAmount) || 0; + if (amount <= 0) return null; + + const fraction = fromAmount.split('.')[1]; + if (fraction && fromToken && fraction.length > fromToken.decimals) { + return 'tooManyDecimals'; + } + + if (fromBalance !== undefined && amount > parseFloat(fromBalance)) { + return 'insufficientBalance'; + } + + if (quoteError && fromAmountDebounced) { + return 'quoteError'; + } + + return null; + }, [fromAmount, fromToken, fromBalance, quoteError, fromAmountDebounced]); + + const canSubmit = (parseFloat(fromAmount) || 0) > 0 && fromToken !== null && toToken !== null && error === null; + + return { error, canSubmit }; +} diff --git a/packages/appkit-react/src/features/swap/components/swap-widget-ui/index.ts b/packages/appkit-react/src/features/swap/components/swap-widget-ui/index.ts new file mode 100644 index 000000000..8940f4663 --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/swap-widget-ui/index.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export * from './swap-widget-ui'; diff --git a/packages/appkit-react/src/features/swap/components/swap-widget-ui/swap-widget-ui.module.css b/packages/appkit-react/src/features/swap/components/swap-widget-ui/swap-widget-ui.module.css new file mode 100644 index 000000000..c54adaca6 --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/swap-widget-ui/swap-widget-ui.module.css @@ -0,0 +1,44 @@ +.widget { + box-sizing: border-box; + width: 100%; + max-width: 440px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.headerTitle { + composes: title from "../../../../styles/typography.module.css"; + color: var(--ta-color-text); + margin: 0; +} + +.card { + position: relative; + border-radius: var(--ta-border-radius-xl); +} + +.fieldsContainer { + box-sizing: border-box; + width: 100%; + display: flex; + flex-direction: column; + position: relative; + gap: 8px; +} + +/* Flip Button positioning */ +.flipButtonWrapper { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 10; +} diff --git a/packages/appkit-react/src/features/swap/components/swap-widget-ui/swap-widget-ui.tsx b/packages/appkit-react/src/features/swap/components/swap-widget-ui/swap-widget-ui.tsx new file mode 100644 index 000000000..d088a1354 --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/swap-widget-ui/swap-widget-ui.tsx @@ -0,0 +1,144 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useCallback, useState } from 'react'; +import type { FC } from 'react'; + +import { Button } from '../../../../components/button'; +import { useI18n } from '../../../settings/hooks/use-i18n'; +import { useConnect, useConnectors } from '../../../wallets'; +import { SwapField } from '../swap-field'; +import { SwapFlipButton } from '../swap-flip-button'; +import { SwapInfo } from '../swap-info'; +import { SwapSettingsButton } from '../swap-settings-button'; +import { SwapSettingsModal } from '../swap-settings-modal'; +import { SwapTokenSelectModal } from '../swap-token-select-modal'; +import styles from './swap-widget-ui.module.css'; +import { getInfoFromQuote } from '../../utils/get-info-from-quote'; +import type { SwapContextType } from '../swap-widget-provider'; +import { useSwapProvider } from '../../hooks/use-swap-provider'; + +export type SwapWidgetRenderProps = SwapContextType; + +export const SwapWidgetUI: FC = ({ + fromToken, + toToken, + tokens, + fromAmount, + toAmount, + fromBalance, + toBalance, + canSubmit, + isWalletConnected, + quote, + isQuoteLoading, + error, + slippage, + onFlip, + onMaxClick, + setFromAmount, + setFromToken, + setToToken, + setSlippage, + sendSwapTransaction, + isSendingTransaction, +}) => { + const connectors = useConnectors(); + const { mutate: connect, isPending: isConnecting } = useConnect(); + const { t } = useI18n(); + const [activeField, setActiveField] = useState<'from' | 'to' | null>(null); + const [isSettingsOpen, setIsSettingsOpen] = useState(false); + const [isFlipped, setIsFlipped] = useState(false); + + const handleFlip = useCallback(() => { + setIsFlipped((prev) => !prev); + onFlip(); + }, [onFlip]); + + const provider = useSwapProvider({ id: quote?.providerId }); + const infoRows = getInfoFromQuote({ quote, slippage, provider, toToken }); + + return ( +
+
+

{t('swap.title')}

+ setIsSettingsOpen(true)} /> +
+ +
+ setActiveField('from')} + isWalletConnected={isWalletConnected} + /> + +
+ +
+ + setActiveField('to')} + loading={isQuoteLoading} + isWalletConnected={isWalletConnected} + /> +
+ + setActiveField(null)} + tokens={tokens} + onSelect={(token) => { + if (activeField === 'from') setFromToken(token); + else setToToken(token); + }} + /> + + setIsSettingsOpen(false)} + slippage={slippage} + onSlippageChange={setSlippage} + /> + + {fromAmount && } + + {isWalletConnected ? ( + + ) : ( + + )} +
+ ); +}; diff --git a/packages/appkit-react/src/features/swap/components/swap-widget/index.ts b/packages/appkit-react/src/features/swap/components/swap-widget/index.ts new file mode 100644 index 000000000..f9e0bd6cd --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/swap-widget/index.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export * from './swap-widget'; diff --git a/packages/appkit-react/src/features/swap/components/swap-widget/swap-widget.stories.tsx b/packages/appkit-react/src/features/swap/components/swap-widget/swap-widget.stories.tsx new file mode 100644 index 000000000..ec8064239 --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/swap-widget/swap-widget.stories.tsx @@ -0,0 +1,73 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { Network } from '@ton/appkit'; + +import { STORY_TOKENS } from '../../../../storybook/fixtures/tokens'; +import { SwapWidget } from './swap-widget'; + +const meta: Meta = { + title: 'Public/Features/Swap/SwapWidget', + component: SwapWidget, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + tokens: STORY_TOKENS, + network: Network.mainnet(), + fiatSymbol: '$', + defaultFromSymbol: 'TON', + defaultToSymbol: 'USDT', + }, +}; + +export const CustomUI: Story = { + args: { + tokens: STORY_TOKENS, + network: Network.mainnet(), + fiatSymbol: '$', + defaultFromSymbol: 'TON', + defaultToSymbol: 'USDT', + }, + render: (args) => ( + + {({ fromToken, toToken, fromAmount, toAmount, isQuoteLoading, canSubmit, setFromAmount, onFlip }) => ( +
+
+ + setFromAmount(e.target.value)} placeholder="0" /> +
+ +
+ + +
+ +
+ )} +
+ ), +}; diff --git a/packages/appkit-react/src/features/swap/components/swap-widget/swap-widget.tsx b/packages/appkit-react/src/features/swap/components/swap-widget/swap-widget.tsx new file mode 100644 index 000000000..6f7786208 --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/swap-widget/swap-widget.tsx @@ -0,0 +1,52 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { FC, ReactNode } from 'react'; + +import type { SwapWidgetRenderProps } from '../swap-widget-ui'; +import { SwapWidgetUI } from '../swap-widget-ui'; +import { SwapWidgetProvider, useSwapContext } from '../swap-widget-provider'; +import type { SwapProviderProps } from '../swap-widget-provider'; + +export interface SwapWidgetProps extends Omit { + /** Custom render function — when provided, replaces the default widget UI */ + children?: (props: SwapWidgetRenderProps) => ReactNode; +} + +const SwapWidgetContent: FC<{ children?: (props: SwapWidgetRenderProps) => ReactNode }> = ({ children }) => { + const ctx = useSwapContext(); + + if (children) { + return <>{children(ctx)}; + } + + return ; +}; + +export const SwapWidget: FC = ({ + children, + tokens, + network, + fiatSymbol, + defaultFromSymbol, + defaultToSymbol, + defaultSlippage, +}) => { + return ( + + {children} + + ); +}; diff --git a/packages/appkit-react/src/features/swap/components/token-selector/index.ts b/packages/appkit-react/src/features/swap/components/token-selector/index.ts new file mode 100644 index 000000000..9801a9e75 --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/token-selector/index.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export * from './token-selector'; diff --git a/packages/appkit-react/src/features/swap/components/token-selector/token-selector.module.css b/packages/appkit-react/src/features/swap/components/token-selector/token-selector.module.css new file mode 100644 index 000000000..4619cdc0a --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/token-selector/token-selector.module.css @@ -0,0 +1,12 @@ +.tokenSelector { + padding: 8px; +} + +.tokenSelector:hover { + opacity: 0.8; +} + +.chevron { + opacity: 0.5; + margin-left: 2px; +} diff --git a/packages/appkit-react/src/features/swap/components/token-selector/token-selector.tsx b/packages/appkit-react/src/features/swap/components/token-selector/token-selector.tsx new file mode 100644 index 000000000..2f4cfadb0 --- /dev/null +++ b/packages/appkit-react/src/features/swap/components/token-selector/token-selector.tsx @@ -0,0 +1,37 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { FC } from 'react'; + +import styles from './token-selector.module.css'; +import { Button } from '../../../../components/button'; +import { Logo } from '../../../../components/logo'; + +export interface TokenSelectorProps { + symbol: string; + icon?: string; + onClick?: () => void; +} + +export const TokenSelector: FC = ({ symbol, icon, onClick }) => { + return ( + + ); +}; diff --git a/packages/appkit-react/src/features/swap/hooks/use-build-swap-transaction.ts b/packages/appkit-react/src/features/swap/hooks/use-build-swap-transaction.ts index 2152f5d54..c5f9af939 100644 --- a/packages/appkit-react/src/features/swap/hooks/use-build-swap-transaction.ts +++ b/packages/appkit-react/src/features/swap/hooks/use-build-swap-transaction.ts @@ -17,7 +17,7 @@ import type { BuildSwapTransactionVariables, } from '@ton/appkit/queries'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; import { useMutation } from '../../../libs/query'; export type UseBuildSwapTransactionParameters = BuildSwapTransactionMutationOptions; diff --git a/packages/appkit-react/src/features/swap/hooks/use-swap-provider.ts b/packages/appkit-react/src/features/swap/hooks/use-swap-provider.ts new file mode 100644 index 000000000..c0d89eff4 --- /dev/null +++ b/packages/appkit-react/src/features/swap/hooks/use-swap-provider.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useSyncExternalStore, useCallback } from 'react'; +import { getSwapProvider, watchSwapProviders } from '@ton/appkit'; +import type { GetSwapProviderOptions, GetSwapProviderReturnType } from '@ton/appkit'; + +import { useAppKit } from '../../settings/hooks/use-app-kit'; + +export type UseSwapProviderReturnType = GetSwapProviderReturnType; + +/** + * Hook to get swap provider + */ +export const useSwapProvider = (options: GetSwapProviderOptions = {}): UseSwapProviderReturnType | undefined => { + const appKit = useAppKit(); + const { id } = options; + + const subscribe = useCallback( + (onChange: () => void) => { + return watchSwapProviders(appKit, { onChange }); + }, + [appKit], + ); + + const getSnapshot = useCallback(() => { + try { + return getSwapProvider(appKit, { id }); + } catch { + return undefined; + } + }, [appKit, id]); + + return useSyncExternalStore(subscribe, getSnapshot, getSnapshot); +}; diff --git a/packages/appkit-react/src/features/swap/hooks/use-swap-quote.ts b/packages/appkit-react/src/features/swap/hooks/use-swap-quote.ts index 16e20734b..bd39f58b2 100644 --- a/packages/appkit-react/src/features/swap/hooks/use-swap-quote.ts +++ b/packages/appkit-react/src/features/swap/hooks/use-swap-quote.ts @@ -11,7 +11,7 @@ import { getSwapQuoteQueryOptions } from '@ton/appkit/queries'; import type { GetSwapQuoteData, GetSwapQuoteErrorType, GetSwapQuoteQueryConfig } from '@ton/appkit/queries'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; import { useQuery } from '../../../libs/query'; import type { UseQueryReturnType } from '../../../libs/query'; diff --git a/packages/appkit-react/src/features/swap/index.ts b/packages/appkit-react/src/features/swap/index.ts index 6cc283512..4f33f573a 100644 --- a/packages/appkit-react/src/features/swap/index.ts +++ b/packages/appkit-react/src/features/swap/index.ts @@ -6,5 +6,9 @@ * */ +export * from './components/swap-widget'; +export * from './components/swap-widget-provider'; + export * from './hooks/use-swap-quote'; export * from './hooks/use-build-swap-transaction'; +export * from './hooks/use-swap-provider'; diff --git a/packages/appkit-react/src/features/swap/utils/get-info-from-quote.ts b/packages/appkit-react/src/features/swap/utils/get-info-from-quote.ts new file mode 100644 index 000000000..5b3835f28 --- /dev/null +++ b/packages/appkit-react/src/features/swap/utils/get-info-from-quote.ts @@ -0,0 +1,44 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { SwapQuote, SwapProvider } from '@ton/appkit'; +import { formatLargeValue } from '@ton/appkit'; + +import type { SwapInfoRowProps } from '../components/swap-info'; +import type { AppkitUIToken } from '../../../types/appkit-ui-token'; + +interface GetInfoFromQuoteArgs { + slippage: number; + toToken: AppkitUIToken | null; + quote?: SwapQuote; + provider?: SwapProvider; +} + +export const getInfoFromQuote = ({ quote, slippage, provider, toToken }: GetInfoFromQuoteArgs): SwapInfoRowProps[] => { + const rows: SwapInfoRowProps[] = []; + + if (!quote) return []; + + if (provider) { + const metadata = provider.getMetadata(); + rows.push({ label: 'Provider', value: metadata.name }); + } + + if (quote.priceImpact) rows.push({ label: 'Price impact', value: `${(quote.priceImpact / 100).toFixed(2)}%` }); + + if (toToken) { + rows.push({ + label: 'Min received', + value: `${formatLargeValue(quote.minReceived, Math.min(toToken.decimals, 6))} ${toToken.symbol}`, + }); + } + + rows.push({ label: 'Slippage', value: `${(slippage / 100).toFixed(2)}%` }); + + return rows; +}; diff --git a/packages/appkit-react/src/features/swap/utils/map-swap-widget-tokens.ts b/packages/appkit-react/src/features/swap/utils/map-swap-widget-tokens.ts new file mode 100644 index 000000000..d9dec3838 --- /dev/null +++ b/packages/appkit-react/src/features/swap/utils/map-swap-widget-tokens.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { toNonBounceableAddress } from '@ton/appkit'; + +import type { AppkitUIToken } from '../../../types/appkit-ui-token'; + +export const mapSwapWidgetTokens = (tokens: AppkitUIToken[]): AppkitUIToken[] => { + const mapped = tokens.reduce((acc, token) => { + if (token.address === 'ton') { + acc.push({ ...token, address: 'ton' }); + + return acc; + } + + const friendlyAddress = toNonBounceableAddress(token.address); + + if (!friendlyAddress) return acc; + + const mappedToken = { + ...token, + address: friendlyAddress, + }; + + acc.push(mappedToken); + + return acc; + }, [] as AppkitUIToken[]); + + return mapped; +}; diff --git a/packages/appkit-react/src/features/swap/utils/truncate-decimals.ts b/packages/appkit-react/src/features/swap/utils/truncate-decimals.ts new file mode 100644 index 000000000..9d620fb11 --- /dev/null +++ b/packages/appkit-react/src/features/swap/utils/truncate-decimals.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export function truncateDecimals(value: string, maxDecimals: number): string { + const dotIndex = value.indexOf('.'); + if (dotIndex === -1) return value; + return value.slice(0, dotIndex + 1 + maxDecimals); +} diff --git a/packages/appkit-react/src/features/transaction/components/transaction-progress/transaction-progress.module.css b/packages/appkit-react/src/features/transaction/components/transaction-progress/transaction-progress.module.css index 1cd74761b..16c4352b7 100644 --- a/packages/appkit-react/src/features/transaction/components/transaction-progress/transaction-progress.module.css +++ b/packages/appkit-react/src/features/transaction/components/transaction-progress/transaction-progress.module.css @@ -21,15 +21,15 @@ } .success { - color: #4caf50; + color: var(--ta-color-success); } .failed { - color: #f44336; + color: var(--ta-color-error); } .message { - composes: bodyLargeMedium from "../../../../styles/typography.module.css"; + composes: labelMedium from "../../../../styles/typography.module.css"; text-align: center; font-family: var(--ta-font-family-mono); } diff --git a/packages/appkit-react/src/features/transaction/components/transaction-progress/transaction-progress.stories.tsx b/packages/appkit-react/src/features/transaction/components/transaction-progress/transaction-progress.stories.tsx index f7940987b..e88162f57 100644 --- a/packages/appkit-react/src/features/transaction/components/transaction-progress/transaction-progress.stories.tsx +++ b/packages/appkit-react/src/features/transaction/components/transaction-progress/transaction-progress.stories.tsx @@ -6,7 +6,7 @@ * */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { I18nProvider } from '../../../../providers/i18n-provider'; import { TransactionProgressContext } from './transaction-progress-provider'; @@ -46,9 +46,6 @@ const meta: Meta = { title: 'Public/Features/Transaction/TransactionProgress', component: TransactionProgressPreview, tags: ['autodocs'], - parameters: { - layout: 'centered', - }, }; export default meta; diff --git a/packages/appkit-react/src/features/transaction/components/transaction-progress/transaction-progress.tsx b/packages/appkit-react/src/features/transaction/components/transaction-progress/transaction-progress.tsx index a79e8c351..8cc866cf1 100644 --- a/packages/appkit-react/src/features/transaction/components/transaction-progress/transaction-progress.tsx +++ b/packages/appkit-react/src/features/transaction/components/transaction-progress/transaction-progress.tsx @@ -10,7 +10,7 @@ import { useMemo } from 'react'; import type { FC, ReactNode, ComponentProps } from 'react'; import { clsx } from 'clsx'; -import { useI18n } from '../../../../hooks/use-i18n'; +import { useI18n } from '../../../settings/hooks/use-i18n'; import { TransactionProgressProvider, useTransactionProgressContext } from './transaction-progress-provider'; import type { TransactionProgressContextValue } from './transaction-progress-provider'; import { TransactionProgressIcon } from './transaction-progress-icons'; diff --git a/packages/appkit-react/src/features/transaction/components/transaction/send.stories.tsx b/packages/appkit-react/src/features/transaction/components/transaction/send.stories.tsx index 1204175d7..171c5ba7c 100644 --- a/packages/appkit-react/src/features/transaction/components/transaction/send.stories.tsx +++ b/packages/appkit-react/src/features/transaction/components/transaction/send.stories.tsx @@ -6,7 +6,7 @@ * */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; import { Button } from '../../../../components/button'; @@ -27,9 +27,6 @@ const meta: Meta = { title: 'Public/Features/Transaction/Send', component: SendPreview, tags: ['autodocs'], - parameters: { - layout: 'centered', - }, }; export default meta; diff --git a/packages/appkit-react/src/features/transaction/components/transaction/send.tsx b/packages/appkit-react/src/features/transaction/components/transaction/send.tsx index ff06f8205..fcdb30786 100644 --- a/packages/appkit-react/src/features/transaction/components/transaction/send.tsx +++ b/packages/appkit-react/src/features/transaction/components/transaction/send.tsx @@ -7,12 +7,13 @@ */ import { useCallback, useMemo } from 'react'; -import type { FC, ReactNode, ComponentProps } from 'react'; +import type { FC, ReactNode } from 'react'; import type { SendTransactionParameters, SendTransactionReturnType } from '@ton/appkit'; +import { useI18n } from '../../../settings/hooks/use-i18n'; import { SendProvider, useSendContext } from '../transaction-provider'; -import { useI18n } from '../../../../hooks/use-i18n'; import { Button } from '../../../../components/button'; +import type { ButtonProps } from '../../../../components/button'; export interface SendRenderProps { isLoading: boolean; @@ -27,7 +28,7 @@ export type SendRequest = | (() => SendTransactionParameters) | (() => Promise); -export interface SendProps extends Omit, 'children' | 'onError'> { +export interface SendProps extends Omit { /** The transaction request parameters */ request: SendRequest; /** Callback when an error occurs */ @@ -40,7 +41,7 @@ export interface SendProps extends Omit, 'children' | ' children?: (props: SendRenderProps) => ReactNode; } -interface SendContentProps extends Omit, 'children'> { +interface SendContentProps extends Omit { text?: ReactNode; children?: (props: SendRenderProps) => ReactNode; } diff --git a/packages/appkit-react/src/features/transaction/hooks/use-send-transaction.ts b/packages/appkit-react/src/features/transaction/hooks/use-send-transaction.ts index a11a55c4e..b8aca2b68 100644 --- a/packages/appkit-react/src/features/transaction/hooks/use-send-transaction.ts +++ b/packages/appkit-react/src/features/transaction/hooks/use-send-transaction.ts @@ -19,7 +19,7 @@ import { sendTransactionMutationOptions } from '@ton/appkit/queries'; import { useMutation } from '../../../libs/query'; import type { UseMutationReturnType } from '../../../libs/query'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; export type UseSendTransactionParameters = SendTransactionOptions; diff --git a/packages/appkit-react/src/features/transaction/hooks/use-transaction-status.ts b/packages/appkit-react/src/features/transaction/hooks/use-transaction-status.ts index de4a41d10..005216ff0 100644 --- a/packages/appkit-react/src/features/transaction/hooks/use-transaction-status.ts +++ b/packages/appkit-react/src/features/transaction/hooks/use-transaction-status.ts @@ -16,7 +16,7 @@ import { getTransactionStatusQueryOptions } from '@ton/appkit/queries'; import { useQuery } from '../../../libs/query'; import type { UseQueryReturnType } from '../../../libs/query'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; export type UseTransactionStatusParameters = GetTransactionStatusParameters & GetTransactionStatusQueryConfig; diff --git a/packages/appkit-react/src/features/transaction/hooks/use-transfer-ton.ts b/packages/appkit-react/src/features/transaction/hooks/use-transfer-ton.ts index 34ce1b7ca..d1d65f1ec 100644 --- a/packages/appkit-react/src/features/transaction/hooks/use-transfer-ton.ts +++ b/packages/appkit-react/src/features/transaction/hooks/use-transfer-ton.ts @@ -19,7 +19,7 @@ import { transferTonMutationOptions } from '@ton/appkit/queries'; import { useMutation } from '../../../libs/query'; import type { UseMutationReturnType } from '../../../libs/query'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; export type UseTransferTonParameters = TransferTonOptions; diff --git a/packages/appkit-react/src/features/transaction/hooks/use-watch-transactions-by-address.ts b/packages/appkit-react/src/features/transaction/hooks/use-watch-transactions-by-address.ts index 84cf0c493..06886c82d 100644 --- a/packages/appkit-react/src/features/transaction/hooks/use-watch-transactions-by-address.ts +++ b/packages/appkit-react/src/features/transaction/hooks/use-watch-transactions-by-address.ts @@ -10,7 +10,7 @@ import { useEffect } from 'react'; import { watchTransactionsByAddress, hasStreamingProvider, resolveNetwork } from '@ton/appkit'; import type { WatchTransactionsByAddressOptions, TransactionsUpdate } from '@ton/appkit'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; export type UseWatchTransactionsByAddressParameters = Partial; diff --git a/packages/appkit-react/src/features/wallets/hooks/use-connect.ts b/packages/appkit-react/src/features/wallets/hooks/use-connect.ts index bbbd8175a..014bba94e 100644 --- a/packages/appkit-react/src/features/wallets/hooks/use-connect.ts +++ b/packages/appkit-react/src/features/wallets/hooks/use-connect.ts @@ -12,7 +12,7 @@ import type { ConnectData, ConnectErrorType, ConnectOptions, ConnectVariables } import { useMutation } from '../../../libs/query'; import type { UseMutationReturnType } from '../../../libs/query'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; export type UseConnectParameters = ConnectOptions; diff --git a/packages/appkit-react/src/features/wallets/hooks/use-connected-wallets.ts b/packages/appkit-react/src/features/wallets/hooks/use-connected-wallets.ts index d0edf039e..b473d3093 100644 --- a/packages/appkit-react/src/features/wallets/hooks/use-connected-wallets.ts +++ b/packages/appkit-react/src/features/wallets/hooks/use-connected-wallets.ts @@ -10,7 +10,7 @@ import { useSyncExternalStore, useCallback } from 'react'; import { getConnectedWallets, watchConnectedWallets } from '@ton/appkit'; import type { GetConnectedWalletsReturnType } from '@ton/appkit'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; export type UseConnectedWalletsReturnType = GetConnectedWalletsReturnType; diff --git a/packages/appkit-react/src/features/wallets/hooks/use-connector-by-id.ts b/packages/appkit-react/src/features/wallets/hooks/use-connector-by-id.ts index 143208573..3ebed506a 100644 --- a/packages/appkit-react/src/features/wallets/hooks/use-connector-by-id.ts +++ b/packages/appkit-react/src/features/wallets/hooks/use-connector-by-id.ts @@ -10,7 +10,7 @@ import { useSyncExternalStore, useCallback } from 'react'; import { getConnectorById, watchConnectorById } from '@ton/appkit'; import type { Connector } from '@ton/appkit'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; export const useConnectorById = (id: string): Connector | undefined => { const appKit = useAppKit(); diff --git a/packages/appkit-react/src/features/wallets/hooks/use-connectors.ts b/packages/appkit-react/src/features/wallets/hooks/use-connectors.ts index f6553b56c..803600732 100644 --- a/packages/appkit-react/src/features/wallets/hooks/use-connectors.ts +++ b/packages/appkit-react/src/features/wallets/hooks/use-connectors.ts @@ -10,7 +10,7 @@ import { useSyncExternalStore, useCallback } from 'react'; import { getConnectors, watchConnectors } from '@ton/appkit'; import type { GetConnectorsReturnType } from '@ton/appkit'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; export type UseConnectorsReturnType = GetConnectorsReturnType; diff --git a/packages/appkit-react/src/features/wallets/hooks/use-disconnect.ts b/packages/appkit-react/src/features/wallets/hooks/use-disconnect.ts index 75702d76e..9bf022804 100644 --- a/packages/appkit-react/src/features/wallets/hooks/use-disconnect.ts +++ b/packages/appkit-react/src/features/wallets/hooks/use-disconnect.ts @@ -14,7 +14,7 @@ import type { DisconnectData, DisconnectErrorType, DisconnectOptions, Disconnect import { useMutation } from '../../../libs/query'; import type { UseMutationReturnType } from '../../../libs/query'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; export type UseDisconnectParameters = DisconnectOptions; diff --git a/packages/appkit-react/src/features/wallets/hooks/use-selected-wallet.ts b/packages/appkit-react/src/features/wallets/hooks/use-selected-wallet.ts index 5963eb4bd..8e8c129c2 100644 --- a/packages/appkit-react/src/features/wallets/hooks/use-selected-wallet.ts +++ b/packages/appkit-react/src/features/wallets/hooks/use-selected-wallet.ts @@ -10,7 +10,7 @@ import { useSyncExternalStore, useCallback } from 'react'; import { getSelectedWallet, watchSelectedWallet, setSelectedWalletId } from '@ton/appkit'; import type { GetSelectedWalletReturnType } from '@ton/appkit'; -import { useAppKit } from '../../../hooks/use-app-kit'; +import { useAppKit } from '../../settings'; export type UseSelectedWalletReturnType = readonly [GetSelectedWalletReturnType, (walletId: string | null) => void]; diff --git a/packages/appkit-react/src/hooks/use-debounce-callback.ts b/packages/appkit-react/src/hooks/use-debounce-callback.ts new file mode 100644 index 000000000..12e23bea9 --- /dev/null +++ b/packages/appkit-react/src/hooks/use-debounce-callback.ts @@ -0,0 +1,69 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useEffect, useMemo, useRef } from 'react'; +import { debounce } from '@ton/appkit'; +import type { DebounceOptions } from '@ton/appkit'; + +import { useUnmount } from './use-unmount'; + +interface ControlFunctions { + cancel: () => void; + flush: () => void; + isPending: () => boolean; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type DebouncedState ReturnType> = (( + ...args: Parameters +) => ReturnType | void) & + ControlFunctions; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function useDebounceCallback ReturnType>( + func: T, + delay = 500, + options?: DebounceOptions, +): DebouncedState { + const debouncedFunc = useRef | null>(null); + + useUnmount(() => { + if (debouncedFunc.current) { + debouncedFunc.current.cancel(); + } + }); + + const debounced = useMemo(() => { + const debouncedFuncInstance = debounce(func, delay, options); + + const wrappedFunc: DebouncedState = (...args: Parameters) => { + return debouncedFuncInstance(...args); + }; + + wrappedFunc.cancel = () => { + debouncedFuncInstance.cancel(); + }; + + wrappedFunc.isPending = () => { + return !!debouncedFunc.current; + }; + + wrappedFunc.flush = () => { + return debouncedFuncInstance.flush(); + }; + + return wrappedFunc; + }, [func, delay, options]); + + // Update the debounced function ref whenever func, wait, or options change + useEffect(() => { + debouncedFunc.current = debounce(func, delay, options); + }, [func, delay, options]); + + return debounced; +} diff --git a/packages/appkit-react/src/hooks/use-debounce-value.ts b/packages/appkit-react/src/hooks/use-debounce-value.ts new file mode 100644 index 000000000..84874b862 --- /dev/null +++ b/packages/appkit-react/src/hooks/use-debounce-value.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useRef, useState } from 'react'; +import type { DebounceOptions } from '@ton/appkit'; + +import type { DebouncedState } from './use-debounce-callback'; +import { useDebounceCallback } from './use-debounce-callback'; + +interface UseDebounceValueOptions extends DebounceOptions { + equalityFn?: (left: T, right: T) => boolean; +} + +export const useDebounceValue = ( + initialValue: T | (() => T), + delay: number, + options?: UseDebounceValueOptions, +): [T, DebouncedState<(value: T) => void>] => { + const eq = options?.equalityFn ?? ((left: T, right: T) => left === right); + const unwrappedInitialValue = initialValue instanceof Function ? initialValue() : initialValue; + const [debouncedValue, setDebouncedValue] = useState(unwrappedInitialValue); + const previousValueRef = useRef(unwrappedInitialValue); + + const updateDebouncedValue = useDebounceCallback(setDebouncedValue, delay, options); + + // Update the debounced value if the initial value changes + if (!eq(previousValueRef.current as T, unwrappedInitialValue)) { + updateDebouncedValue(unwrappedInitialValue); + previousValueRef.current = unwrappedInitialValue; + } + + return [debouncedValue, updateDebouncedValue]; +}; diff --git a/packages/appkit-react/src/hooks/use-unmount.ts b/packages/appkit-react/src/hooks/use-unmount.ts new file mode 100644 index 000000000..8d5312843 --- /dev/null +++ b/packages/appkit-react/src/hooks/use-unmount.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useEffect, useRef } from 'react'; + +export const useUnmount = (func: () => void) => { + const funcRef = useRef(func); + funcRef.current = func; + + useEffect(() => () => funcRef.current(), []); +}; diff --git a/packages/appkit-react/src/index.ts b/packages/appkit-react/src/index.ts index e44576dce..7c1acf378 100644 --- a/packages/appkit-react/src/index.ts +++ b/packages/appkit-react/src/index.ts @@ -7,17 +7,19 @@ */ export { AppKitProvider } from './providers/app-kit-provider'; -export { useAppKit } from './hooks/use-app-kit'; -export { useAppKitTheme } from './hooks/use-app-kit-theme'; export { I18nProvider } from './providers/i18n-provider'; -export { useI18n } from './hooks/use-i18n'; export * from '@ton/appkit'; export * from './components/block'; +export * from './components/info-block'; export * from './components/button'; -export * from './components/circle-icon'; +export * from './components/logo'; +export * from './components/modal'; +export * from './components/skeleton'; export * from './components/ton-icon'; +export * from './components/input'; +export * from './components/token-select-modal'; export * from './features/balances'; export * from './features/jettons'; @@ -25,6 +27,9 @@ export * from './features/network'; export * from './features/nft'; export * from './features/transaction'; export * from './features/wallets'; +export * from './features/settings'; export * from './features/swap'; export * from './features/signing'; export * from './features/staking'; + +export * from './types/appkit-ui-token'; diff --git a/packages/appkit-react/src/locales/en.ts b/packages/appkit-react/src/locales/en.ts index 73ee47f39..9e09e202a 100644 --- a/packages/appkit-react/src/locales/en.ts +++ b/packages/appkit-react/src/locales/en.ts @@ -37,4 +37,24 @@ export default { nft: { onSale: 'On Sale', }, + + // Swap + swap: { + title: 'Swap', + pay: 'Pay', + receive: 'Receive', + max: 'MAX', + continue: 'Continue', + enterAmount: 'Enter an amount', + insufficientBalance: 'Insufficient balance', + tooManyDecimals: 'Too many decimal places', + quoteError: 'Unable to get a quote', + selectToken: 'Select Token', + searchToken: 'Search...', + settings: 'Settings', + slippage: 'Slippage', + slippageError: 'The maximum slippage tolerance cannot be more than 50%. The recommended range is 1%', + slippageWarning: 'High slippage tolerance increases the risk of an unfavorable trade', + provider: 'Provider', + }, } as const; diff --git a/packages/appkit-react/src/storybook/fixtures/tokens.ts b/packages/appkit-react/src/storybook/fixtures/tokens.ts new file mode 100644 index 000000000..7292b6f9e --- /dev/null +++ b/packages/appkit-react/src/storybook/fixtures/tokens.ts @@ -0,0 +1,77 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { AppkitUIToken } from '../../types/appkit-ui-token'; + +export const STORY_TOKENS: AppkitUIToken[] = [ + { + symbol: 'TON', + name: 'Toncoin', + decimals: 9, + address: 'ton', + logo: 'https://asset.ston.fi/img/EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c/c8d21a3d93f9b574381e0a8d8f16d48b325dd8f54ce172f599c1e9d6c62f03f7', + }, + { + symbol: 'USD₮', + name: 'Tether USD', + decimals: 6, + address: 'UQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_p0p', + rate: '1', + logo: 'https://asset.ston.fi/img/EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs/1a87edfee9a28b05578853952e5effb8cc30af1e0fb90043aa2ce19dce490849', + }, + { + symbol: 'STON', + name: 'STON', + decimals: 9, + address: 'UQA2kCVNwVsil2EM2mB0SkXytxCqQjS4mttjDpnXmwG9T_sL', + logo: 'https://asset.ston.fi/img/EQA2kCVNwVsil2EM2mB0SkXytxCqQjS4mttjDpnXmwG9T6bO/7c9798ce1e64707fb4cb8f025d4060f66b386ed381b50498e3b88731cedeffe8', + }, + { + symbol: 'XAUt0', + name: 'Tether Gold', + decimals: 6, + address: 'UQA1R_LuQCLHlMgOo1S4G7Y7W1cd0FrAkbA10Zq7rddKxnKh', + logo: 'https://asset.ston.fi/img/EQA1R_LuQCLHlMgOo1S4G7Y7W1cd0FrAkbA10Zq7rddKxi9k/4aaaa7c30d7811bced81ded6bc116dcc82a78c6aea53d6012fd586a5826963ad', + }, + { + symbol: 'USDe', + name: 'Ethena USDe', + decimals: 6, + address: 'UQAIb6KmdfdDR7CN1GBqVJuP25iCnLKCvBlJ07Evuu2dzKOa', + rate: '1', + logo: 'https://asset.ston.fi/img/EQAIb6KmdfdDR7CN1GBqVJuP25iCnLKCvBlJ07Evuu2dzP5f/dbcc67993cd4aad4845a97a4a9722c6cb618123997c8112c29d4932b2739c4cd', + }, + { + symbol: 'tsTON', + name: 'Tonstakers TON', + decimals: 9, + address: 'UQC98_qAmNEptUtPc7W6xdHh_ZHrBUFpw5Ft_IzNU20QAMtq', + logo: 'https://asset.ston.fi/img/EQC98_qAmNEptUtPc7W6xdHh_ZHrBUFpw5Ft_IzNU20QAJav/38f530facb209e4696b8aef17af51df94d16bd879926c517b07d25841da287b7', + }, + { + symbol: 'GEMSTON', + name: 'GEMSTON', + decimals: 9, + address: 'UQBX6K9aXVl3nXINCyPPL86C4ONVmQ8vK360u6dykFKXpC1f', + logo: 'https://asset.ston.fi/img/EQBX6K9aXVl3nXINCyPPL86C4ONVmQ8vK360u6dykFKXpHCa/c6ab1e58e3b9b58a7429d38b7feab731afae2f66dc301a6c42041fdf7e9d7c9c', + }, + { + symbol: 'UTYA', + name: 'Utya', + decimals: 9, + address: 'UQBaCgUwOoc6gHCNln_oJzb0mVs79YG7wYoavh-o1Itanb8F', + logo: 'https://asset.ston.fi/img/EQBaCgUwOoc6gHCNln_oJzb0mVs79YG7wYoavh-o1ItaneLA/727e6cc971afdfa8ed9c698d0909eee9de344a0b6766ff5e4ddcc3323449d6f6', + }, + { + symbol: 'WETH', + name: 'Wrapped Ether', + decimals: 18, + address: 'UQBTkLAhEteZCRgRe_xMs5ZE0bMrduYxKbyzGCpXXW8dRT5W', + logo: 'https://asset.ston.fi/img/EQBTkLAhEteZCRgRe_xMs5ZE0bMrduYxKbyzGCpXXW8dRWOT/6267787665c30c2500dbde048e2f8a6a6d7ec58633ea038723f4ce1fab337ccb', + }, +]; diff --git a/packages/appkit-react/src/styles/index.css b/packages/appkit-react/src/styles/index.css index 8120d2793..e4b841aa6 100644 --- a/packages/appkit-react/src/styles/index.css +++ b/packages/appkit-react/src/styles/index.css @@ -1,64 +1,88 @@ -:root { +:where(:root) { /* Font */ --ta-font-family: -apple-system, BlinkMacSystemFont, 'Roboto', 'Helvetica Neue', Arial, Tahoma, Verdana, sans-serif; --ta-font-family-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* Typography */ - /* Headings */ - --ta-heading1-font-size: 32px; - --ta-heading1-font-weight: 700; - --ta-heading1-line-height: normal; + /* display */ + --ta-display-large-size: 64px; + --ta-display-large-weight: 800; + --ta-display-large-line-height: 70px; - --ta-heading2-font-size: 28px; - --ta-heading2-font-weight: 700; - --ta-heading2-line-height: normal; + --ta-display-size: 40px; + --ta-display-weight: 800; + --ta-display-line-height: 44px; - --ta-heading3-font-size: 24px; - --ta-heading3-font-weight: 700; - --ta-heading3-line-height: normal; + /* headline */ + --ta-headline-size: 28px; + --ta-headline-weight: 700; + --ta-headline-line-height: 32px; - --ta-heading4-font-size: 20px; - --ta-heading4-font-weight: 700; - --ta-heading4-line-height: normal; + /* title */ + --ta-title-size: 20px; + --ta-title-weight: 700; + --ta-title-line-height: 24px; - --ta-heading5-font-size: 18px; - --ta-heading5-font-weight: 700; - --ta-heading5-line-height: normal; + /* body */ + --ta-body-size: 16px; + --ta-body-medium-weight: 500; + --ta-body-regular-weight: 400; + --ta-body-semibold-weight: 600; + --ta-body-medium-line-height: 20px; + --ta-body-regular-line-height: 24px; - /* Body */ - --ta-body-large-font-size: 16px; - --ta-body-large-line-height: 24px; - --ta-body-large-font-weight: 400; - --ta-body-large-medium-font-weight: 500; - - --ta-body-font-size: 14px; - --ta-body-line-height: 20px; - --ta-body-font-weight: 400; - --ta-body-medium-font-weight: 500; - --ta-body-bold-font-weight: 700; + /* label */ + --ta-label-size: 14px; + --ta-label-regular-weight: 400; + --ta-label-semibold-weight: 600; + --ta-label-medium-weight: 500; + --ta-label-line-height: 18px; + + /* footnote */ + --ta-footnote-size: 12px; + --ta-footnote-regular-weight: 400; + --ta-footnote-caps-weight: 600; + --ta-footnote-line-height: 16px; + + /* caption */ + --ta-caption-size: 11px; + --ta-caption-weight: 600; + --ta-caption-line-height: 12px; - --ta-body-small-font-size: 12px; - --ta-body-small-line-height: 16px; - --ta-body-small-font-weight: 400; + /* input */ + --ta-input-s-size: 16px; + --ta-input-s-weight: 400; + --ta-input-s-line-height: 24px; - /* Caption */ - --ta-caption1-font-size: 12px; - --ta-caption1-line-height: 16px; - --ta-caption1-font-weight: 400; + --ta-input-m-size: 24px; + --ta-input-m-weight: 600; + --ta-input-m-line-height: 30px; - --ta-caption2-font-size: 10px; - --ta-caption2-line-height: 12px; - --ta-caption2-font-weight: 400; + --ta-input-l-size: 32px; + --ta-input-l-weight: 700; + --ta-input-l-line-height: 40px; /* Colors */ - --ta-color-primary: #0098EB; - --ta-color-primary-foreground: #ffffff; - --ta-color-text: #000000; - --ta-color-text-secondary: #8e8e93; - --ta-color-background: #ffffff; - --ta-color-background-secondary: #f2f2f7; - --ta-color-block: #ffffff; - --ta-color-block-foreground: #000000; + --ta-color-primary: #007AFF; + --ta-color-primary-foreground: #FFFFFF; + --ta-color-error: #FF3B30; + --ta-color-success: #34C759; + --ta-color-text: #141416; + --ta-color-text-secondary: #787881; + --ta-color-text-tertiary: #93939D; + --ta-color-background: #FFFFFF; + --ta-color-background-secondary: #F7F8FA; + --ta-color-background-tertiary: #E5E8ED; + --ta-color-background-bezeled: rgba(0, 122, 255, 0.10); + --ta-color-block: var(--ta-color-background); + --ta-color-block-foreground: var(--ta-color-text); + --ta-color-skeleton-shimmer: linear-gradient( + 90deg, + rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 0.4) 20%, + rgba(255, 255, 255, 0.6) 60%, + rgba(255, 255, 255, 0) 100% + ); /* Static Colors */ --ta-color-white: #ffffff; @@ -70,6 +94,7 @@ --ta-border-radius-m: 8px; --ta-border-radius-l: 12px; --ta-border-radius-xl: 16px; + --ta-border-radius-2xl: 20px; --ta-border-radius-full: 9999px; /* Border Width */ @@ -77,13 +102,25 @@ --ta-border-width-m: 2px; } -[data-ta-theme='dark'] { - --ta-color-primary: #0098EB; - --ta-color-primary-foreground: #ffffff; - --ta-color-text: #ffffff; - --ta-color-text-secondary: #8e8e93; - --ta-color-background: #000000; - --ta-color-background-secondary: #1c1c1e; - --ta-color-block: #121214; - --ta-color-block-foreground: #ffffff; +:where([data-ta-theme='dark']) { + --ta-color-primary: #007AFF; + --ta-color-primary-foreground: #FFFFFF; + --ta-color-error: #FF3B30; + --ta-color-success: #30D158; + --ta-color-text: #FFFFFF; + --ta-color-text-secondary: #93939D; + --ta-color-text-tertiary: #787881; + --ta-color-background: #141416; + --ta-color-background-secondary: #1E1E1E; + --ta-color-background-tertiary: #2C2C2C; + --ta-color-background-bezeled: rgba(0, 122, 255, 0.10); + --ta-color-block: var(--ta-color-background); + --ta-color-block-foreground: var(--ta-color-text); + --ta-color-skeleton-shimmer: linear-gradient( + 90deg, + rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 0.05) 20%, + rgba(255, 255, 255, 0.1) 60%, + rgba(255, 255, 255, 0) 100% + ); } diff --git a/packages/appkit-react/src/styles/typography.module.css b/packages/appkit-react/src/styles/typography.module.css index c3251840b..8c0e0d460 100644 --- a/packages/appkit-react/src/styles/typography.module.css +++ b/packages/appkit-react/src/styles/typography.module.css @@ -1,92 +1,128 @@ -.heading1 { +/* display */ +.displayLarge { font-family: var(--ta-font-family); - font-size: var(--ta-heading1-font-size); - font-weight: var(--ta-heading1-font-weight); - line-height: var(--ta-heading1-line-height); + font-size: var(--ta-display-large-size); + font-weight: var(--ta-display-large-weight); + line-height: var(--ta-display-large-line-height); } -.heading2 { +.display { font-family: var(--ta-font-family); - font-size: var(--ta-heading2-font-size); - font-weight: var(--ta-heading2-font-weight); - line-height: var(--ta-heading2-line-height); + font-size: var(--ta-display-size); + font-weight: var(--ta-display-weight); + line-height: var(--ta-display-line-height); } -.heading3 { +/* headline */ +.headline { font-family: var(--ta-font-family); - font-size: var(--ta-heading3-font-size); - font-weight: var(--ta-heading3-font-weight); - line-height: var(--ta-heading3-line-height); + font-size: var(--ta-headline-size); + font-weight: var(--ta-headline-weight); + line-height: var(--ta-headline-line-height); } -.heading4 { +/* title */ +.title { font-family: var(--ta-font-family); - font-size: var(--ta-heading4-font-size); - font-weight: var(--ta-heading4-font-weight); - line-height: var(--ta-heading4-line-height); + font-size: var(--ta-title-size); + font-weight: var(--ta-title-weight); + line-height: var(--ta-title-line-height); } -.heading5 { +/* body */ +.bodyMedium { font-family: var(--ta-font-family); - font-size: var(--ta-heading5-font-size); - font-weight: var(--ta-heading5-font-weight); - line-height: var(--ta-heading5-line-height); + font-size: var(--ta-body-size); + font-weight: var(--ta-body-medium-weight); + line-height: var(--ta-body-medium-line-height); } -/* Body */ -.bodyLarge { +.bodyRegular { font-family: var(--ta-font-family); - font-size: var(--ta-body-large-font-size); - line-height: var(--ta-body-large-line-height); - font-weight: var(--ta-body-large-font-weight); + font-size: var(--ta-body-size); + font-weight: var(--ta-body-regular-weight); + line-height: var(--ta-body-regular-line-height); } -.bodyLargeMedium { +.bodySemibold { font-family: var(--ta-font-family); - font-size: var(--ta-body-large-font-size); - line-height: var(--ta-body-large-line-height); - font-weight: var(--ta-body-large-medium-font-weight); + font-size: var(--ta-body-size); + font-weight: var(--ta-body-semibold-weight); + line-height: var(--ta-body-regular-line-height); } -.body { +/* label */ +.labelRegular { font-family: var(--ta-font-family); - font-size: var(--ta-body-font-size); - line-height: var(--ta-body-line-height); - font-weight: var(--ta-body-font-weight); + font-size: var(--ta-label-size); + font-weight: var(--ta-label-regular-weight); + line-height: var(--ta-label-line-height); } -.bodyMedium { +.labelSemibold { + font-family: var(--ta-font-family); + font-size: var(--ta-label-size); + font-weight: var(--ta-label-semibold-weight); + line-height: var(--ta-label-line-height); +} + +.labelMedium { + font-family: var(--ta-font-family); + font-size: var(--ta-label-size); + font-weight: var(--ta-label-medium-weight); + line-height: var(--ta-label-line-height); +} + +/* footnote */ +.footnoteRegular { + font-family: var(--ta-font-family); + font-size: var(--ta-footnote-size); + font-weight: var(--ta-footnote-regular-weight); + line-height: var(--ta-footnote-line-height); +} + +.footnoteCaps { + font-family: var(--ta-font-family); + font-size: var(--ta-footnote-size); + font-weight: var(--ta-footnote-caps-weight); + line-height: var(--ta-footnote-line-height); + text-transform: uppercase; +} + +/* caption */ +.captionSemibold { font-family: var(--ta-font-family); - font-size: var(--ta-body-font-size); - line-height: var(--ta-body-line-height); - font-weight: var(--ta-body-medium-font-weight); + font-size: var(--ta-caption-size); + font-weight: var(--ta-caption-weight); + line-height: var(--ta-caption-line-height); } -.bodyBold { +.captionCaps { font-family: var(--ta-font-family); - font-size: var(--ta-body-font-size); - line-height: var(--ta-body-line-height); - font-weight: var(--ta-body-bold-font-weight); + font-size: var(--ta-caption-size); + font-weight: var(--ta-caption-weight); + line-height: var(--ta-caption-line-height); + text-transform: uppercase; } -.bodySmall { +/* input */ +.inputS { font-family: var(--ta-font-family); - font-size: var(--ta-body-small-font-size); - line-height: var(--ta-body-small-line-height); - font-weight: var(--ta-body-small-font-weight); + font-size: var(--ta-input-s-size); + font-weight: var(--ta-input-s-weight); + line-height: var(--ta-input-s-line-height); } -/* Caption */ -.caption1 { +.inputM { font-family: var(--ta-font-family); - font-size: var(--ta-caption1-font-size); - line-height: var(--ta-caption1-line-height); - font-weight: var(--ta-caption1-font-weight); + font-size: var(--ta-input-m-size); + font-weight: var(--ta-input-m-weight); + line-height: var(--ta-input-m-line-height); } -.caption2 { +.inputL { font-family: var(--ta-font-family); - font-size: var(--ta-caption2-font-size); - line-height: var(--ta-caption2-line-height); - font-weight: var(--ta-caption2-font-weight); + font-size: var(--ta-input-l-size); + font-weight: var(--ta-input-l-weight); + line-height: var(--ta-input-l-line-height); } diff --git a/packages/appkit-react/src/types/appkit-ui-token.ts b/packages/appkit-react/src/types/appkit-ui-token.ts new file mode 100644 index 000000000..e017916cb --- /dev/null +++ b/packages/appkit-react/src/types/appkit-ui-token.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export interface AppkitUIToken { + /** Token symbol, e.g. "TON" */ + symbol: string; + /** Full token name, e.g. "Toncoin" */ + name: string; + /** Number of decimals for the token */ + decimals: number; + /** Jetton contract address (use "ton" for TON) */ + address: string; + /** Optional token logo */ + logo?: string; + /** Optional exchange rate: 1 token = rate fiat units (used for fiat value display) */ + rate?: string; +} diff --git a/packages/appkit-react/tsconfig.build.json b/packages/appkit-react/tsconfig.build.json index 2c90570b9..6ea2d2462 100644 --- a/packages/appkit-react/tsconfig.build.json +++ b/packages/appkit-react/tsconfig.build.json @@ -12,6 +12,7 @@ "src/**/*.test.ts", "src/**/*.test.tsx", "src/**/*.stories.tsx", - "src/**/*.test-d.ts" + "src/**/*.test-d.ts", + "src/storybook/**" ] } diff --git a/packages/appkit/docs/actions.md b/packages/appkit/docs/actions.md index 8ed695e8c..92bf8aa6e 100644 --- a/packages/appkit/docs/actions.md +++ b/packages/appkit/docs/actions.md @@ -502,6 +502,25 @@ Get the `SwapManager` instance to interact with swap providers directly. const swapManager = getSwapManager(appKit); ``` +### `getSwapProvider` + +Get a specific swap provider by its ID. + +```ts +const swapProvider = getSwapProvider(appKit, { id: 'stonfi' }); +``` + +### `watchSwapProviders` + +Watch for new swap providers registration. + +```ts +const unsubscribe = watchSwapProviders(appKit, { + onChange: () => console.log('Swap providers updated'), +}); +unsubscribe(); +``` + ### `getSwapQuote` Get a swap quote from registered providers. diff --git a/packages/appkit/src/actions/index.ts b/packages/appkit/src/actions/index.ts index db575b2b2..e97a697a2 100644 --- a/packages/appkit/src/actions/index.ts +++ b/packages/appkit/src/actions/index.ts @@ -115,7 +115,13 @@ export { signCell, type SignCellParameters, type SignCellReturnType } from './si // Swap export { getSwapManager, type GetSwapManagerReturnType } from './swap/get-swap-manager'; +export { getSwapProvider, type GetSwapProviderOptions, type GetSwapProviderReturnType } from './swap/get-swap-provider'; export { getSwapQuote, type GetSwapQuoteOptions, type GetSwapQuoteReturnType } from './swap/get-swap-quote'; +export { + watchSwapProviders, + type WatchSwapProvidersParameters, + type WatchSwapProvidersReturnType, +} from './swap/watch-swap-providers'; export { buildSwapTransaction, type BuildSwapTransactionOptions, diff --git a/packages/appkit/src/actions/swap/get-swap-provider.ts b/packages/appkit/src/actions/swap/get-swap-provider.ts new file mode 100644 index 000000000..d302927c0 --- /dev/null +++ b/packages/appkit/src/actions/swap/get-swap-provider.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { SwapProviderInterface } from '@ton/walletkit'; + +import type { AppKit } from '../../core/app-kit'; + +export interface GetSwapProviderOptions { + id?: string; +} + +export type GetSwapProviderReturnType = SwapProviderInterface; + +export const getSwapProvider = (appKit: AppKit, options: GetSwapProviderOptions = {}): GetSwapProviderReturnType => { + return appKit.swapManager.getProvider(options.id); +}; diff --git a/packages/appkit/src/actions/swap/watch-swap-providers.ts b/packages/appkit/src/actions/swap/watch-swap-providers.ts new file mode 100644 index 000000000..fe6a3f4ee --- /dev/null +++ b/packages/appkit/src/actions/swap/watch-swap-providers.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { AppKit } from '../../core/app-kit'; +import { PROVIDER_EVENTS } from '../../core/app-kit'; + +export interface WatchSwapProvidersParameters { + onChange: () => void; +} + +export type WatchSwapProvidersReturnType = () => void; + +/** + * Watch for new swap providers registration + */ +export const watchSwapProviders = ( + appKit: AppKit, + parameters: WatchSwapProvidersParameters, +): WatchSwapProvidersReturnType => { + const { onChange } = parameters; + + const unsubscribe = appKit.emitter.on(PROVIDER_EVENTS.REGISTERED, (event) => { + if (event.payload.providerType === 'swap') { + onChange(); + } + }); + + return unsubscribe; +}; diff --git a/packages/appkit/src/core/app-kit/constants/events.ts b/packages/appkit/src/core/app-kit/constants/events.ts index e8e6a52c5..867b2b257 100644 --- a/packages/appkit/src/core/app-kit/constants/events.ts +++ b/packages/appkit/src/core/app-kit/constants/events.ts @@ -31,8 +31,8 @@ export const NETWORKS_EVENTS = { } as const; /** - * Plugin events + * Providers events */ -export const PLUGIN_EVENTS = { - REGISTERED: 'plugin:registered', +export const PROVIDER_EVENTS = { + REGISTERED: 'providers:registered', } as const; diff --git a/packages/appkit/src/core/app-kit/index.ts b/packages/appkit/src/core/app-kit/index.ts index 8af994de2..9bad301da 100644 --- a/packages/appkit/src/core/app-kit/index.ts +++ b/packages/appkit/src/core/app-kit/index.ts @@ -7,7 +7,7 @@ */ export { AppKit } from './services/app-kit'; -export { CONNECTOR_EVENTS, WALLETS_EVENTS, PLUGIN_EVENTS, NETWORKS_EVENTS } from './constants/events'; +export { CONNECTOR_EVENTS, WALLETS_EVENTS, PROVIDER_EVENTS, NETWORKS_EVENTS } from './constants/events'; export type { AppKitConfig } from './types/config'; export type { @@ -15,6 +15,6 @@ export type { AppKitEvents, WalletConnectedPayload, WalletDisconnectedPayload, - PluginRegisteredPayload, + ProviderRegisteredPayload, DefaultNetworkChangedPayload, } from './types/events'; diff --git a/packages/appkit/src/core/app-kit/services/app-kit.ts b/packages/appkit/src/core/app-kit/services/app-kit.ts index eca376c72..b43586fd9 100644 --- a/packages/appkit/src/core/app-kit/services/app-kit.ts +++ b/packages/appkit/src/core/app-kit/services/app-kit.ts @@ -9,16 +9,16 @@ import { SwapManager, StreamingManager } from '@ton/walletkit'; import type { ProviderInput, SwapProviderInterface, StakingProviderInterface } from '@ton/walletkit'; +import type { AppKitConfig } from '../types/config'; +import { CONNECTOR_EVENTS, PROVIDER_EVENTS, WALLETS_EVENTS } from '../constants/events'; import { StakingManager } from '../../../staking'; import type { Connector, ConnectorFactoryContext, ConnectorInput } from '../../../types/connector'; import { EventEmitter } from '../../emitter'; -import { CONNECTOR_EVENTS, WALLETS_EVENTS } from '../constants/events'; import type { AppKitEmitter, AppKitEvents } from '../types/events'; import type { WalletInterface } from '../../../types/wallet'; import { WalletsManager } from '../../wallets-manager'; import { AppKitNetworkManager } from '../../network'; import { Network } from '../../../types/network'; -import type { AppKitConfig } from '../types/config'; /** * Central hub for wallet management. @@ -111,9 +111,19 @@ export class AppKit { switch (provider.type) { case 'swap': this.swapManager.registerProvider(provider as SwapProviderInterface); + this.emitter.emit( + PROVIDER_EVENTS.REGISTERED, + { providerId: provider.providerId, providerType: provider.type }, + 'appkit', + ); break; case 'staking': this.stakingManager.registerProvider(provider as StakingProviderInterface); + this.emitter.emit( + PROVIDER_EVENTS.REGISTERED, + { providerId: provider.providerId, providerType: provider.type }, + 'appkit', + ); break; default: throw new Error('Unknown provider type'); diff --git a/packages/appkit/src/core/app-kit/types/events.ts b/packages/appkit/src/core/app-kit/types/events.ts index 4f83abca9..091aa22d7 100644 --- a/packages/appkit/src/core/app-kit/types/events.ts +++ b/packages/appkit/src/core/app-kit/types/events.ts @@ -6,9 +6,9 @@ * */ -import type { SharedKitEvents } from '../../emitter'; -import type { CONNECTOR_EVENTS, WALLETS_EVENTS, PLUGIN_EVENTS, NETWORKS_EVENTS } from '../constants/events'; import type { Network } from '../../../types/network'; +import type { CONNECTOR_EVENTS, WALLETS_EVENTS, PROVIDER_EVENTS, NETWORKS_EVENTS } from '../constants/events'; +import type { SharedKitEvents } from '../../emitter'; import type { EventEmitter } from '../../emitter'; import type { WalletInterface } from '../../../types/wallet'; @@ -21,9 +21,9 @@ export interface WalletDisconnectedPayload { connectorId: string; } -export interface PluginRegisteredPayload { - pluginId: string; - pluginType: string; +export interface ProviderRegisteredPayload { + providerId: string; + providerType: string; } export interface DefaultNetworkChangedPayload { @@ -43,8 +43,8 @@ export type AppKitEvents = { [NETWORKS_EVENTS.UPDATED]: Record; [NETWORKS_EVENTS.DEFAULT_CHANGED]: DefaultNetworkChangedPayload; - // Plugin events - [PLUGIN_EVENTS.REGISTERED]: PluginRegisteredPayload; + // Provider events + [PROVIDER_EVENTS.REGISTERED]: ProviderRegisteredPayload; } & SharedKitEvents; export type AppKitEmitter = EventEmitter; diff --git a/packages/appkit/src/utils/address/format.ts b/packages/appkit/src/utils/address/format.ts new file mode 100644 index 000000000..af80eca0c --- /dev/null +++ b/packages/appkit/src/utils/address/format.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { Address } from '@ton/core'; + +export function toBounceableAddress(data?: Address | string | null): string | undefined { + if (data instanceof Address) { + return data.toString(); + } + + try { + if (data) return Address.parse(data).toString({ bounceable: true }); + } catch { + // + } +} + +export function toNonBounceableAddress(data?: Address | string | null): string | undefined { + if (data instanceof Address) { + return data.toString({ bounceable: false }); + } + + try { + if (data) return Address.parse(data).toString({ bounceable: false }); + } catch { + // + } +} diff --git a/packages/appkit/src/utils/amount/calc-fiat-value.test.ts b/packages/appkit/src/utils/amount/calc-fiat-value.test.ts new file mode 100644 index 000000000..a2ae3d7af --- /dev/null +++ b/packages/appkit/src/utils/amount/calc-fiat-value.test.ts @@ -0,0 +1,45 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { describe, it, expect } from 'vitest'; + +import { calcFiatValue } from './calc-fiat-value'; + +describe('calcFiatValue', () => { + it('should return 0 when rate is undefined', () => { + expect(calcFiatValue('100', undefined)).toBe('0'); + }); + + it('should return 0 when rate is 0', () => { + expect(calcFiatValue('100', '0')).toBe('0'); + }); + + it('should return 0 when amount is 0', () => { + expect(calcFiatValue('0', '1.5')).toBe('0'); + }); + + it('should return 0 when amount is negative', () => { + expect(calcFiatValue('-10', '1.5')).toBe('0'); + }); + + it('should return 0 when amount is not a valid number', () => { + expect(calcFiatValue('abc', '1.5')).toBe('0'); + expect(calcFiatValue('', '1.5')).toBe('0'); + }); + + it('should calculate fiat value with 2 decimal places', () => { + expect(calcFiatValue('100', '1.5')).toBe('150'); + expect(calcFiatValue('1', '0.001')).toBe('0'); + expect(calcFiatValue('3', '1.005')).toBe('3.01'); + }); + + it('should handle decimal amounts', () => { + expect(calcFiatValue('0.5', '2')).toBe('1'); + expect(calcFiatValue('1.23456', '100')).toBe('123.46'); + }); +}); diff --git a/packages/appkit/src/utils/amount/calc-fiat-value.ts b/packages/appkit/src/utils/amount/calc-fiat-value.ts new file mode 100644 index 000000000..5353a58aa --- /dev/null +++ b/packages/appkit/src/utils/amount/calc-fiat-value.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +/** + * Calculates the fiat value of a token amount. + * Returns null when rate is unavailable or amount is zero/invalid. + */ +export function calcFiatValue(amount: string, rate: string | undefined): string { + if (!rate) return '0'; + const num = parseFloat(amount); + if (!num || num <= 0) return '0'; + return Number((num * parseFloat(rate)).toFixed(2)).toString(); +} diff --git a/packages/appkit/src/utils/amount/format-large-value.test.ts b/packages/appkit/src/utils/amount/format-large-value.test.ts new file mode 100644 index 000000000..38c959646 --- /dev/null +++ b/packages/appkit/src/utils/amount/format-large-value.test.ts @@ -0,0 +1,44 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { describe, it, expect } from 'vitest'; + +import { formatLargeValue } from './format-large-value'; + +describe('formatLargeValue', () => { + it('should format trillion values', () => { + expect(formatLargeValue('1000000000000')).toBe('1T'); + expect(formatLargeValue('1230000000000')).toBe('1.23T'); + expect(formatLargeValue('12345678901234')).toBe('12.34T'); + }); + + it('should format billion values', () => { + expect(formatLargeValue('1000000000')).toBe('1B'); + expect(formatLargeValue('1230000000')).toBe('1.23B'); + }); + + it('should format million values', () => { + expect(formatLargeValue('1000000')).toBe('1M'); + expect(formatLargeValue('1230000')).toBe('1.23M'); + }); + + it('should format smaller values using toLocaleString', () => { + expect(formatLargeValue('1234.56')).toBe('1,234.56'); + expect(formatLargeValue('100')).toBe('100'); + expect(formatLargeValue('0')).toBe('0'); + }); + + it('should handle spaces and formatting', () => { + expect(formatLargeValue('1 000 000')).toBe('1M'); + }); + + it('should respect decimals for small values', () => { + expect(formatLargeValue('1234.5678', 4)).toBe('1,234.5678'); + expect(formatLargeValue('1234.5678', 2)).toBe('1,234.56'); + }); +}); diff --git a/packages/appkit/src/utils/amount/format-large-value.ts b/packages/appkit/src/utils/amount/format-large-value.ts new file mode 100644 index 000000000..9af2d062f --- /dev/null +++ b/packages/appkit/src/utils/amount/format-large-value.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export const formatLargeValue = (amount: string, decimals: number = 2): string => { + const cleanAmount = amount.toString().replace(/\s/g, ''); + const intPart = cleanAmount.split('.')[0] || '0'; + + // value > 100 000 000 000 000 => 100 T + if (intPart.length > 12) { + const value = Number(intPart.slice(0, -10)) / 100; + return `${value.toLocaleString('en-US')}T`; + } + + // value > 100 000 000 000 => 100 B + if (intPart.length > 9) { + const value = Number(intPart.slice(0, -7)) / 100; + return `${value.toLocaleString('en-US')}B`; + } + + // value > 10 000 000 => 10 M + if (intPart.length > 6) { + const value = Number(intPart.slice(0, -4)) / 100; + return `${value.toLocaleString('en-US')}M`; + } + + const value = parseFloat(cleanAmount); + if (isNaN(value)) { + return '0'; + } + + const factor = Math.pow(10, decimals); + const truncated = Math.floor(value * factor) / factor; + + return truncated.toLocaleString('en-US', { + minimumFractionDigits: 0, + maximumFractionDigits: decimals, + }); +}; diff --git a/packages/appkit/src/utils/functions/debounce.ts b/packages/appkit/src/utils/functions/debounce.ts new file mode 100644 index 000000000..29e3166b5 --- /dev/null +++ b/packages/appkit/src/utils/functions/debounce.ts @@ -0,0 +1,169 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export interface DebounceOptions { + /** + * An optional AbortSignal to cancel the debounced function. + */ + signal?: AbortSignal; + + /** + * An optional array specifying whether the function should be invoked on the leading edge, trailing edge, or both. + * If `edges` include "leading", the function will be invoked at the start of the delay period. + * If `edges` include "trailing", the function will be invoked at the end of the delay period. + * If both "leading" and "trailing" are included, the function will be invoked at both the start and end of the delay period. + * @default ["trailing"] + */ + edges?: Array<'leading' | 'trailing'>; +} + +export interface DebouncedFunction void> { + (...args: Parameters): void; + + /** + * Schedules the execution of the debounced function after the specified debounced delay. + * This method resets any existing timer, ensuring that the function is only invoked + * after the delay has elapsed since the last call to the debounced function. + * It is typically called internally whenever the debounced function is invoked. + * + * @returns {void} + */ + schedule: () => void; + + /** + * Cancels any pending execution of the debounced function. + * This method clears the active timer and resets any stored context or arguments. + */ + cancel: () => void; + + /** + * Immediately invokes the debounced function if there is a pending execution. + * This method executes the function right away if there is a pending execution. + */ + flush: () => void; +} + +/** + * Creates a debounced function that delays invoking the provided function until after `debounceMs` milliseconds + * have elapsed since the last time the debounced function was invoked. The debounced function also has a `cancel` + * method to cancel any pending execution. + * + * @template F - The type of function. + * @param {F} func - The function to debounce. + * @param {number} debounceMs - The number of milliseconds to delay. + * @param {DebounceOptions} options - The options object + * @param {AbortSignal} options.signal - An optional AbortSignal to cancel the debounced function. + * @returns A new debounced function with a `cancel` method. + * + * @example + * const debouncedFunction = debounce(() => { + * console.log('Function executed'); + * }, 1000); + * + * // Will log 'Function executed' after 1 second if not called again in that time + * debouncedFunction(); + * + * // Will not log anything as the previous call is canceled + * debouncedFunction.cancel(); + * + * // With AbortSignal + * const controller = new AbortController(); + * const signal = controller.signal; + * const debouncedWithSignal = debounce(() => { + * console.log('Function executed'); + * }, 1000, { signal }); + * + * debouncedWithSignal(); + * + * // Will cancel the debounced function call + * controller.abort(); + */ +export const debounce = void>( + func: F, + debounceMs: number, + { signal, edges }: DebounceOptions = {}, +): DebouncedFunction => { + let pendingThis: unknown = undefined; + let pendingArgs: Parameters | null = null; + + const leading = edges != null && edges.includes('leading'); + const trailing = edges == null || edges.includes('trailing'); + + const invoke = () => { + if (pendingArgs !== null) { + func.apply(pendingThis, pendingArgs); + pendingThis = undefined; + pendingArgs = null; + } + }; + + const onTimerEnd = () => { + if (trailing) { + invoke(); + } + + cancel(); + }; + + let timeoutId: ReturnType | null = null; + + const schedule = () => { + if (timeoutId != null) { + clearTimeout(timeoutId); + } + + timeoutId = setTimeout(() => { + timeoutId = null; + + onTimerEnd(); + }, debounceMs); + }; + + const cancelTimer = () => { + if (timeoutId !== null) { + clearTimeout(timeoutId); + timeoutId = null; + } + }; + + const cancel = () => { + cancelTimer(); + pendingThis = undefined; + pendingArgs = null; + }; + + const flush = () => { + invoke(); + }; + + const debounced = function (this: unknown, ...args: Parameters) { + if (signal?.aborted) { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-this-alias + pendingThis = this; + pendingArgs = args; + + const isFirstCall = timeoutId == null; + + schedule(); + + if (leading && isFirstCall) { + invoke(); + } + }; + + debounced.schedule = schedule; + debounced.cancel = cancel; + debounced.flush = flush; + + signal?.addEventListener('abort', cancel, { once: true }); + + return debounced; +}; diff --git a/packages/appkit/src/utils/index.ts b/packages/appkit/src/utils/index.ts index b7754dc6a..f66acd3ae 100644 --- a/packages/appkit/src/utils/index.ts +++ b/packages/appkit/src/utils/index.ts @@ -8,10 +8,14 @@ export { formatUnits, parseUnits, compareAddress } from '@ton/walletkit'; +export * from './address/format'; +export * from './amount/calc-fiat-value'; +export * from './amount/format-large-value'; export * from './amount/validate-numeric-string'; export * from './arrays/key-by'; export * from './arrays/random-from-array'; export * from './errors/get-error-message'; +export * from './functions/debounce'; export * from './functions/noop'; export * from './jetton/jetton-info'; export * from './nft/nft-info'; diff --git a/packages/walletkit/src/api/interfaces/SwapAPI.ts b/packages/walletkit/src/api/interfaces/SwapAPI.ts index 6269e9384..9955d79d4 100644 --- a/packages/walletkit/src/api/interfaces/SwapAPI.ts +++ b/packages/walletkit/src/api/interfaces/SwapAPI.ts @@ -6,7 +6,7 @@ * */ -import type { SwapParams, SwapQuote, SwapQuoteParams, TransactionRequest } from '../models'; +import type { SwapParams, SwapQuote, SwapQuoteParams, TransactionRequest, SwapProviderMetadata } from '../models'; import type { DefiManagerAPI } from './DefiManagerAPI'; import type { DefiProvider } from './DefiProvider'; @@ -41,6 +41,12 @@ export interface SwapProviderInterface): Promise; + + /** + * Get provider metadata + */ + abstract getMetadata(): SwapProviderMetadata; } diff --git a/packages/walletkit/src/defi/swap/dedust/DeDustSwapProvider.ts b/packages/walletkit/src/defi/swap/dedust/DeDustSwapProvider.ts index 9f0bf3732..ca3d6a527 100644 --- a/packages/walletkit/src/defi/swap/dedust/DeDustSwapProvider.ts +++ b/packages/walletkit/src/defi/swap/dedust/DeDustSwapProvider.ts @@ -16,7 +16,7 @@ import type { } from './models'; import type { DeDustSwapResponse } from './DeDustPrivateTypes'; import { SwapProvider } from '../SwapProvider'; -import type { SwapQuoteParams, SwapQuote, SwapParams } from '../../../api/models'; +import type { SwapQuoteParams, SwapQuote, SwapParams, SwapProviderMetadata } from '../../../api/models'; import { SwapError } from '../errors'; import { globalLogger } from '../../../core/Logger'; import { tokenToMinter, validateNetwork, isDeDustQuoteMetadata } from './utils'; @@ -76,6 +76,11 @@ export class DeDustSwapProvider extends SwapProvider): Promise { log.debug('Getting DeDust quote', { fromToken: params.from, diff --git a/packages/walletkit/src/defi/swap/dedust/models/DeDustSwapProviderConfig.ts b/packages/walletkit/src/defi/swap/dedust/models/DeDustSwapProviderConfig.ts index 7dc22e72d..9b28aafd5 100644 --- a/packages/walletkit/src/defi/swap/dedust/models/DeDustSwapProviderConfig.ts +++ b/packages/walletkit/src/defi/swap/dedust/models/DeDustSwapProviderConfig.ts @@ -6,6 +6,7 @@ * */ +import type { SwapProviderMetadata } from '../../../../api/models'; import type { DeDustReferralOptions } from './DeDustReferralOptions'; /** @@ -56,4 +57,9 @@ export interface DeDustSwapProviderConfig extends DeDustReferralOptions { * @default '5000' */ minPoolUsdTvl?: string; + + /** + * Custom metadata for the provider + */ + metadata?: Partial; } diff --git a/packages/walletkit/src/defi/swap/omniston/OmnistonSwapProvider.ts b/packages/walletkit/src/defi/swap/omniston/OmnistonSwapProvider.ts index 120e1a5b8..382cb4c93 100644 --- a/packages/walletkit/src/defi/swap/omniston/OmnistonSwapProvider.ts +++ b/packages/walletkit/src/defi/swap/omniston/OmnistonSwapProvider.ts @@ -12,7 +12,7 @@ import { Address } from '@ton/core'; import type { OmnistonQuoteMetadata, OmnistonSwapProviderConfig, OmnistonProviderOptions } from './models'; import { SwapProvider } from '../SwapProvider'; -import type { SwapQuoteParams, SwapQuote, SwapParams, SwapFee } from '../../../api/models'; +import type { SwapQuoteParams, SwapQuote, SwapParams, SwapFee, SwapProviderMetadata } from '../../../api/models'; import { SwapError } from '../errors'; import { globalLogger } from '../../../core/Logger'; import { tokenToAddress, toOmnistonAddress, isOmnistonQuoteMetadata } from './utils'; @@ -51,6 +51,10 @@ export class OmnistonSwapProvider extends SwapProvider private readonly referrerFeeBps?: number; private readonly flexibleReferrerFee: boolean; private omniston$?: Omniston; + private metadata: SwapProviderMetadata = { + name: 'Omniston', + url: 'https://ston.fi/omniston', + }; readonly providerId: string; @@ -66,12 +70,23 @@ export class OmnistonSwapProvider extends SwapProvider this.referrerFeeBps = config?.referrerFeeBps; this.flexibleReferrerFee = config?.flexibleReferrerFee ?? false; + if (config?.metadata) { + this.metadata = { + ...this.metadata, + ...config.metadata, + }; + } + log.info('OmnistonSwapProvider initialized', { defaultSlippageBps: this.defaultSlippageBps, hasReferrer: !!this.referrerAddress, }); } + getMetadata(): SwapProviderMetadata { + return this.metadata; + } + private get omniston(): Omniston { if (!this.omniston$) { this.omniston$ = new Omniston({ apiUrl: this.apiUrl }); diff --git a/packages/walletkit/src/defi/swap/omniston/models/OmnistonSwapProviderConfig.ts b/packages/walletkit/src/defi/swap/omniston/models/OmnistonSwapProviderConfig.ts index 2a7b13cc1..a9c55d1aa 100644 --- a/packages/walletkit/src/defi/swap/omniston/models/OmnistonSwapProviderConfig.ts +++ b/packages/walletkit/src/defi/swap/omniston/models/OmnistonSwapProviderConfig.ts @@ -6,6 +6,7 @@ * */ +import type { SwapProviderMetadata } from '../../../../api/models'; import type { OmnistonReferrerOptions } from './OmnistonReferrerOptions'; /** @@ -34,4 +35,9 @@ export interface OmnistonSwapProviderConfig extends OmnistonReferrerOptions { * Identifier for the provider */ providerId?: string; + + /** + * Custom metadata for the provider + */ + metadata?: Partial; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9735660ae..f8ab6eb3a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -541,6 +541,9 @@ importers: demo/examples: dependencies: + '@tanstack/react-query': + specifier: 'catalog:' + version: 5.90.20(react@19.2.3) '@ton/appkit': specifier: workspace:* version: link:../../packages/appkit @@ -700,17 +703,11 @@ importers: version: 1.1.0 devDependencies: '@storybook/addon-docs': - specifier: 10.2.8 - version: 10.2.8(@types/react@19.2.8)(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.8(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) - '@storybook/react': - specifier: 10.2.8 - version: 10.2.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.8(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3) + specifier: 10.3.3 + version: 10.3.3(@types/react@19.2.8)(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) '@storybook/react-vite': - specifier: 10.2.8 - version: 10.2.8(esbuild@0.27.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.59.0)(storybook@10.2.8(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) - '@storybook/test': - specifier: ^8.6.15 - version: 8.6.15(storybook@10.2.8(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + specifier: 10.3.3 + version: 10.3.3(esbuild@0.27.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/react-query': specifier: 'catalog:' version: 5.90.20(react@19.2.3) @@ -733,8 +730,8 @@ importers: specifier: 'catalog:' version: 19.2.3(react@19.2.3) storybook: - specifier: 10.2.8 - version: 10.2.8(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: 10.3.3 + version: 10.3.3(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) typescript: specifier: ^5.9.2 version: 5.9.3 @@ -896,7 +893,7 @@ importers: version: link:../walletkit '@tonconnect/bridge-sdk': specifier: ^0.2.4 - version: 0.2.4 + version: 0.2.8 '@tonconnect/protocol': specifier: 'catalog:' version: 2.4.0 @@ -3532,23 +3529,23 @@ packages: '@ston-fi/omniston-sdk@0.7.8': resolution: {integrity: sha512-2qXKbFGgwcPEVxTGBqvOjoH8914KAuXzUN7Id1KGe1rCLafZG8OKRg1u+c72ER/vthPhAMANWUsnitjhxoXXvg==} - '@storybook/addon-docs@10.2.8': - resolution: {integrity: sha512-cEoWqQrLzrxOwZFee5zrD4cYrdEWKV80POb7jUZO0r5vfl2DuslIr3n/+RfLT52runCV4aZcFEfOfP/IWHNPxg==} + '@storybook/addon-docs@10.3.3': + resolution: {integrity: sha512-trJQTpOtuOEuNv1Rn8X2Sopp5hSPpb0u0soEJ71BZAbxe4d2Y1d/1MYcxBdRKwncum6sCTsnxTpqQ/qvSJKlTQ==} peerDependencies: - storybook: ^10.2.8 + storybook: ^10.3.3 - '@storybook/builder-vite@10.2.8': - resolution: {integrity: sha512-+6/Lwi7W0YIbzHDh798GPp0IHUYDwp0yv0Y1eVNK/StZD0tnv4/1C28NKyP+O7JOsFsuWI1qHiDhw8kNURugZw==} + '@storybook/builder-vite@10.3.3': + resolution: {integrity: sha512-awspKCTZvXyeV3KabL0id62mFbxR5u/5yyGQultwCiSb2/yVgBfip2MAqLyS850pvTiB6QFVM9deOyd2/G/bEA==} peerDependencies: - storybook: ^10.2.8 - vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + storybook: ^10.3.3 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - '@storybook/csf-plugin@10.2.8': - resolution: {integrity: sha512-kKkLYhRXb33YtIPdavD2DU25sb14sqPYdcQFpyqu4TaD9truPPqW8P5PLTUgERydt/eRvRlnhauPHavU1kjsnA==} + '@storybook/csf-plugin@10.3.3': + resolution: {integrity: sha512-Utlh7zubm+4iOzBBfzLW4F4vD99UBtl2Do4edlzK2F7krQIcFvR2ontjAE8S1FQVLZAC3WHalCOS+Ch8zf3knA==} peerDependencies: esbuild: '*' rollup: '*' - storybook: ^10.2.8 + storybook: ^10.3.3 vite: '*' webpack: '*' peerDependenciesMeta: @@ -3570,42 +3567,32 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@storybook/instrumenter@8.6.15': - resolution: {integrity: sha512-TvHR/+yyIAOp/1bLulFai2kkhIBtAlBw7J6Jd9DKyInoGhTWNE1G1Y61jD5GWXX29AlwaHfzGUaX5NL1K+FJpg==} - peerDependencies: - storybook: ^8.6.15 - - '@storybook/react-dom-shim@10.2.8': - resolution: {integrity: sha512-Xde9X3VszFV1pTXfc2ZFM89XOCGRxJD8MUIzDwkcT9xaki5a+8srs/fsXj75fMY6gMYfcL5lNRZvCqg37HOmcQ==} + '@storybook/react-dom-shim@10.3.3': + resolution: {integrity: sha512-lkhuh4G3UTreU9M3Iz5Dt32c6U+l/4XuvqLtbe1sDHENZH6aPj7y0b5FwnfHyvuTvYRhtbo29xZrF5Bp9kCC0w==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.2.8 + storybook: ^10.3.3 - '@storybook/react-vite@10.2.8': - resolution: {integrity: sha512-x5kmw+TPhxkQV84n4e9X0q6/rA5T8V2QQFolMuN+U93q1HX1r+GZ6g/nXaaq9ox168PhHUJZQnn+LzSQKGCMBA==} + '@storybook/react-vite@10.3.3': + resolution: {integrity: sha512-qHdlBe1hjqFAGXa8JL7bWTLbP/gDqXbWDm+SYCB646NHh5yvVDkZLwigP5Y+UL7M2ASfqFtosnroUK9tcCM2dw==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.2.8 - vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + storybook: ^10.3.3 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - '@storybook/react@10.2.8': - resolution: {integrity: sha512-nMFqQFUXq6Zg2O5SeuomyWnrIx61QfpNQMrfor8eCEzHrWNnXrrvVsz2RnHIgXN8RVyaWGDPh1srAECu/kDHXw==} + '@storybook/react@10.3.3': + resolution: {integrity: sha512-cGG5TbR8Tdx9zwlpsWyBEfWrejm5iWdYF26EwIhwuKq9GFUTAVrQzo0Rs7Tqc3ZyVhRS/YfsRiWSEH+zmq2JiQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.2.8 + storybook: ^10.3.3 typescript: '>= 4.9.x' peerDependenciesMeta: typescript: optional: true - '@storybook/test@8.6.15': - resolution: {integrity: sha512-EwquDRUDVvWcZds3T2abmB5wSN/Vattal4YtZ6fpBlIUqONV4o/cOBX39cFfQSUCBrIXIjQ6RmapQCHK/PvBYw==} - peerDependencies: - storybook: ^8.6.15 - '@stryker-mutator/api@9.5.1': resolution: {integrity: sha512-Z8Waw3v9XfqouOKnRjPv0ePnu7UfYfErJaNE2+al2bqquFaTuONYaeED55A4gzupjmfdGCfBdnMdmiuH4zww5g==} engines: {node: '>=20.0.0'} @@ -3720,18 +3707,10 @@ packages: peerDependencies: react: ^18 || ^19 - '@testing-library/dom@10.4.0': - resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} - engines: {node: '>=18'} - '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} - '@testing-library/jest-dom@6.5.0': - resolution: {integrity: sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==} - engines: {node: '>=14', npm: '>=6', yarn: '>=1'} - '@testing-library/jest-dom@6.9.1': resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} @@ -3751,12 +3730,6 @@ packages: '@types/react-dom': optional: true - '@testing-library/user-event@14.5.2': - resolution: {integrity: sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==} - engines: {node: '>=12', npm: '>=6'} - peerDependencies: - '@testing-library/dom': '>=7.21.4' - '@testing-library/user-event@14.6.1': resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} engines: {node: '>=12', npm: '>=6'} @@ -3789,9 +3762,6 @@ packages: resolution: {tarball: https://codeload.github.com/the-ton-tech/toolchain/tar.gz/31376da778155bd0984d68abf2a46dce417bacb8} version: 1.5.0 - '@tonconnect/bridge-sdk@0.2.4': - resolution: {integrity: sha512-W7fDoo8aT9FYnGPPLqh4Giiu1M5FP3GW4A+AeDB/xfTzM9XsO9aoWoSfrR1+QuNl2OmvtvneCFv4MnowM9+feg==} - '@tonconnect/bridge-sdk@0.2.8': resolution: {integrity: sha512-bii7xs8+jeoBCXQM/VTA83EWGVnDhURrxI9YPGR3RQdSoA+EcxEiX2AGW6god3Ar8mk989eMXRCxcAVLGGHeLw==} @@ -4149,9 +4119,6 @@ packages: '@vitest/browser': optional: true - '@vitest/expect@2.0.5': - resolution: {integrity: sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==} - '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} @@ -4169,12 +4136,6 @@ packages: vite: optional: true - '@vitest/pretty-format@2.0.5': - resolution: {integrity: sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==} - - '@vitest/pretty-format@2.1.9': - resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} - '@vitest/pretty-format@3.2.4': resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} @@ -4190,9 +4151,6 @@ packages: '@vitest/snapshot@4.0.18': resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} - '@vitest/spy@2.0.5': - resolution: {integrity: sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==} - '@vitest/spy@3.2.4': resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} @@ -4204,12 +4162,6 @@ packages: peerDependencies: vitest: 4.0.18 - '@vitest/utils@2.0.5': - resolution: {integrity: sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==} - - '@vitest/utils@2.1.9': - resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} - '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} @@ -4222,6 +4174,7 @@ packages: '@xmldom/xmldom@0.8.11': resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} engines: {node: '>=10.0.0'} + deprecated: this version has critical issues, please update to the latest version abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} @@ -4754,10 +4707,6 @@ packages: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} - chalk@3.0.0: - resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} - engines: {node: '>=8'} - chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -7903,6 +7852,7 @@ packages: prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true prelude-ls@1.2.1: @@ -8788,8 +8738,8 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} - storybook@10.2.8: - resolution: {integrity: sha512-885uSIn8NQw2ZG7vy84K45lHCOSyz1DVsDV8pHiHQj3J0riCuWLNeO50lK9z98zE8kjhgTtxAAkMTy5nkmNRKQ==} + storybook@10.3.3: + resolution: {integrity: sha512-tMoRAts9EVqf+mEMPLC6z1DPyHbcPe+CV1MhLN55IKsl0HxNjvVGK44rVPSePbltPE6vIsn4bdRj6CCUt8SJwQ==} hasBin: true peerDependencies: prettier: ^2 || ^3 @@ -9059,10 +9009,6 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - tinyrainbow@1.2.0: - resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} - engines: {node: '>=14.0.0'} - tinyrainbow@2.0.0: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} @@ -9071,10 +9017,6 @@ packages: resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} - tinyspy@3.0.2: - resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} - engines: {node: '>=14.0.0'} - tinyspy@4.0.4: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} @@ -13347,15 +13289,15 @@ snapshots: - bufferutil - utf-8-validate - '@storybook/addon-docs@10.2.8(@types/react@19.2.8)(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.8(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + '@storybook/addon-docs@10.3.3(@types/react@19.2.8)(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.2.8)(react@19.2.3) - '@storybook/csf-plugin': 10.2.8(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.8(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@storybook/csf-plugin': 10.3.3(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) '@storybook/icons': 2.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@storybook/react-dom-shim': 10.2.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.8(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + '@storybook/react-dom-shim': 10.3.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - storybook: 10.2.8(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + storybook: 10.3.3(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' @@ -13364,10 +13306,10 @@ snapshots: - vite - webpack - '@storybook/builder-vite@10.2.8(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.8(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + '@storybook/builder-vite@10.3.3(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@storybook/csf-plugin': 10.2.8(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.8(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) - storybook: 10.2.8(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@storybook/csf-plugin': 10.3.3(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + storybook: 10.3.3(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) ts-dedent: 2.2.0 vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: @@ -13375,9 +13317,9 @@ snapshots: - rollup - webpack - '@storybook/csf-plugin@10.2.8(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.8(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + '@storybook/csf-plugin@10.3.3(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - storybook: 10.2.8(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + storybook: 10.3.3(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) unplugin: 2.3.11 optionalDependencies: esbuild: 0.27.3 @@ -13391,31 +13333,25 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@storybook/instrumenter@8.6.15(storybook@10.2.8(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))': - dependencies: - '@storybook/global': 5.0.0 - '@vitest/utils': 2.1.9 - storybook: 10.2.8(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - - '@storybook/react-dom-shim@10.2.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.8(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))': + '@storybook/react-dom-shim@10.3.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))': dependencies: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - storybook: 10.2.8(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + storybook: 10.3.3(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@storybook/react-vite@10.2.8(esbuild@0.27.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.59.0)(storybook@10.2.8(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + '@storybook/react-vite@10.3.3(esbuild@0.27.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@joshwooding/vite-plugin-react-docgen-typescript': 0.6.4(typescript@5.9.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) '@rollup/pluginutils': 5.3.0(rollup@4.59.0) - '@storybook/builder-vite': 10.2.8(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.2.8(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) - '@storybook/react': 10.2.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.8(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3) + '@storybook/builder-vite': 10.3.3(esbuild@0.27.3)(rollup@4.59.0)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@storybook/react': 10.3.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3) empathic: 2.0.0 magic-string: 0.30.21 react: 19.2.3 react-docgen: 8.0.2 react-dom: 19.2.3(react@19.2.3) resolve: 1.22.11 - storybook: 10.2.8(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + storybook: 10.3.3(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) tsconfig-paths: 4.2.0 vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: @@ -13425,30 +13361,20 @@ snapshots: - typescript - webpack - '@storybook/react@10.2.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.8(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)': + '@storybook/react@10.3.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 10.2.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.2.8(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + '@storybook/react-dom-shim': 10.3.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) react: 19.2.3 react-docgen: 8.0.2 + react-docgen-typescript: 2.4.0(typescript@5.9.3) react-dom: 19.2.3(react@19.2.3) - storybook: 10.2.8(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + storybook: 10.3.3(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@storybook/test@8.6.15(storybook@10.2.8(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))': - dependencies: - '@storybook/global': 5.0.0 - '@storybook/instrumenter': 8.6.15(storybook@10.2.8(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) - '@testing-library/dom': 10.4.0 - '@testing-library/jest-dom': 6.5.0 - '@testing-library/user-event': 14.5.2(@testing-library/dom@10.4.0) - '@vitest/expect': 2.0.5 - '@vitest/spy': 2.0.5 - storybook: 10.2.8(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@stryker-mutator/api@9.5.1': dependencies: mutation-testing-metrics: 3.7.1 @@ -13582,17 +13508,6 @@ snapshots: '@tanstack/query-core': 5.90.20 react: 19.2.3 - '@testing-library/dom@10.4.0': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/runtime': 7.28.6 - '@types/aria-query': 5.0.4 - aria-query: 5.3.0 - chalk: 4.1.2 - dom-accessibility-api: 0.5.16 - lz-string: 1.5.0 - pretty-format: 27.5.1 - '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.29.0 @@ -13604,16 +13519,6 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.5.0': - dependencies: - '@adobe/css-tools': 4.4.4 - aria-query: 5.3.2 - chalk: 3.0.0 - css.escape: 1.5.1 - dom-accessibility-api: 0.6.3 - lodash: 4.17.23 - redent: 3.0.0 - '@testing-library/jest-dom@6.9.1': dependencies: '@adobe/css-tools': 4.4.4 @@ -13633,10 +13538,6 @@ snapshots: '@types/react': 19.2.8 '@types/react-dom': 19.2.3(@types/react@19.2.8) - '@testing-library/user-event@14.5.2(@testing-library/dom@10.4.0)': - dependencies: - '@testing-library/dom': 10.4.0 - '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': dependencies: '@testing-library/dom': 10.4.1 @@ -13695,14 +13596,6 @@ snapshots: - supports-color - typescript - '@tonconnect/bridge-sdk@0.2.4': - dependencies: - '@tonconnect/isomorphic-eventsource': 0.0.2 - '@tonconnect/isomorphic-fetch': 0.0.3 - '@tonconnect/protocol': 2.4.0 - transitivePeerDependencies: - - encoding - '@tonconnect/bridge-sdk@0.2.8': dependencies: '@tonconnect/isomorphic-eventsource': 0.0.2 @@ -14132,13 +14025,6 @@ snapshots: tinyrainbow: 3.0.3 vitest: 4.0.18(@types/node@25.2.3)(@vitest/ui@4.0.18)(happy-dom@20.8.3)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - '@vitest/expect@2.0.5': - dependencies: - '@vitest/spy': 2.0.5 - '@vitest/utils': 2.0.5 - chai: 5.3.3 - tinyrainbow: 1.2.0 - '@vitest/expect@3.2.4': dependencies: '@types/chai': 5.2.3 @@ -14172,14 +14058,6 @@ snapshots: optionalDependencies: vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - '@vitest/pretty-format@2.0.5': - dependencies: - tinyrainbow: 1.2.0 - - '@vitest/pretty-format@2.1.9': - dependencies: - tinyrainbow: 1.2.0 - '@vitest/pretty-format@3.2.4': dependencies: tinyrainbow: 2.0.0 @@ -14203,10 +14081,6 @@ snapshots: magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@2.0.5': - dependencies: - tinyspy: 3.0.2 - '@vitest/spy@3.2.4': dependencies: tinyspy: 4.0.4 @@ -14224,19 +14098,6 @@ snapshots: tinyrainbow: 3.0.3 vitest: 4.0.18(@types/node@25.2.3)(@vitest/ui@4.0.18)(happy-dom@20.8.3)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - '@vitest/utils@2.0.5': - dependencies: - '@vitest/pretty-format': 2.0.5 - estree-walker: 3.0.3 - loupe: 3.2.1 - tinyrainbow: 1.2.0 - - '@vitest/utils@2.1.9': - dependencies: - '@vitest/pretty-format': 2.1.9 - loupe: 3.2.1 - tinyrainbow: 1.2.0 - '@vitest/utils@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 @@ -14930,11 +14791,6 @@ snapshots: escape-string-regexp: 1.0.5 supports-color: 5.5.0 - chalk@3.0.0: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -17659,7 +17515,7 @@ snapshots: md5.js@1.3.5: dependencies: - hash-base: 3.0.5 + hash-base: 3.1.2 inherits: 2.0.4 safe-buffer: 5.2.1 @@ -19767,7 +19623,7 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 - storybook@10.2.8(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + storybook@10.3.3(@testing-library/dom@10.4.1)(prettier@3.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 2.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -20085,14 +19941,10 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - tinyrainbow@1.2.0: {} - tinyrainbow@2.0.0: {} tinyrainbow@3.0.3: {} - tinyspy@3.0.2: {} - tinyspy@4.0.4: {} tmp@0.2.5: {} diff --git a/template/packages/appkit-react/docs/hooks.md b/template/packages/appkit-react/docs/hooks.md index 13a4c057e..5079f14d1 100644 --- a/template/packages/appkit-react/docs/hooks.md +++ b/template/packages/appkit-react/docs/hooks.md @@ -182,6 +182,12 @@ Hook to build a transaction for a swap operation based on a quote. %%demo/examples/src/appkit/hooks/swap#USE_BUILD_SWAP_TRANSACTION%% +### `useSwapProvider` + +Hook to get a specific swap provider. Returns the provider instance directly or `null` if not found. + +%%demo/examples/src/appkit/hooks/swap#USE_SWAP_PROVIDER%% + ## Transaction ### `useSendTransaction` diff --git a/template/packages/appkit/docs/actions.md b/template/packages/appkit/docs/actions.md index ad33b0014..518a45075 100644 --- a/template/packages/appkit/docs/actions.md +++ b/template/packages/appkit/docs/actions.md @@ -244,6 +244,18 @@ Get the `SwapManager` instance to interact with swap providers directly. %%demo/examples/src/appkit/actions/swap#GET_SWAP_MANAGER%% +### `getSwapProvider` + +Get a specific swap provider by its ID. + +%%demo/examples/src/appkit/actions/swap#GET_SWAP_PROVIDER%% + +### `watchSwapProviders` + +Watch for new swap providers registration. + +%%demo/examples/src/appkit/actions/swap#WATCH_SWAP_PROVIDERS%% + ### `getSwapQuote` Get a swap quote from registered providers.