Skip to content

Commit 1032e08

Browse files
feat: revoke reason, ui types
1 parent 5b60097 commit 1032e08

File tree

3 files changed

+230
-43
lines changed

3 files changed

+230
-43
lines changed

src/languages/en_US.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2948,6 +2948,36 @@
29482948
"view_private_key": "View Private Key",
29492949
"copy_wallet_address": "Copy wallet address"
29502950
},
2951+
"delegations": {
2952+
"active": "Active",
2953+
"not_active": "Not active",
2954+
"enable_smart_wallet": "Enable Smart Wallet",
2955+
"disable_smart_wallet": "Disable Smart Wallet",
2956+
"enabled_description": "You'll miss out on faster transactions and lower network costs",
2957+
"disabled_description": "Updates will take effect on your next swap",
2958+
"revoke_panel": {
2959+
"disable_title": "Disable Smart Wallet",
2960+
"disable_subtitle": "This will revoke all active delegations and disable Smart Wallet features.",
2961+
"disable_button": "Hold to Disable",
2962+
"revoke_network_title": "Revoke %{network}",
2963+
"revoke_network_subtitle": "This will remove the delegation for this network.",
2964+
"revoke_network_button": "Hold to Revoke",
2965+
"conflict_title": "Smart Wallet Conflict",
2966+
"conflict_subtitle": "Your wallet is delegated to a different provider's Smart Wallet. Disable this to use Rainbow instead.",
2967+
"conflict_button": "Hold to Disable",
2968+
"security_title": "Security Alert",
2969+
"security_subtitle": "An unknown delegation was detected. Revoke it to secure your wallet.",
2970+
"security_button": "Hold to Revoke",
2971+
"settings_title": "Revoke Delegation",
2972+
"settings_subtitle": "Remove delegation from this contract.",
2973+
"settings_button": "Hold to Revoke",
2974+
"revoking": "Revoking...",
2975+
"done": "Done",
2976+
"next": "Next",
2977+
"try_again": "Try Again",
2978+
"gas_fee": "to revoke on %{chainName}"
2979+
}
2980+
},
29512981
"balance_title": "Balance",
29522982
"buy": "Buy",
29532983
"change_wallet": {

src/navigation/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { ScrollView } from 'react-native';
4040
import { HlTrade, PerpMarket, PerpsPosition, TriggerOrderSource, TriggerOrderType } from '@/features/perps/types';
4141
import { PolymarketPosition } from '@/features/polymarket/types';
4242
import { PolymarketEvent, PolymarketMarket, PolymarketMarketEvent } from '@/features/polymarket/types/polymarket-event';
43+
import { RevokeReason } from '@/screens/delegation/RevokeDelegationPanel';
4344

4445
export type PortalSheetProps = {
4546
children: React.FC;
@@ -709,6 +710,7 @@ type RouteParams = {
709710
contractAddress: Address;
710711
}>;
711712
onSuccess?: () => void;
713+
revokeReason?: RevokeReason;
712714
};
713715
};
714716

Lines changed: 198 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import React, { useCallback, useState } from 'react';
2+
import { StyleSheet } from 'react-native';
23
import { RouteProp, useRoute } from '@react-navigation/native';
34
import { Wallet } from '@ethersproject/wallet';
5+
import LinearGradient from 'react-native-linear-gradient';
46
import { 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';
810
import { RootStackParamList } from '@/navigation/types';
911
import Routes from '@/navigation/routesNames';
1012
import { logger, RainbowError } from '@/logger';
@@ -14,28 +16,104 @@ import { useWalletsStore } from '@/state/wallets/walletsStore';
1416
import { loadWallet } from '@/model/wallet';
1517
import { getProvider } from '@/handlers/web3';
1618
import { 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

1839
export 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+
2796
export 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

Comments
 (0)