1
+ import { InformationCircleIcon } from "@heroicons/react/24/outline" ;
1
2
import Image from "next/image" ;
2
3
import { type ComponentProps , type ReactNode , useCallback } from "react" ;
3
4
import {
@@ -50,7 +51,7 @@ export const AccountSummary = ({
50
51
alt = ""
51
52
className = "absolute -right-40 hidden h-full object-cover object-right [mask-image:linear-gradient(to_right,_transparent,_black_50%)] md:block"
52
53
/>
53
- < div className = "relative flex flex-col items-start justify-between gap-8 sm:px-6 sm:py-10 md:gap-16 md :px-12 md :py-20 xl:flex-row xl:items-center " >
54
+ < div className = "relative flex flex-row items-center justify-between gap-8 sm:px-6 sm:py-10 md:gap-16 lg :px-12 lg :py-20" >
54
55
< div >
55
56
< div className = "mb-2 inline-block border border-neutral-600/50 bg-neutral-900 px-4 py-1 text-xs text-neutral-400 sm:mb-4" >
56
57
Total Balance
@@ -114,24 +115,42 @@ export const AccountSummary = ({
114
115
transfer = { api . deposit }
115
116
enableWithZeroMax
116
117
/>
118
+ < WithdrawButton
119
+ api = { api }
120
+ max = { availableToWithdraw }
121
+ className = "xl:hidden"
122
+ />
123
+ { api . type === ApiStateType . Loaded ? (
124
+ < DialogTrigger >
125
+ < Button variant = "secondary" className = "xl:hidden" >
126
+ Claim
127
+ </ Button >
128
+ { availableRewards > 0n ? (
129
+ < ClaimDialog
130
+ expiringRewards = { expiringRewards }
131
+ availableRewards = { availableRewards }
132
+ api = { api }
133
+ />
134
+ ) : (
135
+ < ModalDialog title = "No Rewards" closeButtonText = "Ok" >
136
+ < p > You have no rewards available to be claimed</ p >
137
+ </ ModalDialog >
138
+ ) }
139
+ </ DialogTrigger >
140
+ ) : (
141
+ < Button variant = "secondary" isDisabled = { true } className = "lg:hidden" >
142
+ Claim
143
+ </ Button >
144
+ ) }
117
145
</ div >
118
146
</ div >
119
- < div className = "flex w-full flex-row items-stretch gap-4 xl:w-auto " >
147
+ < div className = "hidden w-auto items-stretch gap-4 xl:flex " >
120
148
< BalanceCategory
121
149
name = "Unlocked & Unstaked"
122
150
amount = { availableToWithdraw }
123
151
description = "The amount of unlocked tokens that are not staked in either program"
124
152
action = {
125
- < TransferButton
126
- size = "small"
127
- variant = "secondary"
128
- actionDescription = "Move funds from your account back to your wallet"
129
- actionName = "Withdraw"
130
- max = { availableToWithdraw }
131
- { ...( api . type === ApiStateType . Loaded && {
132
- transfer : api . withdraw ,
133
- } ) }
134
- />
153
+ < WithdrawButton api = { api } max = { availableToWithdraw } size = "small" />
135
154
}
136
155
/>
137
156
< BalanceCategory
@@ -140,7 +159,12 @@ export const AccountSummary = ({
140
159
description = "Rewards you have earned from OIS"
141
160
action = {
142
161
api . type === ApiStateType . Loaded ? (
143
- < ClaimButton isDisabled = { availableRewards === 0n } api = { api } />
162
+ < ClaimButton
163
+ size = "small"
164
+ variant = "secondary"
165
+ isDisabled = { availableRewards === 0n }
166
+ api = { api }
167
+ />
144
168
) : (
145
169
< Button size = "small" variant = "secondary" isDisabled = { true } >
146
170
Claim
@@ -163,6 +187,33 @@ export const AccountSummary = ({
163
187
</ section >
164
188
) ;
165
189
190
+ type WithdrawButtonProps = Omit <
191
+ ComponentProps < typeof TransferButton > ,
192
+ "variant" | "actionDescription" | "actionName" | "transfer"
193
+ > & {
194
+ api : States [ ApiStateType . Loaded ] | States [ ApiStateType . LoadedNoStakeAccount ] ;
195
+ } ;
196
+
197
+ const WithdrawButton = ( { api, ...props } : WithdrawButtonProps ) => (
198
+ < TransferButton
199
+ variant = "secondary"
200
+ actionDescription = "Move funds from your account back to your wallet"
201
+ actionName = "Withdraw"
202
+ { ...( api . type === ApiStateType . Loaded && {
203
+ transfer : api . withdraw ,
204
+ } ) }
205
+ { ...props }
206
+ >
207
+ < div className = "mb-4 flex max-w-96 flex-row gap-2 border border-neutral-600/50 bg-pythpurple-400/20 p-4" >
208
+ < InformationCircleIcon className = "size-8 flex-none" />
209
+ < div className = "text-sm" >
210
+ You can only withdraw tokens that are unlocked and not staked in either
211
+ OIS or Pyth Governance
212
+ </ div >
213
+ </ div >
214
+ </ TransferButton >
215
+ ) ;
216
+
166
217
type BalanceCategoryProps = {
167
218
name : string ;
168
219
amount : bigint ;
@@ -178,23 +229,88 @@ const BalanceCategory = ({
178
229
action,
179
230
warning,
180
231
} : BalanceCategoryProps ) => (
181
- < div className = "flex w-full flex-col justify-between border border-neutral-600/50 bg-pythpurple-800/60 p-6 backdrop-blur xl :w-96" >
232
+ < div className = "flex w-full flex-col justify-between border border-neutral-600/50 bg-pythpurple-800/60 p-4 backdrop-blur sm:p-6 xl:w-80 2xl :w-96" >
182
233
< div >
183
234
< div className = "mb-4 inline-block border border-neutral-600/50 bg-neutral-900 px-4 py-1 text-xs text-neutral-400" >
184
235
{ name }
185
236
</ div >
186
237
< div >
187
238
< Tokens className = "text-xl font-light" > { amount } </ Tokens >
188
239
</ div >
189
- < p className = "mt-4 max-w-xs text-sm text-neutral-500" > { description } </ p >
240
+ < p className = "mt-4 text-sm text-neutral-500" > { description } </ p >
190
241
</ div >
191
242
< div className = "mt-4 flex flex-row items-center gap-4" >
192
243
{ action }
193
- { warning && < p className = "max-w-xs text-xs text-red-600" > { warning } </ p > }
244
+ { warning && < p className = "text-xs text-red-600" > { warning } </ p > }
194
245
</ div >
195
246
</ div >
196
247
) ;
197
248
249
+ type ClaimDialogProps = {
250
+ availableRewards : bigint ;
251
+ expiringRewards : Date | undefined ;
252
+ api : States [ ApiStateType . Loaded ] ;
253
+ } ;
254
+
255
+ const ClaimDialog = ( {
256
+ api,
257
+ expiringRewards,
258
+ availableRewards,
259
+ } : ClaimDialogProps ) => {
260
+ const { state, execute } = useAsync ( api . claim ) ;
261
+
262
+ const doClaim = useCallback ( ( ) => {
263
+ execute ( ) . catch ( ( ) => {
264
+ /* TODO figure out a better UI treatment for when claim fails */
265
+ } ) ;
266
+ } , [ execute ] ) ;
267
+
268
+ return (
269
+ < ModalDialog title = "Claim" >
270
+ { ( { close } ) => (
271
+ < >
272
+ < p className = "mb-4" >
273
+ Claim your < Tokens > { availableRewards } </ Tokens > rewards
274
+ </ p >
275
+ { expiringRewards && (
276
+ < div className = "mb-4 flex max-w-96 flex-row gap-2 border border-neutral-600/50 bg-pythpurple-400/20 p-4" >
277
+ < InformationCircleIcon className = "size-8 flex-none" />
278
+ < div className = "text-sm" >
279
+ Rewards expire one year from the epoch in which they were
280
+ earned. You have rewards expiring on{ " " }
281
+ { expiringRewards . toLocaleDateString ( ) } .
282
+ </ div >
283
+ </ div >
284
+ ) }
285
+ { state . type === StateType . Error && (
286
+ < p className = "mt-8 text-red-600" >
287
+ Uh oh, an error occurred! Please try again
288
+ </ p >
289
+ ) }
290
+ < div className = "mt-14 flex flex-col gap-8 sm:flex-row sm:justify-between" >
291
+ < Button
292
+ variant = "secondary"
293
+ className = "w-full sm:w-auto"
294
+ size = "noshrink"
295
+ onPress = { close }
296
+ >
297
+ Cancel
298
+ </ Button >
299
+ < Button
300
+ className = "w-full sm:w-auto"
301
+ size = "noshrink"
302
+ isLoading = { state . type === StateType . Running }
303
+ onPress = { doClaim }
304
+ >
305
+ Claim
306
+ </ Button >
307
+ </ div >
308
+ </ >
309
+ ) }
310
+ </ ModalDialog >
311
+ ) ;
312
+ } ;
313
+
198
314
type ClaimButtonProps = Omit <
199
315
ComponentProps < typeof Button > ,
200
316
"onClick" | "disabled" | "loading"
@@ -213,8 +329,6 @@ const ClaimButton = ({ api, ...props }: ClaimButtonProps) => {
213
329
214
330
return (
215
331
< Button
216
- size = "small"
217
- variant = "secondary"
218
332
onPress = { doClaim }
219
333
isDisabled = { state . type !== StateType . Base }
220
334
isLoading = { state . type === StateType . Running }
0 commit comments