11import React , { useCallback , useState } from 'react' ;
2+ import { StyleSheet } from 'react-native' ;
23import { RouteProp , useRoute } from '@react-navigation/native' ;
34import { Wallet } from '@ethersproject/wallet' ;
5+ import LinearGradient from 'react-native-linear-gradient' ;
46import { useNavigation } from '@/navigation' ;
5- import { Box , Text } from '@/design-system' ;
6- import { ClaimButton } from '@/screens/claimables/shared/ components/ClaimButton ' ;
7- import { ClaimPanel } from '@/screens/claimables/shared/ components/ClaimPanel ' ;
7+ import { Box , Text , globalColors , Separator } from '@/design-system' ;
8+ import { HoldToActivateButton } from '@/components/hold-to-activate-button/HoldToActivateButton ' ;
9+ import { PanelSheet } from '@/components/PanelSheet/PanelSheet ' ;
810import { RootStackParamList } from '@/navigation/types' ;
911import Routes from '@/navigation/routesNames' ;
1012import { logger , RainbowError } from '@/logger' ;
@@ -14,28 +16,104 @@ import { useWalletsStore } from '@/state/wallets/walletsStore';
1416import { loadWallet } from '@/model/wallet' ;
1517import { getProvider } from '@/handlers/web3' ;
1618import { getNextNonce } from '@/state/nonces' ;
19+ import { ChainId } from '@/state/backendNetworks/types' ;
20+ import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks' ;
21+ import * as i18n from '@/languages' ;
22+
23+ /**
24+ * Reasons for revoking delegation - determines the panel's appearance and messaging
25+ */
26+ export enum RevokeReason {
27+ /** User manually chose to disable Smart Wallet for all chains */
28+ DISABLE_SMART_WALLET = 'disable_smart_wallet' ,
29+ /** User manually chose to revoke a single network delegation */
30+ REVOKE_SINGLE_NETWORK = 'revoke_single_network' ,
31+ /** Third-party Smart Wallet provider detected - user can switch to Rainbow */
32+ THIRD_PARTY_CONFLICT = 'third_party_conflict' ,
33+ /** Security concern - unknown delegation detected */
34+ SECURITY_ALERT = 'security_alert' ,
35+ /** User-initiated revoke from settings */
36+ SETTINGS_REVOKE = 'settings_revoke' ,
37+ }
1738
1839export type RevokeStatus =
1940 | 'notReady' // preparing the data necessary to revoke
2041 | 'ready' // ready to revoke state
21- | 'claiming' // user has pressed the revoke button (keeping 'claiming' for compatibility with ClaimPanel/ClaimButton)
42+ | 'claiming' // user has pressed the revoke button
2243 | 'pending' // revoke has been submitted but we don't have a tx hash
2344 | 'success' // revoke has been submitted and we have a tx hash
2445 | 'recoverableError' // revoke or auth has failed, can try again
2546 | 'unrecoverableError' ; // revoke has failed, unrecoverable error
2647
48+ type SheetContent = {
49+ title : string ;
50+ subtitle : string ;
51+ buttonLabel : string ;
52+ accentColor : string ;
53+ } ;
54+
55+ const getSheetContent = ( reason : RevokeReason , chainName ?: string ) : SheetContent => {
56+ switch ( reason ) {
57+ case RevokeReason . DISABLE_SMART_WALLET :
58+ return {
59+ title : i18n . t ( i18n . l . wallet . delegations . revoke_panel . disable_title ) ,
60+ subtitle : i18n . t ( i18n . l . wallet . delegations . revoke_panel . disable_subtitle ) ,
61+ buttonLabel : i18n . t ( i18n . l . wallet . delegations . revoke_panel . disable_button ) ,
62+ accentColor : globalColors . blue60 ,
63+ } ;
64+ case RevokeReason . REVOKE_SINGLE_NETWORK :
65+ return {
66+ title : i18n . t ( i18n . l . wallet . delegations . revoke_panel . revoke_network_title , { network : chainName || '' } ) ,
67+ subtitle : i18n . t ( i18n . l . wallet . delegations . revoke_panel . revoke_network_subtitle ) ,
68+ buttonLabel : i18n . t ( i18n . l . wallet . delegations . revoke_panel . revoke_network_button ) ,
69+ accentColor : globalColors . blue60 ,
70+ } ;
71+ case RevokeReason . THIRD_PARTY_CONFLICT :
72+ return {
73+ title : i18n . t ( i18n . l . wallet . delegations . revoke_panel . conflict_title ) ,
74+ subtitle : i18n . t ( i18n . l . wallet . delegations . revoke_panel . conflict_subtitle ) ,
75+ buttonLabel : i18n . t ( i18n . l . wallet . delegations . revoke_panel . conflict_button ) ,
76+ accentColor : globalColors . orange60 ,
77+ } ;
78+ case RevokeReason . SECURITY_ALERT :
79+ return {
80+ title : i18n . t ( i18n . l . wallet . delegations . revoke_panel . security_title ) ,
81+ subtitle : i18n . t ( i18n . l . wallet . delegations . revoke_panel . security_subtitle ) ,
82+ buttonLabel : i18n . t ( i18n . l . wallet . delegations . revoke_panel . security_button ) ,
83+ accentColor : globalColors . red60 ,
84+ } ;
85+ case RevokeReason . SETTINGS_REVOKE :
86+ default :
87+ return {
88+ title : i18n . t ( i18n . l . wallet . delegations . revoke_panel . settings_title ) ,
89+ subtitle : i18n . t ( i18n . l . wallet . delegations . revoke_panel . settings_subtitle ) ,
90+ buttonLabel : i18n . t ( i18n . l . wallet . delegations . revoke_panel . settings_button ) ,
91+ accentColor : globalColors . blue60 ,
92+ } ;
93+ }
94+ } ;
95+
2796export const RevokeDelegationPanel = ( ) => {
2897 const { goBack } = useNavigation ( ) ;
2998 const {
30- params : { delegationsToRevoke, onSuccess } ,
99+ params : { delegationsToRevoke, onSuccess, revokeReason = RevokeReason . SETTINGS_REVOKE } ,
31100 } = useRoute < RouteProp < RootStackParamList , typeof Routes . REVOKE_DELEGATION_PANEL > > ( ) ;
32101
33102 const [ currentIndex , setCurrentIndex ] = useState ( 0 ) ;
34103 const [ revokeStatus , setRevokeStatus ] = useState < RevokeStatus > ( 'ready' ) ;
35104 const accountAddress = useWalletsStore ( state => state . accountAddress ) ;
105+ const getChainsLabel = useBackendNetworksStore ( state => state . getChainsLabel ) ;
36106
37107 const currentDelegation = delegationsToRevoke [ currentIndex ] ;
38108 const isLastDelegation = currentIndex === delegationsToRevoke . length - 1 ;
109+ const chainId = currentDelegation ?. chainId as ChainId ;
110+
111+ // Get chain name for display
112+ const chainsLabel = getChainsLabel ( ) ;
113+ const chainName = chainsLabel [ chainId ] || `Chain ${ chainId } ` ;
114+
115+ // Get sheet content based on revoke reason
116+ const sheetContent = getSheetContent ( revokeReason , chainName ) ;
39117
40118 const handleRevoke = useCallback ( async ( ) => {
41119 if ( ! currentDelegation || ! accountAddress ) return ;
@@ -55,14 +133,12 @@ export const RevokeDelegationPanel = () => {
55133 }
56134
57135 // Get current gas prices from provider
58- // TODO: use simulation to get the gas prices
59136 const feeData = await provider . getFeeData ( ) ;
60137 const maxFeePerGas = feeData . maxFeePerGas ?. toBigInt ( ) ?? 0n ;
61138 const maxPriorityFeePerGas = feeData . maxPriorityFeePerGas ?. toBigInt ( ) ?? 0n ;
62139
63140 const nonce = await getNextNonce ( { address : accountAddress , chainId : currentDelegation . chainId } ) ;
64141
65- // Remove the delegation using the SDK function
66142 const result = await executeRevokeDelegation ( {
67143 signer : wallet as Wallet ,
68144 address : accountAddress ,
@@ -88,7 +164,6 @@ export const RevokeDelegationPanel = () => {
88164 // Move to next delegation or finish
89165 setTimeout ( ( ) => {
90166 if ( isLastDelegation ) {
91- // Call success callback if provided
92167 onSuccess ?.( ) ;
93168 goBack ( ) ;
94169 } else {
@@ -105,63 +180,143 @@ export const RevokeDelegationPanel = () => {
105180 haptics . notificationError ( ) ;
106181 setRevokeStatus ( 'recoverableError' ) ;
107182 }
108- } , [ currentDelegation , accountAddress , isLastDelegation , goBack ] ) ;
183+ } , [ currentDelegation , accountAddress , isLastDelegation , goBack , onSuccess ] ) ;
109184
110185 const buttonLabel = ( ( ) => {
111186 switch ( revokeStatus ) {
112187 case 'ready' :
113- return 'Revoke Delegation' ;
114- case 'claiming' : // Transaction is being processed
115- return 'Revoking...' ;
188+ return sheetContent . buttonLabel ;
189+ case 'claiming' :
190+ return i18n . t ( i18n . l . wallet . delegations . revoke_panel . revoking ) ;
116191 case 'success' :
117- return isLastDelegation ? 'Done' : 'Next' ;
192+ return isLastDelegation ? i18n . t ( i18n . l . wallet . delegations . revoke_panel . done ) : i18n . t ( i18n . l . wallet . delegations . revoke_panel . next ) ;
118193 case 'recoverableError' :
119- return 'Try Again' ;
194+ return i18n . t ( i18n . l . wallet . delegations . revoke_panel . try_again ) ;
120195 default :
121- return 'Revoke Delegation' ;
196+ return sheetContent . buttonLabel ;
122197 }
123198 } ) ( ) ;
124199
200+ const handleButtonPress = useCallback ( ( ) => {
201+ if ( revokeStatus === 'ready' || revokeStatus === 'recoverableError' ) {
202+ handleRevoke ( ) ;
203+ } else if ( revokeStatus === 'success' && ! isLastDelegation ) {
204+ setCurrentIndex ( prev => prev + 1 ) ;
205+ setRevokeStatus ( 'ready' ) ;
206+ } else {
207+ goBack ( ) ;
208+ }
209+ } , [ revokeStatus , handleRevoke , isLastDelegation , goBack ] ) ;
210+
125211 if ( ! currentDelegation ) {
126212 return null ;
127213 }
128214
215+ const isReady = revokeStatus === 'ready' ;
216+ const isProcessing = revokeStatus === 'claiming' ;
217+ const isError = revokeStatus === 'recoverableError' ;
218+ const isSuccess = revokeStatus === 'success' ;
219+ const isConflict = revokeReason === RevokeReason . THIRD_PARTY_CONFLICT ;
220+ const isSecurityAlert = revokeReason === RevokeReason . SECURITY_ALERT ;
221+
129222 return (
130- < ClaimPanel title = "Security Notice" subtitle = "Remove delegation from this contract" claimStatus = { revokeStatus } iconUrl = "" >
131- < Box gap = { 20 } alignItems = "center" >
132- < Box alignItems = "center" gap = { 12 } >
133- < Text size = "17pt" weight = "bold" color = "label" >
134- Chain ID: { currentDelegation . chainId }
135- </ Text >
136- < Text size = "15pt" color = "labelSecondary" align = "center" >
137- { currentDelegation . contractAddress }
223+ < PanelSheet showHandle showTapToDismiss >
224+ { /* Header with Smart Wallet Icon */ }
225+ < Box alignItems = "center" paddingTop = "28px" paddingHorizontal = "20px" >
226+ { /* Smart Wallet Lock Icon with Gradient */ }
227+ < Box
228+ width = { { custom : 52 } }
229+ height = { { custom : 52 } }
230+ borderRadius = { 16 }
231+ borderWidth = { 1.926 }
232+ borderColor = { { custom : 'rgba(255, 255, 255, 0.1)' } }
233+ style = { styles . iconContainer }
234+ >
235+ < LinearGradient
236+ colors = {
237+ isError
238+ ? [ globalColors . red60 , globalColors . red80 , '#19002d' ]
239+ : isSuccess
240+ ? [ globalColors . green60 , globalColors . green80 , '#19002d' ]
241+ : isSecurityAlert
242+ ? [ globalColors . red60 , globalColors . red80 , '#19002d' ]
243+ : isConflict
244+ ? [ globalColors . orange60 , globalColors . orange80 , '#19002d' ]
245+ : [ '#3b7fff' , '#b724ad' , '#19002d' ]
246+ }
247+ locations = { [ 0.043 , 0.887 , 1 ] }
248+ useAngle
249+ angle = { 132.532 }
250+ angleCenter = { { x : 0.5 , y : 0.5 } }
251+ style = { StyleSheet . absoluteFill }
252+ />
253+ < Box alignItems = "center" justifyContent = "center" width = "full" height = "full" >
254+ < Text color = "white" size = "20pt" weight = "heavy" align = "center" style = { styles . iconText } >
255+ { isSuccess ? '' : isError ? '' : '' }
256+ </ Text >
257+ </ Box >
258+ </ Box >
259+
260+ { /* Title */ }
261+ < Box paddingTop = "24px" alignItems = "center" >
262+ < Text size = "26pt" weight = "heavy" color = "label" align = "center" >
263+ { sheetContent . title }
138264 </ Text >
139- < Text size = "13pt" color = "labelTertiary" align = "center" >
140- { currentIndex + 1 } of { delegationsToRevoke . length }
265+ </ Box >
266+
267+ { /* Subtitle */ }
268+ < Box paddingTop = "24px" width = { { custom : 295 } } >
269+ < Text size = "17pt" weight = "semibold" color = "labelSecondary" align = "center" >
270+ { sheetContent . subtitle }
141271 </ Text >
142272 </ Box >
143273 </ Box >
144274
145- < Box alignItems = "center" width = "full" >
146- < ClaimButton
147- enableHoldToPress = { revokeStatus === 'ready' }
148- isLoading = { revokeStatus === 'claiming' }
149- onPress = {
150- revokeStatus === 'ready' || revokeStatus === 'recoverableError'
151- ? handleRevoke
152- : revokeStatus === 'success' && ! isLastDelegation
153- ? ( ) => {
154- setCurrentIndex ( prev => prev + 1 ) ;
155- setRevokeStatus ( 'ready' ) ;
156- }
157- : goBack
158- }
159- disabled = { revokeStatus === 'claiming' }
160- shimmer = { revokeStatus === 'claiming' }
161- biometricIcon = { revokeStatus === 'ready' }
275+ { /* Separator */ }
276+ < Box paddingTop = "24px" paddingHorizontal = "20px" >
277+ < Separator color = "separatorTertiary" />
278+ </ Box >
279+
280+ { /* Action Button */ }
281+ < Box paddingTop = "24px" paddingHorizontal = "20px" >
282+ < HoldToActivateButton
283+ backgroundColor = { isSuccess ? globalColors . green60 : isError ? globalColors . red60 : globalColors . blue60 }
284+ disabledBackgroundColor = { 'rgba(38, 143, 255, 0.2)' }
285+ disabled = { isProcessing }
286+ isProcessing = { isProcessing }
162287 label = { buttonLabel }
288+ onLongPress = { handleButtonPress }
289+ height = { 48 }
290+ showBiometryIcon = { isReady }
291+ testID = "revoke-delegation-button"
292+ processingLabel = { buttonLabel }
293+ borderColor = { { custom : 'rgba(255, 255, 255, 0.08)' } }
294+ borderWidth = { 1 }
163295 />
164296 </ Box >
165- </ ClaimPanel >
297+
298+ { /* Gas Fee Preview */ }
299+ < Box paddingTop = "24px" paddingBottom = "24px" alignItems = "center" justifyContent = "center" >
300+ < Box flexDirection = "row" alignItems = "center" gap = { 4 } >
301+ < Text size = "13pt" weight = "heavy" color = { { custom : 'rgba(245, 248, 255, 0.56)' } } align = "center" >
302+
303+ </ Text >
304+ < Text size = "13pt" weight = "bold" color = { { custom : 'rgba(245, 248, 255, 0.56)' } } align = "center" >
305+ { i18n . t ( i18n . l . wallet . delegations . revoke_panel . gas_fee , { chainName } ) }
306+ </ Text >
307+ </ Box >
308+ </ Box >
309+ </ PanelSheet >
166310 ) ;
167311} ;
312+
313+ const styles = StyleSheet . create ( {
314+ iconContainer : {
315+ overflow : 'hidden' ,
316+ } ,
317+ iconText : {
318+ textShadowColor : 'rgba(0, 0, 0, 0.15)' ,
319+ textShadowOffset : { width : 0 , height : 2.167 } ,
320+ textShadowRadius : 5.778 ,
321+ } ,
322+ } ) ;
0 commit comments