Skip to content

Commit 2c2f7fa

Browse files
committed
feat: enhance vault management by adding new hooks for staking, multiplier points, and vault locking; update UI components and improve state management for better user experience
1 parent 7b28420 commit 2c2f7fa

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+2933
-1824
lines changed

apps/hub/src/app/_components/stake/action-status-dialog.tsx

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,17 @@ import { CloseIcon } from '@status-im/icons/20'
44
import { match } from 'ts-pattern'
55

66
import { ProcessingIcon, RejectIcon, VaultIcon } from '../icons'
7-
8-
import type { ActionStatusState } from './use-action-status-content'
7+
import { type ActionStatusState } from './types/action-status'
98

109
type Props = {
1110
open: boolean
1211
onClose: () => void
13-
title: string
14-
description: string
12+
title?: string
13+
description?: string
1514
children?: React.ReactNode
1615
state?: ActionStatusState
1716
showCloseButton?: boolean
17+
content?: React.ReactNode
1818
}
1919

2020
const ActionStatusDialog = (props: Props) => {
@@ -26,6 +26,7 @@ const ActionStatusDialog = (props: Props) => {
2626
children,
2727
state = 'pending',
2828
showCloseButton = true,
29+
content,
2930
} = props
3031

3132
const handleOpenChange = (nextOpen: boolean) => {
@@ -62,17 +63,25 @@ const ActionStatusDialog = (props: Props) => {
6263
</button>
6364
</Dialog.Close>
6465
)}
65-
<div className="relative z-10 flex min-h-[198px] flex-col items-center justify-center gap-2 p-8">
66-
{mapIconToState(state)}
67-
<Dialog.Title asChild>
68-
<h2 className="text-center text-19 font-semibold text-neutral-100">
69-
{title}
70-
</h2>
71-
</Dialog.Title>
72-
<Dialog.Description asChild>
73-
<span className="text-15 text-neutral-100">{description}</span>
74-
</Dialog.Description>
75-
</div>
66+
{content ? (
67+
<div className="relative z-10">{content}</div>
68+
) : (
69+
<div className="relative z-10 flex min-h-[198px] flex-col items-center justify-center gap-2 p-8">
70+
{mapIconToState(state)}
71+
<Dialog.Title asChild>
72+
<h2 className="text-center text-19 font-semibold text-neutral-100">
73+
{title}
74+
</h2>
75+
</Dialog.Title>
76+
{description && (
77+
<Dialog.Description asChild>
78+
<span className="text-15 text-neutral-100">
79+
{description}
80+
</span>
81+
</Dialog.Description>
82+
)}
83+
</div>
84+
)}
7685
</div>
7786
</Dialog.Content>
7887
</Dialog.Portal>
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/* eslint-disable import/no-unresolved */
2+
import * as Dialog from '@radix-ui/react-dialog'
3+
import { InfoIcon } from '@status-im/icons/16'
4+
import { formatUnits } from 'viem'
5+
6+
import { LaunchIcon } from '~components/icons'
7+
import { useMultiplierPointsBalance } from '~hooks/useMultiplierPoints'
8+
import { useStakingVaults } from '~hooks/useStakingVaults'
9+
import { useWeightedBoost } from '~hooks/useWeightedBoost'
10+
import { formatSNT } from '~utils/currency'
11+
12+
import { SNT_TOKEN } from '../../_constants'
13+
14+
export const CompoundStatusContent = () => {
15+
const { data: vaults } = useStakingVaults()
16+
const weightedBoost = useWeightedBoost(vaults)
17+
const { data: multiplierPointsData } = useMultiplierPointsBalance()
18+
19+
const earnRateWithBoost = Math.floor(weightedBoost.totalStaked * 0.05)
20+
const earnRateWithoutBoost = Math.floor(weightedBoost.totalStaked * 0.05)
21+
22+
return (
23+
<>
24+
<div className="flex items-center justify-center px-4 py-8">
25+
<LaunchIcon />
26+
</div>
27+
28+
<div className="flex flex-col gap-8 px-8 pb-8">
29+
<div className="flex flex-col gap-1">
30+
<Dialog.Title asChild>
31+
<h2 className="text-center text-[19px] font-semibold leading-[1.35] tracking-[-0.019rem] text-neutral-100">
32+
{`Ready to compound ${formatSNT(
33+
formatUnits(
34+
multiplierPointsData?.totalUncompounded ?? 0n,
35+
SNT_TOKEN.decimals
36+
)
37+
)} points`}
38+
</h2>
39+
</Dialog.Title>
40+
<Dialog.Description asChild>
41+
<p className="text-center text-[15px] font-regular leading-[1.45] tracking-[-0.0084375rem] text-neutral-100">
42+
Please sign the message in your wallet.
43+
</p>
44+
</Dialog.Description>
45+
</div>
46+
47+
<div className="flex flex-col gap-[27px] rounded-16 bg-neutral-5 py-4">
48+
<div className="flex flex-col items-center gap-2 text-center">
49+
<p className="text-[13px] font-medium leading-[1.4] tracking-[-0.024375rem] text-neutral-50">
50+
Total compounded
51+
</p>
52+
<p className="text-[27px] font-semibold leading-8 tracking-[-0.0354375rem] text-neutral-100">
53+
{`${formatSNT(
54+
formatUnits(
55+
multiplierPointsData?.totalMpRedeemed ?? 0n,
56+
SNT_TOKEN.decimals
57+
)
58+
)} points`}
59+
</p>
60+
</div>
61+
62+
<div className="flex flex-col items-center gap-2 text-center">
63+
<p className="text-[13px] font-medium leading-[1.4] tracking-[-0.024375rem] text-neutral-50">
64+
Your earn rate at {weightedBoost.formatted} boost
65+
</p>
66+
<p className="text-[27px] font-semibold leading-8 tracking-[-0.0354375rem] text-neutral-100">
67+
{earnRateWithBoost} Karma / day
68+
</p>
69+
</div>
70+
71+
<div className="flex flex-col items-center gap-2 text-center">
72+
<p className="text-[13px] font-medium leading-[1.4] tracking-[-0.024375rem] text-neutral-50">
73+
Equivalent at x0.00 boost
74+
</p>
75+
<p className="text-[27px] font-semibold leading-8 tracking-[-0.0354375rem] text-neutral-100">
76+
{earnRateWithoutBoost} Karma / day
77+
</p>
78+
</div>
79+
</div>
80+
81+
<div className="flex gap-2 rounded-12 border border-customisation-blue-50/10 bg-customisation-blue-50/5 px-4 py-[11px]">
82+
<div className="flex shrink-0 items-start justify-center py-px">
83+
<InfoIcon />
84+
</div>
85+
<p className="flex-1 text-[13px] font-regular leading-[1.4] tracking-[-0.024375rem] text-neutral-100">
86+
Boost the rate at which you receive Karma. More points you compound,
87+
the higher your rate. The longer you lock your vault, the higher
88+
your boost, and the faster you accumulate Karma.
89+
</p>
90+
</div>
91+
</div>
92+
</>
93+
)
94+
}

apps/hub/src/app/_components/stake/use-action-status-content.tsx renamed to apps/hub/src/app/_components/stake/hooks/use-action-status-content.tsx

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,18 @@
11
import { match } from 'ts-pattern'
22

3-
import { formatSNT } from '../../../utils/currency'
3+
import { type VaultState } from '~hooks/useVaultStateMachine'
4+
import { formatSNT } from '~utils/currency'
45

5-
import type { VaultState } from '../../_hooks/useVaultStateMachine'
6-
7-
export type ActionStatusState =
8-
| 'pending' // Waiting for user action (sign, approve)
9-
| 'processing' // Transaction in progress
10-
| 'error' // Failed/rejected
11-
| 'success' // Completed successfully
12-
13-
export type ActionStatusContent = {
14-
title: string
15-
description: string
16-
state: ActionStatusState
17-
showCloseButton: boolean
18-
}
6+
import { CompoundStatusContent } from '../compound-status-content'
7+
import { type ActionStatusContent } from '../types/action-status'
198

9+
/**
10+
* Hook to generate action status dialog content based on vault state
11+
* Maps vault state machine states to user-facing dialog content
12+
*/
2013
export function useActionStatusContent(
2114
state: VaultState
2215
): ActionStatusContent | null {
23-
console.log(state)
2416
return (
2517
match<VaultState, ActionStatusContent | null>(state)
2618
// SIWE flow
@@ -110,19 +102,7 @@ export function useActionStatusContent(
110102
showCloseButton: true,
111103
}))
112104

113-
// Withdraw flow
114-
.with(
115-
{
116-
type: 'withdraw',
117-
step: 'initialize',
118-
},
119-
state => ({
120-
title: `Ready to withdraw ${formatSNT(state.amount ?? 0, { includeSymbol: true })}`,
121-
description: 'Please sign the message in your wallet.',
122-
state: 'pending',
123-
showCloseButton: true,
124-
})
125-
)
105+
// Withdraw flow (goes directly to processing, no initialize step)
126106
.with({ type: 'withdraw', step: 'processing' }, state => ({
127107
title: `Withdrawing ${formatSNT(state.amount ?? 0, { includeSymbol: true })}`,
128108
description: 'Wait a moment...',
@@ -156,6 +136,25 @@ export function useActionStatusContent(
156136
showCloseButton: true,
157137
}))
158138

139+
// compound flow
140+
.with({ type: 'compound', step: 'initialize' }, state => ({
141+
state: 'pending',
142+
showCloseButton: true,
143+
content: <CompoundStatusContent amount={state.amount} />,
144+
}))
145+
.with({ type: 'compound', step: 'processing' }, state => ({
146+
title: `Compounding ${formatSNT(state.amount ?? 0)} points`,
147+
description: 'Wait a moment...',
148+
state: 'processing',
149+
showCloseButton: false,
150+
}))
151+
.with({ type: 'compound', step: 'rejected' }, () => ({
152+
title: 'Request was rejected',
153+
description: 'Request was rejected by user',
154+
state: 'error',
155+
showCloseButton: true,
156+
}))
157+
159158
// Success
160159
.with({ type: 'success' }, () => ({
161160
title: 'Success!',
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Represents the current state of an action status dialog
3+
*/
4+
export type ActionStatusState =
5+
| 'pending' // Waiting for user action (sign, approve)
6+
| 'processing' // Transaction in progress
7+
| 'error' // Failed/rejected
8+
| 'success' // Completed successfully
9+
10+
/**
11+
* Content configuration for action status dialog
12+
*/
13+
export interface ActionStatusContent {
14+
state: ActionStatusState
15+
title?: string
16+
description?: string
17+
showCloseButton?: boolean
18+
content?: React.ReactNode
19+
}

apps/hub/src/app/_components/vault-select.tsx

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import {
1111
UnlockedIcon,
1212
} from '@status-im/icons/20'
1313

14-
import { formatSNT } from '../../utils/currency'
14+
import { formatSNT } from '~utils/currency'
15+
import { isVaultLocked } from '~utils/vault'
1516

16-
import type { VaultWithAddress } from '../_hooks/useAccountVaults'
17+
import type { StakingVault } from '~hooks/useStakingVaults'
1718

1819
// ============================================================================
1920
// Types
@@ -23,7 +24,7 @@ interface VaultSelectProps {
2324
/**
2425
* List of available vaults to select from
2526
*/
26-
vaults: VaultWithAddress[]
27+
vaults: StakingVault[]
2728
/**
2829
* Currently selected vault address
2930
*/
@@ -51,7 +52,7 @@ interface VaultSelectProps {
5152
/**
5253
* Gets display label for a vault option
5354
*/
54-
function getVaultLabel(vault: VaultWithAddress, index: number): string {
55+
function getVaultLabel(vault: StakingVault, index: number): string {
5556
const stakedAmount = vault.data?.stakedBalance
5657
? formatSNT(vault.data.stakedBalance)
5758
: '0'
@@ -100,9 +101,7 @@ export function VaultSelect({
100101
const selectedVault = vaults.find(vault => vault.address === value)
101102
const selectedIndex = vaults.findIndex(vault => vault.address === value)
102103

103-
const now = Math.floor(Date.now() / 1000)
104-
const lockUntilTimestamp = Number(selectedVault?.data?.lockUntil)
105-
const isLocked = lockUntilTimestamp > now
104+
const isLocked = isVaultLocked(selectedVault?.data?.lockUntil)
106105

107106
const displayLabel =
108107
selectedVault && selectedIndex !== -1
@@ -155,15 +154,13 @@ export function VaultSelect({
155154
{hasVaults ? (
156155
<>
157156
{vaults.map((vault, index) => {
158-
const now = Math.floor(Date.now() / 1000)
159-
const lockUntilTimestamp = Number(vault.data?.lockUntil)
160-
const isVaultLocked = lockUntilTimestamp > now
157+
const isLocked = isVaultLocked(vault.data?.lockUntil)
161158

162159
return (
163160
<DropdownMenu.Item
164161
key={vault.address}
165162
icon={
166-
isVaultLocked ? (
163+
isLocked ? (
167164
<LockedIcon className="text-purple" />
168165
) : (
169166
<UnlockedIcon className="text-purple" />
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/* eslint-disable import/no-unresolved */
2+
'use client'
3+
4+
import * as Dialog from '@radix-ui/react-dialog'
5+
import { CloseIcon } from '@status-im/icons/20'
6+
7+
import type { ReactNode } from 'react'
8+
9+
interface BaseVaultModalProps {
10+
open?: boolean
11+
onOpenChange?: (open: boolean) => void
12+
onClose: () => void
13+
title: string
14+
description: string
15+
children?: ReactNode
16+
trigger?: ReactNode
17+
}
18+
19+
/**
20+
* Base modal component for vault-related actions.
21+
* Provides consistent dialog wrapper with close button, overlay, and styling.
22+
*/
23+
export function BaseVaultModal(props: BaseVaultModalProps) {
24+
const { open, onOpenChange, onClose, title, description, children, trigger } =
25+
props
26+
27+
const handleOpenChange = (nextOpen: boolean) => {
28+
if (onOpenChange) {
29+
onOpenChange(nextOpen)
30+
}
31+
if (!nextOpen) {
32+
onClose()
33+
}
34+
}
35+
36+
return (
37+
<Dialog.Root open={open} onOpenChange={handleOpenChange}>
38+
{trigger && <Dialog.Trigger asChild>{trigger}</Dialog.Trigger>}
39+
<Dialog.Portal>
40+
<Dialog.Overlay className="fixed inset-0 z-40 bg-neutral-80/60 backdrop-blur-sm" />
41+
<Dialog.Content className="fixed left-1/2 top-1/2 z-50 w-full max-w-[480px] -translate-x-1/2 -translate-y-1/2 px-4 focus:outline-none">
42+
<div className="relative mx-auto w-full max-w-[480px] overflow-hidden rounded-20 bg-white-100 shadow-3">
43+
<Dialog.Close asChild>
44+
<button
45+
aria-label="Close"
46+
className="absolute right-3 top-3 z-50 flex size-8 items-center justify-center rounded-10 border border-[rgba(27,39,61,0.1)] backdrop-blur-[20px] transition-colors hover:bg-neutral-10 focus:outline-none"
47+
>
48+
<CloseIcon className="text-neutral-100" />
49+
</button>
50+
</Dialog.Close>
51+
52+
<div className="box-border flex flex-col items-center px-4 pb-4 pt-8">
53+
<Dialog.Title asChild>
54+
<div className="flex w-full items-center gap-[6px]">
55+
<span className="min-h-px min-w-px shrink-0 grow basis-0 text-[19px] font-semibold leading-[1.35] tracking-[-0.304px] text-neutral-100">
56+
{title}
57+
</span>
58+
</div>
59+
</Dialog.Title>
60+
<Dialog.Description asChild>
61+
<div className="flex w-full flex-col justify-center text-[15px] leading-[0] tracking-[-0.135px] text-neutral-100">
62+
<span className="leading-[1.45]">{description}</span>
63+
</div>
64+
</Dialog.Description>
65+
</div>
66+
67+
{children}
68+
</div>
69+
</Dialog.Content>
70+
</Dialog.Portal>
71+
</Dialog.Root>
72+
)
73+
}

0 commit comments

Comments
 (0)