@@ -12,13 +12,17 @@ import type { BarkVtxo } from "react-native-nitro-ark";
1212import { useGetBlockHeight } from "~/hooks/useMarketData" ;
1313import { formatBip177 } from "~/lib/utils" ;
1414import { getMempoolTxUrl } from "~/constants" ;
15- import { NoahButton } from "~/components/ui/NoahButton" ;
1615import type { SettingsStackParamList } from "~/Navigators" ;
16+ import { useGetExpiringVtxos , useGetVtxos , useRefreshExpiringVtxos } from "~/hooks/useWallet" ;
17+ import { StatusBannerStrip } from "~/components/StatusBannerStrip" ;
1718
1819type VTXOWithStatus = BarkVtxo & {
1920 isExpiring : boolean ;
21+ isExpired : boolean ;
2022} ;
2123
24+ const EXPIRED_COLOR = "#ef4444" ;
25+
2226const VTXODetailRow = ( {
2327 label,
2428 value,
@@ -93,27 +97,54 @@ const VTXODetailScreen = () => {
9397 const navigation = useNavigation < NativeStackNavigationProp < SettingsStackParamList > > ( ) ;
9498 const iconColor = useIconColor ( ) ;
9599 const { data : blockHeight } = useGetBlockHeight ( ) ;
96- const { vtxo } = route . params as { vtxo : VTXOWithStatus } ;
100+ const { vtxo : routeVtxo } = route . params as { vtxo : VTXOWithStatus } ;
101+ const { data : allVtxos = [ ] } = useGetVtxos ( ) ;
102+ const { data : expiringVtxos } = useGetExpiringVtxos ( ) ;
103+ const refreshExpiringVtxos = useRefreshExpiringVtxos ( ) ;
104+ const latestVtxo = allVtxos . find ( ( item ) => item . point === routeVtxo . point ) ;
105+ const currentVtxo = latestVtxo ?? routeVtxo ;
106+ const isLatestExpiring = expiringVtxos ?. some ( ( item ) => item . point === currentVtxo . point ) ;
107+ const vtxo : VTXOWithStatus = {
108+ ...currentVtxo ,
109+ isExpiring : isLatestExpiring ?? routeVtxo . isExpiring ,
110+ isExpired : routeVtxo . isExpired ?? false ,
111+ } ;
97112 const anchorExplorerUrl = getMempoolTxUrl ( vtxo . anchor_point ) ;
113+ const isExpired =
114+ vtxo . state !== "Locked" &&
115+ ( blockHeight !== undefined ? vtxo . expiry_height <= blockHeight : vtxo . isExpired ) ;
116+ const canRefresh = vtxo . state !== "Locked" && ( vtxo . isExpiring || isExpired ) ;
117+ const statusLabel =
118+ vtxo . state === "Locked"
119+ ? "Locked"
120+ : isExpired
121+ ? "Expired"
122+ : vtxo . isExpiring
123+ ? "Expiring"
124+ : "Active" ;
98125
99126 const getStatusColor = ( vtxo : VTXOWithStatus ) => {
100127 if ( vtxo . state === "Locked" ) return "text-gray-500" ;
128+ if ( isExpired ) return "text-red-500" ;
101129 return vtxo . isExpiring ? "text-orange-500" : "text-green-500" ;
102130 } ;
103131
104132 const getStatusIcon = ( vtxo : VTXOWithStatus ) => {
105133 if ( vtxo . state === "Locked" ) return "lock-closed-outline" ;
134+ if ( isExpired ) return "alert-circle-outline" ;
106135 return vtxo . isExpiring ? "warning-outline" : "checkmark-circle-outline" ;
107136 } ;
108137
109138 const getVtxoIcon = ( vtxo : VTXOWithStatus ) => {
110139 if ( vtxo . state === "Locked" ) return "lock-closed-outline" ;
140+ if ( isExpired ) return "alert-circle-outline" ;
111141 return vtxo . isExpiring ? "warning-outline" : "cube-outline" ;
112142 } ;
113143
114144 const getVtxoColor = ( vtxo : VTXOWithStatus ) => {
115145 if ( vtxo . state === "Locked" ) return "#6b7280" ;
116- return vtxo . isExpiring ? "#f97316" : "#22c55e" ;
146+ if ( isExpired ) return EXPIRED_COLOR ;
147+ return vtxo . isExpiring ? COLORS . BITCOIN_ORANGE : "#22c55e" ;
117148 } ;
118149
119150 return (
@@ -131,6 +162,27 @@ const VTXODetailScreen = () => {
131162 showsVerticalScrollIndicator = { false }
132163 contentContainerStyle = { { paddingBottom : 50 } }
133164 >
165+ { canRefresh ? (
166+ < StatusBannerStrip
167+ className = "mb-4"
168+ title = { isExpired ? "VTXO expired" : "VTXO expiring soon" }
169+ message = "Refresh this VTXO to keep it available."
170+ icon = {
171+ < Icon
172+ name = { isExpired ? "alert-circle-outline" : "warning-outline" }
173+ size = { 16 }
174+ color = { isExpired ? EXPIRED_COLOR : COLORS . BITCOIN_ORANGE }
175+ />
176+ }
177+ tone = { isExpired ? "failed" : "info" }
178+ actionLabel = "Refresh"
179+ actionBusyLabel = "Refreshing"
180+ actionTextStyle = { { color : COLORS . BITCOIN_ORANGE } }
181+ isActionLoading = { refreshExpiringVtxos . isPending }
182+ onActionPress = { ( ) => refreshExpiringVtxos . mutate ( ) }
183+ />
184+ ) : null }
185+
134186 < View className = "items-center my-8" >
135187 < View className = "mb-4" >
136188 < Icon name = { getVtxoIcon ( vtxo ) } size = { 64 } color = { getVtxoColor ( vtxo ) } />
@@ -141,18 +193,15 @@ const VTXODetailScreen = () => {
141193 < View className = "flex-row items-center" >
142194 < Icon name = { getStatusIcon ( vtxo ) } size = { 20 } color = { getVtxoColor ( vtxo ) } />
143195 < Text className = { `text-xl font-medium ml-2 ${ getStatusColor ( vtxo ) } ` } >
144- { vtxo . state === "Locked" ? "Locked" : vtxo . isExpiring ? "Expiring" : "Active" }
196+ { statusLabel }
145197 </ Text >
146198 </ View >
147199 </ View >
148200
149201 < View className = "bg-card p-4 rounded-lg mb-4" >
150202 < VTXODetailRow label = "Amount" value = { formatBip177 ( vtxo . amount ) } />
151203 < VTXODetailRow label = "State" value = { vtxo . state } />
152- < VTXODetailRow
153- label = "Status"
154- value = { vtxo . state === "Locked" ? "Locked" : vtxo . isExpiring ? "Expiring" : "Active" }
155- />
204+ < VTXODetailRow label = "Status" value = { statusLabel } />
156205 < VTXODetailRow
157206 label = "Current Block Height"
158207 value = { blockHeight ? blockHeight . toLocaleString ( ) : "Loading..." }
@@ -182,21 +231,6 @@ const VTXODetailScreen = () => {
182231 />
183232 < VTXODetailRow label = "Server Public Key" value = { vtxo . server_pubkey } copyable />
184233 </ View >
185-
186- < View className = "rounded-lg border border-amber-500/30 bg-amber-500/10 p-4" >
187- < Text className = "text-base font-semibold text-amber-700 dark:text-amber-300" >
188- Emergency exit
189- </ Text >
190- < Text className = "mt-1 text-sm leading-5 text-amber-700/90 dark:text-amber-200/90" >
191- Use only if the Ark server is unavailable and normal offboarding cannot be used.
192- </ Text >
193- < NoahButton
194- className = "mt-4"
195- onPress = { ( ) => navigation . navigate ( "UnilateralExit" , { vtxoIds : [ vtxo . point ] } ) }
196- >
197- Exit This VTXO
198- </ NoahButton >
199- </ View >
200234 </ ScrollView >
201235 </ View >
202236 </ NoahSafeAreaView >
0 commit comments