diff --git a/docs/assets/savings-widget-ui.png b/docs/assets/savings-widget-ui.png new file mode 100644 index 0000000..157b7be Binary files /dev/null and b/docs/assets/savings-widget-ui.png differ diff --git a/examples/storybook/package.json b/examples/storybook/package.json index 044f9aa..9275e14 100644 --- a/examples/storybook/package.json +++ b/examples/storybook/package.json @@ -10,11 +10,13 @@ }, "dependencies": { "@goodwidget/core": "workspace:*", + "@goodwidget/savings-widget": "workspace:*", "@goodwidget/ui": "workspace:*", "@goodwidget/claim-widget-theme-demo": "workspace:*", "react": "^18.3.0", "react-dom": "^18.3.0", - "react-native-web": "^0.19.13" + "react-native-web": "^0.19.13", + "viem": "^2.31.0" }, "devDependencies": { "@storybook/addon-essentials": "^8.6.17", diff --git a/examples/storybook/src/stories/ClaimWidget.stories.tsx b/examples/storybook/src/stories/ClaimWidget.stories.tsx index 9065e50..0edff8b 100644 --- a/examples/storybook/src/stories/ClaimWidget.stories.tsx +++ b/examples/storybook/src/stories/ClaimWidget.stories.tsx @@ -10,12 +10,12 @@ */ import React from 'react' import type { Meta, StoryObj } from '@storybook/react' -import { ClaimWidget } from '@goodwidget/claim-widget' +import { ClaimWidget } from '@goodwidget/claim-widget-theme-demo' import { YStack } from '@goodwidget/ui' -import { createMockEip1193Provider } from '../fixtures/mockEip1193' +import { createCustodialEip1193Provider } from '../fixtures/custodialEip1193' // Stable mock provider — created once at module level to prevent re-render churn. -const mockProvider = createMockEip1193Provider() +const mockProvider = createCustodialEip1193Provider() /** Cobalt brand overrides (matches the "Host / Cobalt" tab in ThemePlayground). */ const cobaltOverrides = { diff --git a/examples/storybook/src/stories/SavingsWidget.stories.tsx b/examples/storybook/src/stories/SavingsWidget.stories.tsx new file mode 100644 index 0000000..3c18867 --- /dev/null +++ b/examples/storybook/src/stories/SavingsWidget.stories.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import type { Meta, StoryObj } from '@storybook/react' +import { SavingsWidget } from '@goodwidget/savings-widget' +import { YStack } from '@goodwidget/ui' +import { createCustodialEip1193Provider } from '../fixtures/custodialEip1193' + +const mockProvider = createCustodialEip1193Provider() + +const meta: Meta = { + title: 'Widgets/SavingsWidget', + component: SavingsWidget, + tags: ['autodocs'], + parameters: { layout: 'padded' }, +} + +export default meta + +type Story = StoryObj + +export const Default: Story = { + render: () => ( + + + + ), +} + +export const Disconnected: Story = { + render: () => ( + + undefined} /> + + ), +} diff --git a/examples/storybook/src/stories/ThemePlayground.stories.tsx b/examples/storybook/src/stories/ThemePlayground.stories.tsx index bb9d846..a72a6f1 100644 --- a/examples/storybook/src/stories/ThemePlayground.stories.tsx +++ b/examples/storybook/src/stories/ThemePlayground.stories.tsx @@ -13,11 +13,11 @@ */ import React from 'react' import type { Meta, StoryObj } from '@storybook/react' -import { ClaimWidget } from '@goodwidget/claim-widget' +import { ClaimWidget } from '@goodwidget/claim-widget-theme-demo' import { Card, Heading, Text, Alert, YStack } from '@goodwidget/ui' -import { createMockEip1193Provider } from '../fixtures/mockEip1193' +import { createCustodialEip1193Provider } from '../fixtures/custodialEip1193' -const mockProvider = createMockEip1193Provider() +const mockProvider = createCustodialEip1193Provider() const meta: Meta = { title: 'Theme/ThemePlayground', diff --git a/packages/savings-widget/README.md b/packages/savings-widget/README.md new file mode 100644 index 0000000..c0ed036 --- /dev/null +++ b/packages/savings-widget/README.md @@ -0,0 +1,21 @@ +# @goodwidget/savings-widget + +A GoodDollar savings widget built for GoodWidget that uses `@goodsdks/savings-sdk` for deposit, withdraw, and reward-claim flows. + +## Usage + +```tsx +import { SavingsWidget } from '@goodwidget/savings-widget' + +export function App({ provider }) { + return +} +``` + +## Wallet onboarding + +If wallet connection is handled outside the widget, pass `connectWallet`: + +```tsx + appKit.open()} /> +``` diff --git a/packages/savings-widget/package.json b/packages/savings-widget/package.json new file mode 100644 index 0000000..8ec0588 --- /dev/null +++ b/packages/savings-widget/package.json @@ -0,0 +1,40 @@ +{ + "name": "@goodwidget/savings-widget", + "version": "0.1.0", + "description": "GoodDollar savings widget powered by @goodsdks/savings-sdk", + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "lint": "eslint src/", + "clean": "rm -rf dist .turbo" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + }, + "dependencies": { + "@goodsdks/savings-sdk": "^1.0.0", + "@goodwidget/core": "workspace:*", + "@goodwidget/ui": "workspace:*", + "viem": "^2.31.0" + }, + "devDependencies": { + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "react": "^18.3.0", + "react-dom": "^18.3.0", + "tsup": "^8.4.0", + "typescript": "^5.7.0" + } +} diff --git a/packages/savings-widget/src/SavingsWidget.tsx b/packages/savings-widget/src/SavingsWidget.tsx new file mode 100644 index 0000000..9abe7b0 --- /dev/null +++ b/packages/savings-widget/src/SavingsWidget.tsx @@ -0,0 +1,385 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react' +import { GoodWidgetProvider, useWallet } from '@goodwidget/core' +import type { + EIP1193Provider, + GoodWidgetConfig, + GoodWidgetThemeOverrides, +} from '@goodwidget/core' +import { + Badge, + BadgeText, + Button, + ButtonText, + Card, + Heading, + Spinner, + Text, + TokenInput, + ToastContainer, + XStack, + YStack, + createToast, + updateToast, +} from '@goodwidget/ui' +import { GooddollarSavingsSDK } from '@goodsdks/savings-sdk' +import { createPublicClient, createWalletClient, custom, formatEther, http, parseEther } from 'viem' +import { celo } from 'viem/chains' + +export type SavingsTab = 'deposit' | 'withdraw' + +interface SavingsWidgetInnerProps { + connectWallet?: () => void + refreshIntervalMs: number +} + +interface SavingsGlobalStats { + totalStaked: bigint + annualAPR: number +} + +interface SavingsUserStats { + walletBalance: bigint + currentStake: bigint + unclaimedRewards: bigint + userWeeklyRewards: bigint +} + +const DEFAULT_GLOBAL_STATS: SavingsGlobalStats = { + totalStaked: 0n, + annualAPR: 0, +} + +const DEFAULT_USER_STATS: SavingsUserStats = { + walletBalance: 0n, + currentStake: 0n, + unclaimedRewards: 0n, + userWeeklyRewards: 0n, +} + +type SavingsSdkPublicClient = ConstructorParameters[0] +type SavingsSdkWalletClient = ConstructorParameters[1] + +function formatTokenAmount(value: bigint): string { + return Number(formatEther(value)).toLocaleString(undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: 2, + }) +} + +function formatPercent(value: number): string { + return `${value.toFixed(2)}%` +} + +function parseAmount(value: string): bigint | null { + if (!value.trim()) return null + try { + return parseEther(value) + } catch { + return null + } +} + +function SavingsWidgetInner({ connectWallet, refreshIntervalMs }: SavingsWidgetInnerProps) { + const { address, connect, isConnected, provider } = useWallet() + const [activeTab, setActiveTab] = useState('deposit') + const [inputAmount, setInputAmount] = useState('') + const [globalStats, setGlobalStats] = useState(DEFAULT_GLOBAL_STATS) + const [userStats, setUserStats] = useState(DEFAULT_USER_STATS) + const [loading, setLoading] = useState(true) + const [txPending, setTxPending] = useState(false) + const [claimPending, setClaimPending] = useState(false) + const [sdkError, setSdkError] = useState(null) + + const connected = Boolean(isConnected && provider && address) + + const publicClient = useMemo( + () => + createPublicClient({ + chain: celo, + transport: http(), + }), + [], + ) + + const walletClient = useMemo(() => { + if (!provider || !connected) return null + return createWalletClient({ + chain: celo, + transport: custom(provider), + }) + }, [provider, connected]) + + const [sdk, setSdk] = useState(null) + + useEffect(() => { + try { + const sdkPublicClient = publicClient as SavingsSdkPublicClient + const sdkWalletClient = walletClient as SavingsSdkWalletClient + const nextSdk = walletClient + ? new GooddollarSavingsSDK(sdkPublicClient, sdkWalletClient) + : new GooddollarSavingsSDK(sdkPublicClient) + setSdk(nextSdk) + setSdkError(null) + } catch (error) { + setSdk(null) + setSdkError(error instanceof Error ? error.message : 'Failed to initialize savings SDK') + } + }, [publicClient, walletClient]) + + const refreshData = useCallback(async () => { + if (!sdk) return + + setLoading(true) + try { + const stats = await sdk.getGlobalStats() + setGlobalStats(stats) + + if (connected) { + const userStatsResult = await sdk.getUserStats() + setUserStats(userStatsResult) + } else { + setUserStats(DEFAULT_USER_STATS) + } + + setSdkError(null) + } catch (error) { + setSdkError(error instanceof Error ? error.message : 'Failed to fetch savings data') + } finally { + setLoading(false) + } + }, [sdk, connected]) + + useEffect(() => { + void refreshData() + }, [refreshData]) + + useEffect(() => { + if (refreshIntervalMs <= 0) return + const interval = setInterval(() => { + void refreshData() + }, refreshIntervalMs) + + return () => { + clearInterval(interval) + } + }, [refreshData, refreshIntervalMs]) + + const selectedBalance = activeTab === 'deposit' ? userStats.walletBalance : userStats.currentStake + const parsedAmount = parseAmount(inputAmount) + + const inputError = useMemo(() => { + if (!inputAmount.trim()) return null + if (!parsedAmount) return 'Invalid amount' + if (parsedAmount <= 0n) return 'Amount must be greater than zero' + if (parsedAmount > selectedBalance) { + return activeTab === 'deposit' ? 'Insufficient wallet balance' : 'Amount exceeds staked balance' + } + return null + }, [activeTab, inputAmount, parsedAmount, selectedBalance]) + + const handleConnect = useCallback(async () => { + if (connectWallet) { + connectWallet() + return + } + await connect() + }, [connect, connectWallet]) + + const handleSetMax = useCallback(() => { + setInputAmount(formatEther(selectedBalance)) + }, [selectedBalance]) + + const handleDepositWithdraw = useCallback(async () => { + if (!sdk || !parsedAmount || inputError) return + + const actionLabel = activeTab === 'deposit' ? 'deposit' : 'withdrawal' + const toastId = createToast({ message: `Submitting ${actionLabel}...`, status: 'pending', duration: 0 }) + + try { + setTxPending(true) + if (activeTab === 'deposit') { + await sdk.stake(parsedAmount) + } else { + await sdk.unstake(parsedAmount) + } + updateToast(toastId, { + message: `${activeTab === 'deposit' ? 'Deposit' : 'Withdrawal'} completed`, + status: 'success', + duration: 4000, + }) + setInputAmount('') + await refreshData() + } catch (error) { + updateToast(toastId, { + message: error instanceof Error ? error.message : `Failed to process ${actionLabel}`, + status: 'error', + duration: 5000, + }) + } finally { + setTxPending(false) + } + }, [activeTab, inputError, parsedAmount, refreshData, sdk]) + + const handleClaimRewards = useCallback(async () => { + if (!sdk || userStats.unclaimedRewards <= 0n) return + + const toastId = createToast({ message: 'Claiming rewards...', status: 'pending', duration: 0 }) + + try { + setClaimPending(true) + await sdk.claimReward() + updateToast(toastId, { + message: 'Rewards claimed', + status: 'success', + duration: 4000, + }) + await refreshData() + } catch (error) { + updateToast(toastId, { + message: error instanceof Error ? error.message : 'Failed to claim rewards', + status: 'error', + duration: 5000, + }) + } finally { + setClaimPending(false) + } + }, [refreshData, sdk, userStats.unclaimedRewards]) + + return ( + + + + GoodDollar Savings + + Celo + + + + + + + + + {loading ? ( + + + + ) : !connected ? ( + + Connect your wallet to deposit and withdraw G$ from savings. + + + ) : ( + + + + {activeTab === 'deposit' ? 'Wallet Balance' : 'Staked Balance'}: {formatTokenAmount(selectedBalance)} G$ + + + + + + {inputError && {inputError}} + + + + + Unclaimed rewards + + + {formatTokenAmount(userStats.unclaimedRewards)} G$ + + )} + + {sdkError && {sdkError}} + + + + Savings statistics + + Total G$ staked + {formatTokenAmount(globalStats.totalStaked)} G$ + + + Annual APR + {formatPercent(globalStats.annualAPR)} + + + Your current stake + {formatTokenAmount(userStats.currentStake)} G$ + + + Your weekly rewards + {formatTokenAmount(userStats.userWeeklyRewards)} G$ + + + + + + ) +} + +export interface SavingsWidgetProps { + provider?: EIP1193Provider + connectWallet?: () => void + config?: GoodWidgetConfig + themeOverrides?: GoodWidgetThemeOverrides + defaultTheme?: 'light' | 'dark' + refreshIntervalMs?: number +} + +export function SavingsWidget({ + provider, + connectWallet, + themeOverrides, + config, + defaultTheme = 'light', + refreshIntervalMs = 30_000, +}: SavingsWidgetProps) { + return ( + + + + ) +} diff --git a/packages/savings-widget/src/index.ts b/packages/savings-widget/src/index.ts new file mode 100644 index 0000000..5f2913c --- /dev/null +++ b/packages/savings-widget/src/index.ts @@ -0,0 +1,2 @@ +export { SavingsWidget } from './SavingsWidget' +export type { SavingsWidgetProps, SavingsTab } from './SavingsWidget' diff --git a/packages/savings-widget/tsconfig.build.json b/packages/savings-widget/tsconfig.build.json new file mode 100644 index 0000000..54871c4 --- /dev/null +++ b/packages/savings-widget/tsconfig.build.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "paths": { + "react-native": ["./node_modules/react-native-web"] + } + }, + "include": ["src"] +} diff --git a/packages/savings-widget/tsconfig.json b/packages/savings-widget/tsconfig.json new file mode 100644 index 0000000..006b6f4 --- /dev/null +++ b/packages/savings-widget/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "paths": { + "@goodwidget/core": ["../core/src/index.ts"], + "@goodwidget/core/*": ["../core/src/*"], + "@goodwidget/ui": ["../ui/src/index.ts"], + "react-native": ["./node_modules/react-native-web"] + } + }, + "include": ["src"] +} diff --git a/packages/savings-widget/tsup.config.ts b/packages/savings-widget/tsup.config.ts new file mode 100644 index 0000000..511ca60 --- /dev/null +++ b/packages/savings-widget/tsup.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: { + index: 'src/index.ts', + }, + format: ['esm', 'cjs'], + dts: true, + sourcemap: true, + clean: true, + tsconfig: 'tsconfig.build.json', + external: ['react', 'react-dom', 'react-native', 'react-native-web'], +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2aa2dac..284a996 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -167,6 +167,9 @@ importers: '@goodwidget/core': specifier: workspace:* version: link:../../packages/core + '@goodwidget/savings-widget': + specifier: workspace:* + version: link:../../packages/savings-widget '@goodwidget/ui': specifier: workspace:* version: link:../../packages/ui @@ -179,6 +182,9 @@ importers: react-native-web: specifier: ^0.19.13 version: 0.19.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + viem: + specifier: ^2.31.0 + version: 2.48.8(typescript@5.9.3) devDependencies: '@storybook/addon-essentials': specifier: ^8.6.17 @@ -329,6 +335,40 @@ importers: specifier: ^5.7.0 version: 5.9.3 + packages/savings-widget: + dependencies: + '@goodsdks/savings-sdk': + specifier: ^1.0.0 + version: 1.0.0(@swc/core@1.15.30)(postcss@8.5.8)(typescript@5.9.3)(viem@2.48.8(typescript@5.9.3))(wagmi@3.6.9(@tanstack/query-core@5.100.9)(@tanstack/react-query@5.100.9(react@18.3.1))(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(viem@2.48.8(typescript@5.9.3)))(yaml@2.8.3) + '@goodwidget/core': + specifier: workspace:* + version: link:../core + '@goodwidget/ui': + specifier: workspace:* + version: link:../ui + viem: + specifier: ^2.31.0 + version: 2.48.8(typescript@5.9.3) + devDependencies: + '@types/react': + specifier: ^18.3.0 + version: 18.3.28 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.7(@types/react@18.3.28) + react: + specifier: ^18.3.0 + version: 18.3.1 + react-dom: + specifier: ^18.3.0 + version: 18.3.1(react@18.3.1) + tsup: + specifier: ^8.4.0 + version: 8.5.1(@swc/core@1.15.30)(postcss@8.5.8)(typescript@5.9.3)(yaml@2.8.3) + typescript: + specifier: ^5.7.0 + version: 5.9.3 + packages/ui: dependencies: '@tamagui/animations-react-native': @@ -376,6 +416,9 @@ packages: '@adobe/css-tools@4.4.4': resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} + '@adraffy/ens-normalize@1.11.1': + resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==} + '@babel/code-frame@7.10.4': resolution: {integrity: sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==} @@ -1577,6 +1620,12 @@ packages: '@floating-ui/utils@0.2.11': resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@goodsdks/savings-sdk@1.0.0': + resolution: {integrity: sha512-SoVkmT0bBjwA+COoP7b39CXqKLY4ds/Mbn6GrZlrmSSXffUgAt+lVZnxQNwFvfpxTxtavyoZXVisQep1YmQ0BA==} + peerDependencies: + viem: '*' + wagmi: '*' + '@hapi/hoek@9.3.0': resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} @@ -1753,6 +1802,18 @@ packages: '@motionone/utils@10.18.0': resolution: {integrity: sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw==} + '@noble/ciphers@1.3.0': + resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.9.1': + resolution: {integrity: sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2083,6 +2144,15 @@ packages: cpu: [x64] os: [win32] + '@scure/base@1.2.6': + resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} + + '@scure/bip32@1.7.0': + resolution: {integrity: sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==} + + '@scure/bip39@1.6.0': + resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==} + '@segment/loosely-validate-event@2.0.0': resolution: {integrity: sha512-ZMCSfztDBqwotkl848ODgVcAmN4OItEWDCkshcKz0/W6gGSQayuuCtWV/MlodFivAZD793d6UgANd6wCXUfrIw==} @@ -2989,6 +3059,14 @@ packages: react-dom: '*' react-native: '*' + '@tanstack/query-core@5.100.9': + resolution: {integrity: sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ==} + + '@tanstack/react-query@5.100.9': + resolution: {integrity: sha512-Oa44XkaI3kCNN6ME0KByU3xT3SEUNOMfZpHxL6+wFoTm+OeUFYHKdeYVe0aOXlRDm/f15sgLwEt2HDorIdW8+A==} + peerDependencies: + react: ^18 || ^19 + '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} engines: {node: '>=18'} @@ -3204,6 +3282,55 @@ packages: '@vitest/utils@2.1.9': resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + '@wagmi/connectors@8.0.9': + resolution: {integrity: sha512-/wLCFLeQbZRRLeYKxfp1s+Ukcm3PW/cy0HIqS4vbGsKRAH/NAGFSGqsIj7g7Xz11hI5dzQ6N2/o2fuUd8uQZSw==} + peerDependencies: + '@base-org/account': ^2.5.1 + '@coinbase/wallet-sdk': ^4.3.6 + '@metamask/connect-evm': ~1.0.0 + '@safe-global/safe-apps-provider': ~0.18.6 + '@safe-global/safe-apps-sdk': ^9.1.0 + '@wagmi/core': 3.4.8 + '@walletconnect/ethereum-provider': ^2.21.1 + accounts: ~0.8.1 + porto: ~0.2.35 + typescript: '>=5.7.3' + viem: 2.x + peerDependenciesMeta: + '@base-org/account': + optional: true + '@coinbase/wallet-sdk': + optional: true + '@metamask/connect-evm': + optional: true + '@safe-global/safe-apps-provider': + optional: true + '@safe-global/safe-apps-sdk': + optional: true + '@walletconnect/ethereum-provider': + optional: true + accounts: + optional: true + porto: + optional: true + typescript: + optional: true + + '@wagmi/core@3.4.8': + resolution: {integrity: sha512-G/t3WGCUYY/T86MBzr9mAsyAjuZP8UfiFbdDL+/klUs6oBqLavSxhygvjMnOpTDKOrPqWWGh00wubwBx4rxZEg==} + peerDependencies: + '@tanstack/query-core': '>=5.0.0' + accounts: ~0.8.1 + typescript: '>=5.7.3' + viem: 2.x + peerDependenciesMeta: + '@tanstack/query-core': + optional: true + accounts: + optional: true + typescript: + optional: true + '@xmldom/xmldom@0.7.13': resolution: {integrity: sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==} engines: {node: '>=10.0.0'} @@ -3213,6 +3340,17 @@ packages: resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} engines: {node: '>=10.0.0'} + abitype@1.2.3: + resolution: {integrity: sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3.22.0 || ^4.0.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -4150,6 +4288,9 @@ packages: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + execa@1.0.0: resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==} engines: {node: '>=6'} @@ -4816,6 +4957,11 @@ packages: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} + isows@1.0.7: + resolution: {integrity: sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==} + peerDependencies: + ws: '*' + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -5497,6 +5643,14 @@ packages: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} engines: {node: '>= 8'} + mipd@0.0.7: + resolution: {integrity: sha512-aAPZPNDQ3uMTdKbuO2YmAw2TxLHO0moa4YKAyETM/DTj5FloZo+a+8tU+iv4GmW+sOxKLSRwcSFuczk+Cpt6fg==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true @@ -5658,6 +5812,14 @@ packages: resolution: {integrity: sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==} engines: {node: '>=0.10.0'} + ox@0.14.20: + resolution: {integrity: sha512-rby38C3nDn8eQkf29Zgw4hkCZJ64Qqi0zRPWL8ENUQ7JVuoITqrVtwWQgM/He19SCMUEc7hS/Sjw0jIOSLJhOw==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + p-finally@1.0.0: resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} engines: {node: '>=4'} @@ -6818,6 +6980,11 @@ packages: '@types/react': optional: true + use-sync-external-store@1.4.0: + resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + use-sync-external-store@1.6.0: resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: @@ -6857,6 +7024,14 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + viem@2.48.8: + resolution: {integrity: sha512-Xj3Nrt66SKtn06kczU91ELn9Difr84ZM5A62BTlaisT5lpgt058i2mBkfMZCXHGb1ocOLjzC2ztPhD0Lvky7uQ==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + vite@6.4.1: resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -6906,6 +7081,17 @@ packages: w-json@1.3.11: resolution: {integrity: sha512-Xa8vTinB5XBIYZlcN8YyHpE625pBU6k+lvCetTQM+FKxRtLJxAY9zUVZbRqCqkMeEGbQpKvGUzwh4wZKGem+ag==} + wagmi@3.6.9: + resolution: {integrity: sha512-9Lrkf7bXyhG/aSK/65V2t+44Kti2m9tqaTS2vQTCeUgfaYlmFfx1RDUm4f8me5zcYclAo1XbJjm5x99dw7xAiA==} + peerDependencies: + '@tanstack/react-query': '>=5.0.0' + react: '>=18' + typescript: '>=5.7.3' + viem: 2.x + peerDependenciesMeta: + typescript: + optional: true + wait-on@7.2.0: resolution: {integrity: sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==} engines: {node: '>=12.0.0'} @@ -7020,6 +7206,18 @@ packages: utf-8-validate: optional: true + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@8.20.0: resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} engines: {node: '>=10.0.0'} @@ -7093,12 +7291,32 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zustand@5.0.0: + resolution: {integrity: sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: '@0no-co/graphql.web@1.2.0': {} '@adobe/css-tools@4.4.4': {} + '@adraffy/ens-normalize@1.11.1': {} + '@babel/code-frame@7.10.4': dependencies: '@babel/highlight': 7.25.9 @@ -8547,6 +8765,21 @@ snapshots: '@floating-ui/utils@0.2.11': {} + '@goodsdks/savings-sdk@1.0.0(@swc/core@1.15.30)(postcss@8.5.8)(typescript@5.9.3)(viem@2.48.8(typescript@5.9.3))(wagmi@3.6.9(@tanstack/query-core@5.100.9)(@tanstack/react-query@5.100.9(react@18.3.1))(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(viem@2.48.8(typescript@5.9.3)))(yaml@2.8.3)': + dependencies: + tsup: 8.5.1(@swc/core@1.15.30)(postcss@8.5.8)(typescript@5.9.3)(yaml@2.8.3) + viem: 2.48.8(typescript@5.9.3) + wagmi: 3.6.9(@tanstack/query-core@5.100.9)(@tanstack/react-query@5.100.9(react@18.3.1))(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(viem@2.48.8(typescript@5.9.3)) + transitivePeerDependencies: + - '@microsoft/api-extractor' + - '@swc/core' + - jiti + - postcss + - supports-color + - tsx + - typescript + - yaml + '@hapi/hoek@9.3.0': {} '@hapi/topo@5.1.0': @@ -8848,6 +9081,14 @@ snapshots: hey-listen: 1.0.8 tslib: 2.8.1 + '@noble/ciphers@1.3.0': {} + + '@noble/curves@1.9.1': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/hashes@1.8.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -9264,6 +9505,19 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.0': optional: true + '@scure/base@1.2.6': {} + + '@scure/bip32@1.7.0': + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + + '@scure/bip39@1.6.0': + dependencies: + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + '@segment/loosely-validate-event@2.0.0': dependencies: component-type: 1.2.2 @@ -10987,6 +11241,13 @@ snapshots: - sf-symbols-typescript - zeego + '@tanstack/query-core@5.100.9': {} + + '@tanstack/react-query@5.100.9(react@18.3.1)': + dependencies: + '@tanstack/query-core': 5.100.9 + react: 18.3.1 + '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.29.0 @@ -11261,10 +11522,36 @@ snapshots: loupe: 3.2.1 tinyrainbow: 1.2.0 + '@wagmi/connectors@8.0.9(@wagmi/core@3.4.8(@tanstack/query-core@5.100.9)(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.48.8(typescript@5.9.3)))(typescript@5.9.3)(viem@2.48.8(typescript@5.9.3))': + dependencies: + '@wagmi/core': 3.4.8(@tanstack/query-core@5.100.9)(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.48.8(typescript@5.9.3)) + viem: 2.48.8(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + + '@wagmi/core@3.4.8(@tanstack/query-core@5.100.9)(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.48.8(typescript@5.9.3))': + dependencies: + eventemitter3: 5.0.1 + mipd: 0.0.7(typescript@5.9.3) + viem: 2.48.8(typescript@5.9.3) + zustand: 5.0.0(@types/react@18.3.28)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)) + optionalDependencies: + '@tanstack/query-core': 5.100.9 + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/react' + - immer + - react + - use-sync-external-store + '@xmldom/xmldom@0.7.13': {} '@xmldom/xmldom@0.8.11': {} + abitype@1.2.3(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -12282,6 +12569,8 @@ snapshots: event-target-shim@5.0.1: {} + eventemitter3@5.0.1: {} + execa@1.0.0: dependencies: cross-spawn: 6.0.6 @@ -13016,6 +13305,10 @@ snapshots: isobject@3.0.1: {} + isows@1.0.7(ws@8.18.3): + dependencies: + ws: 8.18.3 + istanbul-lib-coverage@3.2.2: {} istanbul-lib-hook@3.0.0: @@ -14116,6 +14409,10 @@ snapshots: minipass: 3.3.6 yallist: 4.0.0 + mipd@0.0.7(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + mkdirp@0.5.6: dependencies: minimist: 1.2.8 @@ -14295,6 +14592,21 @@ snapshots: os-homedir@1.0.2: {} + ox@0.14.20(typescript@5.9.3): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.2.3(typescript@5.9.3) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - zod + p-finally@1.0.0: {} p-limit@2.3.0: @@ -15553,6 +15865,10 @@ snapshots: optionalDependencies: '@types/react': 18.3.28 + use-sync-external-store@1.4.0(react@18.3.1): + dependencies: + react: 18.3.1 + use-sync-external-store@1.6.0(react@18.3.1): dependencies: react: 18.3.1 @@ -15585,6 +15901,23 @@ snapshots: vary@1.1.2: {} + viem@2.48.8(typescript@5.9.3): + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.2.3(typescript@5.9.3) + isows: 1.0.7(ws@8.18.3) + ox: 0.14.20(typescript@5.9.3) + ws: 8.18.3 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + vite@6.4.1(@types/node@25.5.0)(lightningcss@1.27.0)(terser@5.46.1)(yaml@2.8.3): dependencies: esbuild: 0.25.12 @@ -15606,6 +15939,29 @@ snapshots: w-json@1.3.11: {} + wagmi@3.6.9(@tanstack/query-core@5.100.9)(@tanstack/react-query@5.100.9(react@18.3.1))(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(viem@2.48.8(typescript@5.9.3)): + dependencies: + '@tanstack/react-query': 5.100.9(react@18.3.1) + '@wagmi/connectors': 8.0.9(@wagmi/core@3.4.8(@tanstack/query-core@5.100.9)(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.48.8(typescript@5.9.3)))(typescript@5.9.3)(viem@2.48.8(typescript@5.9.3)) + '@wagmi/core': 3.4.8(@tanstack/query-core@5.100.9)(@types/react@18.3.28)(react@18.3.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.48.8(typescript@5.9.3)) + react: 18.3.1 + use-sync-external-store: 1.4.0(react@18.3.1) + viem: 2.48.8(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@base-org/account' + - '@coinbase/wallet-sdk' + - '@metamask/connect-evm' + - '@safe-global/safe-apps-provider' + - '@safe-global/safe-apps-sdk' + - '@tanstack/query-core' + - '@types/react' + - '@walletconnect/ethereum-provider' + - accounts + - immer + - porto + wait-on@7.2.0: dependencies: axios: 1.15.2 @@ -15723,6 +16079,8 @@ snapshots: ws@7.5.10: {} + ws@8.18.3: {} + ws@8.20.0: {} xcode@3.0.1: @@ -15785,3 +16143,9 @@ snapshots: yargs-parser: 21.1.1 yocto-queue@0.1.0: {} + + zustand@5.0.0(@types/react@18.3.28)(react@18.3.1)(use-sync-external-store@1.4.0(react@18.3.1)): + optionalDependencies: + '@types/react': 18.3.28 + react: 18.3.1 + use-sync-external-store: 1.4.0(react@18.3.1) diff --git a/tests/demo/smoke.spec.ts b/tests/demo/smoke.spec.ts index 741b22b..85babf8 100644 --- a/tests/demo/smoke.spec.ts +++ b/tests/demo/smoke.spec.ts @@ -85,6 +85,13 @@ test('ClaimWidget/TealBrand story renders', async ({ page }) => { await page.screenshot({ path: 'test-results/story-claimwidget-teal.png', fullPage: true }) }) +test('SavingsWidget/Default story renders', async ({ page }) => { + await gotoStory(page, 'widgets-savingswidget--default') + const frame = getStoryFrame(page) + await expect(frame.getByTestId('SavingsWidget-default')).toBeVisible() + await page.screenshot({ path: 'test-results/story-savingswidget-default.png', fullPage: true }) +}) + test('ThemePlayground/DefaultPreset story renders', async ({ page }) => { await gotoStory(page, 'theme-themeplayground--default-preset') const frame = getStoryFrame(page)