Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ function createAdapter(
connectWallet: async () => {},
setSecondaryAddressInput: () => {},
checkSecondaryAddress: async () => {},
changeSecondaryAddress: () => {},
connectChain: async () => {},
disconnectChain: async () => {},
...actionOverrides,
Expand Down
20 changes: 19 additions & 1 deletion packages/connect-a-wallet-widget/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,16 @@ export function useConnectAWalletAdapter(
setSecondaryAddressInputState(value)
}, [])

// Lets the user re-open the address form from the 'ready' state to check a
// different address, since there's otherwise no way back to it once a
// secondary address has been validated.
const changeSecondaryAddress = useCallback(() => {
setSecondaryAddress(null)
setSecondaryAddressInputState('')
setError(null)
setStatus('connected_no_input')
}, [])

const handleConnectWallet = useCallback(async (): Promise<void> => {
setIsConnectingWallet(true)
try {
Expand Down Expand Up @@ -402,10 +412,18 @@ export function useConnectAWalletAdapter(
connectWallet: handleConnectWallet,
setSecondaryAddressInput,
checkSecondaryAddress,
changeSecondaryAddress,
connectChain,
disconnectChain,
}),
[handleConnectWallet, setSecondaryAddressInput, checkSecondaryAddress, connectChain, disconnectChain],
[
handleConnectWallet,
setSecondaryAddressInput,
checkSecondaryAddress,
changeSecondaryAddress,
connectChain,
disconnectChain,
],
)

return { state, actions }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ export function AddressLinkForm({
}: AddressLinkFormProps) {
return (
<AddressFormCard>
<Heading level={5}>Wallet address to link</Heading>
<Text secondary>
<Heading level={6} fontSize="$1">Connect or Disconnect Address</Heading>
<Text fontSize="$1" secondary>
Enter the address you want to connect to or disconnect from your GoodID, then check its
status on each supported chain.
</Text>
Expand All @@ -27,9 +27,10 @@ export function AddressLinkForm({
onChangeText={onChangeAddressInput}
placeholder="0x…"
disabled={isChecking}
size="sm"
/>
<ActionButton onPress={onCheckAddress} disabled={isChecking || !addressInput} fullWidth>
{isChecking ? <Spinner size="sm" /> : <ButtonText>Check address</ButtonText>}
<ActionButton onPress={onCheckAddress} disabled={isChecking || !addressInput} fullWidth size="sm">
{isChecking ? <Spinner size="sm" /> : <ButtonText fontSize="$1">Check address</ButtonText>}
</ActionButton>
</AddressFormCard>
)
Expand Down
124 changes: 105 additions & 19 deletions packages/connect-a-wallet-widget/src/components/ChainLinkRow.tsx

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const [disconnectHovered, setDisconnectHovered] = useState(false)
...
onMouseEnter={() => setDisconnectHovered(true)}
onMouseLeave={() => setDisconnectHovered(false)}

Nit: this widget runs on RN Web too, so onMouseEnter/onMouseLeave won't fire on touch. The copy button in PrimaryIdentityCard (and Copilot's earlier suggestion) uses hoverStyle instead — could we do the same here and drop the extra useState?
hoverStyle={{ backgroundColor: '$error' }}

Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import React from 'react'
import { AddressDisplay, Badge, BadgeText, ButtonText, ChainBadge, Spinner } from '@goodwidget/ui'
import React, { useState } from 'react'
import { ButtonText, Icon, Spinner, Stack, Text, XStack, YStack } from '@goodwidget/ui'
import type { ConnectAWalletChainLinkState } from '../widgetRuntimeContract'
import { chainLinkRowPresentation } from './format'
import { ActionButton, ChainRowCard } from './shared'
import { ActionButton } from './shared'

interface ChainLinkRowProps {
row: ConnectAWalletChainLinkState
address: `0x${string}`
onConnect: () => void
onDisconnect: () => void
}
Expand All @@ -16,24 +15,111 @@ interface ChainLinkRowProps {
* Disconnect (never hidden) with a Spinner while a status is in flight, per
* Bounty Lead sign-off on the human-reviewer checklist.
*/
export function ChainLinkRow({ row, address, onConnect, onDisconnect }: ChainLinkRowProps) {
export function ChainLinkRow({ row, onConnect, onDisconnect }: ChainLinkRowProps) {
const { actionLabel, isBusy, isDisabled } = chainLinkRowPresentation(row.status)
const handlePress = actionLabel === 'Connect' ? onConnect : onDisconnect
const [disconnectHovered, setDisconnectHovered] = useState(false)

// Map status to dot color and display text
let dotColor = '$placeholderColor'
let statusText = 'Not Connected'

if (row.status === 'connected') {
dotColor = '$success'
statusText = 'Connected'
} else if (row.status === 'connecting') {
statusText = 'Connecting...'
} else if (row.status === 'disconnecting') {
statusText = 'Disconnecting...'
} else if (row.status === 'checking') {
statusText = 'Checking...'
}

const isDisconnect = actionLabel === 'Disconnect'

return (
<ChainRowCard>
<ChainBadge chainId={row.chainId} name={row.chainName} />
<AddressDisplay address={address} size="sm" />
<Badge type={row.status === 'connected' ? 'success' : 'info'}>
<BadgeText>{row.status === 'checking' ? 'checking…' : row.status.replace('_', ' ')}</BadgeText>
</Badge>
<ActionButton
onPress={handlePress}
disabled={isDisabled}
variant={actionLabel === 'Disconnect' ? 'outline' : 'primary'}
>
{isBusy ? <Spinner size="sm" /> : <ButtonText>{actionLabel}</ButtonText>}
</ActionButton>
</ChainRowCard>
<XStack
padding="$4"
alignItems="center"
justifyContent="space-between"
gap="$3"
borderBottomWidth={1}
borderBottomColor="$borderColor"
flexWrap="wrap"
>
<XStack alignItems="center" gap="$2" flex={1} minWidth={0}>
<Stack
width={32}
height={32}
borderRadius="$full"
backgroundColor="$backgroundPress"
alignItems="center"
justifyContent="center"
flexShrink={0}
>
<Text fontWeight="700" fontSize="$1" color="$color">
{row.chainName.charAt(0)}
</Text>
</Stack>

<YStack gap="$0.5" flex={1} minWidth={0}>
<Text fontWeight="700" fontSize="$1" color="$color" numberOfLines={1}>
{row.chainName}
</Text>
<XStack alignItems="center" gap="$1">
<Stack width={5} height={5} borderRadius={3} backgroundColor={dotColor} flexShrink={0} />
<Text fontSize="$1" fontWeight="600" color="$placeholderColor">
{statusText}
</Text>
</XStack>
</YStack>
</XStack>

{isBusy ? (
<ActionButton
disabled
variant={isDisconnect ? 'outline' : 'primary'}
borderColor={isDisconnect ? '$error' : undefined}
paddingHorizontal="$3.5"
flexShrink={0}
>
<Spinner size="sm" />
</ActionButton>
) : isDisconnect ? (
<ActionButton
onPress={handlePress}
disabled={isDisabled}
variant="outline"
borderColor="$error"
hoverStyle={{ backgroundColor: '$error' }}
onHoverIn={() => setDisconnectHovered(true)}
onHoverOut={() => setDisconnectHovered(false)}
paddingHorizontal="$2.5"
paddingVertical="$1.5"
flexShrink={0}
>
Comment on lines +79 to +100

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call switching to hoverStyle for the background — that's the right cross-platform pattern. But the icon and label are still hardcoded to color="error" / color="$error", and previously the manual disconnectHovered state also flipped these to white on hover so the text stayed readable against the now-red background. With that state removed, hovering this button now renders red text/icon on a red background — effectively invisible.

hoverStyle on the parent ActionButton won't cascade to the Icon/ButtonText children's color props on its own. A couple of ways to fix:

If the design system supports Tamagui's group-hover pattern (wrap in a group, use $group-hover theme tokens on the children), that'd keep this fully declarative.
Otherwise, the simplest fix is reintroducing a small bit of hover state just for the text/icon color (keeping hoverStyle for the background, since that part works fine as-is):
const [hovered, setHovered] = useState(false)

...
<ActionButton
  ...
  hoverStyle={{ backgroundColor: '$error' }}
  onHoverIn={() => setHovered(true)}
  onHoverOut={() => setHovered(false)}
>
  <Icon name="unlink" size="xs" color={hovered ? 'white' : 'error'} />
  <ButtonText fontSize="$1" color={hovered ? '$white' : '$error'}>Disconnect</ButtonText>
</ActionButton>

(onHoverIn/onHoverOut are Tamagui's cross-platform hover events, so this keeps the RN Web compatibility win from this commit while restoring contrast.)

<XStack alignItems="center" gap="$1">
<Icon name="unlink" size="xs" color={disconnectHovered ? 'white' : 'error'} />
<ButtonText fontSize="$1" color={disconnectHovered ? '$white' : '$error'}>
Disconnect
</ButtonText>
</XStack>
</ActionButton>
) : (
<ActionButton
onPress={handlePress}
disabled={isDisabled}
variant="primary"
paddingHorizontal="$2.5"
paddingVertical="$1.5"
flexShrink={0}
>
<XStack alignItems="center" gap="$1">
<Icon name="link" size="xs" color="white" />
<ButtonText fontSize="$1" color="$white">Connect</ButtonText>
</XStack>
</ActionButton>
)}
</XStack>
)
}
Loading