feat(wallet): Edit Spend Limit Popup (#30690)

This commit is contained in:
Douglas Daniel
2025-08-19 09:48:21 -05:00
committed by GitHub
parent a81e9deace
commit abcec02274
7 changed files with 349 additions and 0 deletions
@@ -955,6 +955,8 @@ inline constexpr webui::LocalizedString kLocalizedStrings[] = {
IDS_BRAVE_WALLET_EDIT_PERMISSIONS_PROPOSED_ALLOWANCE},
{"braveWalletEditPermissionsCustomAllowance",
IDS_BRAVE_WALLET_EDIT_PERMISSIONS_CUSTOM_ALLOWANCE},
{"braveWalletProposedSpendLimit", IDS_BRAVE_WALLET_PROPOSED_SPEND_LIMIT},
{"braveWalletCustomSpendLimit", IDS_BRAVE_WALLET_CUSTOM_SPEND_LIMIT},
{"braveWalletNotValidEthAddress", IDS_BRAVE_WALLET_NOT_VALID_ETH_ADDRESS},
{"braveWalletNotValidSolAddress", IDS_BRAVE_WALLET_NOT_VALID_SOL_ADDRESS},
{"braveWalletNotValidAddress", IDS_BRAVE_WALLET_NOT_VALID_ADDRESS},
@@ -0,0 +1,54 @@
// Copyright (c) 2025 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
import * as React from 'react'
// Utils
import { getLocale } from '../../../../common/locale'
// Components
import {
EditSpendLimit, //
} from './edit_spend_limit'
import { BottomSheet } from '../../shared/bottom_sheet/bottom_sheet'
import {
WalletPanelStory, //
} from '../../../stories/wrappers/wallet-panel-story-wrapper'
export const _EditSpendLimit = {
render: () => {
return (
<BottomSheet
isOpen={true}
title={getLocale('braveWalletEditPermissionsTitle')}
onClose={() => alert('Close Clicked')}
>
<EditSpendLimit
onCancel={() => alert('Cancel Clicked')}
onSave={(limit: string) => alert(`Spend limit updated to ${limit}`)}
approvalTarget='0x Exchange Proxy'
isApprovalUnlimited={false}
proposedAllowance='100'
symbol='ETH'
/>
</BottomSheet>
)
},
}
export default {
title: 'Wallet/Panel/Components/Edit Spend Limit',
component: EditSpendLimit,
parameters: {
layout: 'centered',
},
decorators: [
(Story: any) => (
<WalletPanelStory>
<Story />
</WalletPanelStory>
),
],
}
@@ -0,0 +1,68 @@
// Copyright (c) 2025 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
import styled from 'styled-components'
import * as leo from '@brave/leo/tokens/css/variables'
// Shared Styles
import { Column, Text } from '../../shared/style'
export const StyledWrapper = styled(Column)`
overflow: hidden;
`
export const LabelText = styled(Text)`
font: ${leo.font.default.regular};
letter-spacing: ${leo.typography.letterSpacing.default};
`
export const AmountText = styled(Text)`
font: ${leo.font.large.semibold};
letter-spacing: ${leo.typography.letterSpacing.default};
`
export const Input = styled.input<{
hasError?: boolean
}>`
font: ${leo.font.small.regular};
letter-spacing: ${leo.typography.letterSpacing.small};
background-color: ${leo.color.container.highlight};
color: ${leo.color.text.primary};
border: none;
width: 100%;
padding: 0px;
outline: 1px solid ${leo.color.container.highlight};
transition:
outline 0.1s ease-in-out,
box-shadow 0.1s ease-in-out;
border-radius: ${leo.radius.m};
padding: 10px 12px;
::placeholder {
font: ${leo.font.small.regular};
letter-spacing: ${leo.typography.letterSpacing.small};
color: ${leo.color.text.tertiary};
}
:hover {
outline: 1px solid ${leo.color.divider.strong};
box-shadow: ${leo.effect.elevation['02']};
}
:focus {
outline: 2px solid ${leo.color.primary[40]};
}
::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
`
export const ButtonText = styled.span`
display: flex;
width: 80px;
justify-content: center;
`
@@ -0,0 +1,55 @@
// Copyright (c) 2025 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
import * as React from 'react'
import { render, screen } from '@testing-library/react'
import { Provider } from 'react-redux'
// Utils
import {
// eslint-disable-next-line import/no-named-default
default as BraveCoreThemeProvider,
} from '../../../../common/BraveCoreThemeProvider'
import { createMockStore } from '../../../utils/test-utils'
// Components
import { EditSpendLimit } from './edit_spend_limit'
describe('EditSpendLimit', () => {
const renderComponent = () => {
const store = createMockStore({})
return render(
<Provider store={store}>
<BraveCoreThemeProvider>
<EditSpendLimit
onCancel={jest.fn()}
onSave={jest.fn()}
proposedAllowance='100'
symbol='ETH'
approvalTarget='Uniswap V3'
isApprovalUnlimited={false}
/>
</BraveCoreThemeProvider>
</Provider>,
)
}
it('should render the component', () => {
renderComponent()
expect(
screen.getByText('braveWalletEditPermissionsDescription'),
).toBeInTheDocument()
expect(
screen.getByText('braveWalletProposedSpendLimit'),
).toBeInTheDocument()
expect(screen.getByText('100 ETH')).toBeInTheDocument()
expect(screen.getByText('braveWalletCustomSpendLimit')).toBeInTheDocument()
expect(screen.getByText('braveWalletButtonCancel')).toBeInTheDocument()
expect(
screen.getByText('braveWalletAccountSettingsSave'),
).toBeInTheDocument()
})
})
@@ -0,0 +1,166 @@
// Copyright (c) 2025 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
import * as React from 'react'
import Button from '@brave/leo/react/button'
import RadioButton from '@brave/leo/react/radioButton'
// Utils
import { getLocale } from '../../../../common/locale'
// Styled Components
import {
StyledWrapper,
LabelText,
AmountText,
Input,
ButtonText,
} from './edit_spend_limit.style'
import { Column, Row } from '../../shared/style'
type AllowanceTypes = 'proposed' | 'custom'
export interface Props {
onCancel: () => void
onSave: (limit: string) => void
proposedAllowance: string
symbol: string
approvalTarget: string
isApprovalUnlimited: boolean
}
export const EditSpendLimit = (props: Props) => {
const {
onCancel,
onSave,
approvalTarget,
isApprovalUnlimited,
proposedAllowance,
symbol,
} = props
// State
const [allowanceType, setAllowanceType] =
React.useState<AllowanceTypes>('proposed')
const [customAllowance, setCustomAllowance] = React.useState<string>('')
// Refs
const customAllowanceInputRef = React.useRef<HTMLInputElement>(null)
// Methods
const toggleAllowanceRadio = (key: AllowanceTypes) => {
if (key === 'custom') {
customAllowanceInputRef.current?.focus()
}
setAllowanceType(key)
}
const onChangeCustomAllowance = (
event: React.ChangeEvent<HTMLInputElement>,
) => {
setCustomAllowance(event.target.value)
}
const onClickSave = React.useCallback(() => {
onSave(allowanceType === 'custom' ? customAllowance : proposedAllowance)
onCancel()
}, [allowanceType, customAllowance, proposedAllowance, onSave, onCancel])
// Memos
const isSaveButtonDisabled = React.useMemo(() => {
return allowanceType === 'custom' && customAllowance === ''
}, [allowanceType, customAllowance])
const formattedProposedAllowance = React.useMemo(() => {
return isApprovalUnlimited
? getLocale('braveWalletTransactionApproveUnlimited')
: proposedAllowance
}, [proposedAllowance, isApprovalUnlimited])
return (
<StyledWrapper
gap='32px'
padding='16px'
width='100%'
>
<LabelText
textColor='secondary'
textAlign='left'
>
{getLocale('braveWalletEditPermissionsDescription').replace(
'$1',
approvalTarget,
)}
</LabelText>
<Column
gap='16px'
width='100%'
alignItems='flex-start'
justifyContent='flex-start'
>
<RadioButton
name='proposed'
value='proposed'
currentValue={allowanceType}
onChange={() => toggleAllowanceRadio('proposed')}
>
<Column
alignItems='flex-start'
justifyContent='flex-start'
>
<LabelText textColor='primary'>
{getLocale('braveWalletProposedSpendLimit')}
</LabelText>
<AmountText textColor='primary'>
{formattedProposedAllowance} {symbol}
</AmountText>
</Column>
</RadioButton>
<RadioButton
name='custom'
value='custom'
currentValue={allowanceType}
onChange={() => toggleAllowanceRadio('custom')}
>
<Column
alignItems='flex-start'
justifyContent='flex-start'
gap='4px'
>
<LabelText textColor='primary'>
{getLocale('braveWalletCustomSpendLimit')}
</LabelText>
<Input
ref={customAllowanceInputRef}
placeholder={proposedAllowance}
type='number'
value={customAllowance}
onChange={onChangeCustomAllowance}
data-testid='custom-allowance-input'
/>
</Column>
</RadioButton>
</Column>
<Row gap='16px'>
<Button
kind='outline'
size='medium'
onClick={onCancel}
>
<ButtonText>{getLocale('braveWalletButtonCancel')}</ButtonText>
</Button>
<Button
size='medium'
onClick={onClickSave}
isDisabled={isSaveButtonDisabled}
>
<ButtonText>{getLocale('braveWalletAccountSettingsSave')}</ButtonText>
</Button>
</Row>
</StyledWrapper>
)
}
export default EditSpendLimit
@@ -1080,6 +1080,8 @@ provideStrings({
braveWalletEditPermissionsButton: 'Edit permissions',
braveWalletEditPermissionsProposedAllowance: 'Proposed allowance',
braveWalletEditPermissionsCustomAllowance: 'Custom allowance',
braveWalletProposedSpendLimit: 'Proposed spend limit',
braveWalletCustomSpendLimit: 'Custom spend limit',
// Send Input Errors
braveWalletNotValidFilAddress: 'Not a valid FIL address',
+2
View File
@@ -514,6 +514,8 @@
<message name="IDS_BRAVE_WALLET_EDIT_PERMISSIONS_BUTTON" desc="Edit allowance panel button">Edit permissions</message>
<message name="IDS_BRAVE_WALLET_EDIT_PERMISSIONS_PROPOSED_ALLOWANCE" desc="Edit allowance panel button">Proposed allowance</message>
<message name="IDS_BRAVE_WALLET_EDIT_PERMISSIONS_CUSTOM_ALLOWANCE" desc="Edit allowance panel button">Custom allowance</message>
<message name="IDS_BRAVE_WALLET_PROPOSED_SPEND_LIMIT" desc="Edit allowance proposed spend limit label">Proposed spend limit</message>
<message name="IDS_BRAVE_WALLET_CUSTOM_SPEND_LIMIT" desc="Edit allowance custom spend limit label">Custom spend limit</message>
<message name="IDS_BRAVE_WALLET_NOT_VALID_ETH_ADDRESS" desc="Send input not valid ETH address error">Not a valid ETH address</message>
<message name="IDS_BRAVE_WALLET_NOT_VALID_SOL_ADDRESS" desc="Send input not valid SOL address error">Not a valid SOL address</message>
<message name="IDS_BRAVE_WALLET_NOT_VALID_ADDRESS" desc="Send input not valid address error">Not a valid <ph name="COIN_TYPE_SYMBOL"><ex>ETH</ex>$1</ph> address.</message>