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 @@ -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
122 changes: 103 additions & 19 deletions packages/connect-a-wallet-widget/src/components/ChainLinkRow.tsx
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,109 @@ 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"
backgroundColor={disconnectHovered ? '$error' : 'transparent'}
paddingHorizontal="$2.5"
paddingVertical="$1.5"
flexShrink={0}
onMouseEnter={() => setDisconnectHovered(true)}
onMouseLeave={() => setDisconnectHovered(false)}
>
<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>
)
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
import React from 'react'
import { Alert, Badge, BadgeText, ButtonText, Heading, Spinner, XStack } from '@goodwidget/ui'
import {
AddressDisplay,
Alert,
ButtonText,
Card,
Heading,
Icon,
Spinner,
Stack,
Text,
XStack,
YStack,
} from '@goodwidget/ui'
import type { ConnectAWalletWidgetAdapterResult } from '../widgetRuntimeContract'
import { AddressLinkForm } from './AddressLinkForm'
import { ChainLinkRow } from './ChainLinkRow'
import { PrimaryIdentityCard } from './PrimaryIdentityCard'
import { ActionButton, EmptyStateCard, WidgetContent } from './shared'
import { WalletGate } from './WalletGate'

const CHAIN_NAMES: Record<number, string> = {
122: 'Fuse',
42220: 'Celo',
50: 'XDC',
}

interface ConnectAWalletWidgetViewProps {
adapter: ConnectAWalletWidgetAdapterResult
'data-testid'?: string
Expand All @@ -22,16 +40,61 @@ export function ConnectAWalletWidgetView({
// header rather than relying on the host shell for it (see
// StreamingWidgetView) — matches the #113 design reference's top bar.
const header = (
<XStack justifyContent="space-between" alignItems="center" paddingHorizontal="$1">
<Heading level={4}>GoodDollar</Heading>
{state.activeChainId && (
<Badge type={state.isActiveChainSupported ? 'info' : 'warning'}>
<BadgeText>Chain {state.activeChainId}</BadgeText>
</Badge>
)}
<YStack gap="$3" width="100%">
<XStack justifyContent="space-between" alignItems="center" paddingHorizontal="$1">
<Heading level={4} fontSize="$3">GoodDollar</Heading>
{state.activeChainId && (
<Stack
paddingHorizontal="$2"
paddingVertical="$1"
borderRadius="$full"
borderWidth={1}
borderColor="$borderColor"
backgroundColor="$backgroundHover"
>
<Text fontSize="$1" fontWeight="600" color="$color">
{CHAIN_NAMES[state.activeChainId] ?? `Chain ${state.activeChainId}`}
</Text>
</Stack>
)}
</XStack>
<YStack alignItems="center" width="100%" gap="$2">
<Text fontSize="$2" fontWeight="600" color="$placeholderColor">
Connect identity
</Text>
<Stack height={2} width="100%" backgroundColor="$primary" />
</YStack>
</YStack>
)

const infoCallout = (
<XStack
backgroundColor="$backgroundHover"
padding="$4"
borderRadius="$3"
borderWidth={1}
borderColor="$borderColor"
gap="$3"
alignItems="flex-start"
>
<Icon name="info" size="sm" color="muted" marginTop="$0.5" />
<YStack flex={1} gap="$1">
<Text fontSize="$1" color="$placeholderColor" lineHeight="$3">
You can connect multiple wallet addresses.
</Text>
<Text fontSize="$1" fontWeight="700" color="$color" lineHeight="$3">
However, only one claim per day is available, shared between the connected accounts.
</Text>
</YStack>
</XStack>
)

const footer = (
<Text fontSize="$1" color="$placeholderColor" textAlign="center" paddingVertical="$2">
Supported Networks: Celo, XDC, Fuse
</Text>
)

if (!state.isWalletConnected) {
return (
<WidgetContent data-testid={dataTestId}>
Expand All @@ -45,10 +108,14 @@ export function ConnectAWalletWidgetView({
)
}

const showForm = state.status !== 'error' && !(state.status === 'ready' && state.secondaryAddress)

return (
<WidgetContent data-testid={dataTestId}>
{header}

{infoCallout}

<PrimaryIdentityCard walletAddress={state.walletAddress} />

{!state.isActiveChainSupported && (
Expand All @@ -61,17 +128,17 @@ export function ConnectAWalletWidgetView({

{state.status === 'error' && (
<EmptyStateCard>
<Heading level={5} textAlign="center">
<Heading level={6} fontSize="$1" textAlign="center">
Couldn't load link status
</Heading>
<Alert type="error" message={state.error ?? 'Something went wrong.'} />
<ActionButton onPress={actions.checkSecondaryAddress}>
<ButtonText>Retry</ButtonText>
<ActionButton onPress={actions.checkSecondaryAddress} size="sm">
<ButtonText fontSize="$1">Retry</ButtonText>
</ActionButton>
</EmptyStateCard>
)}

{state.status !== 'error' && (
{showForm && (
<AddressLinkForm
addressInput={state.secondaryAddressInput}
isChecking={state.status === 'checking_address'}
Expand All @@ -88,17 +155,48 @@ export function ConnectAWalletWidgetView({

{state.status === 'ready' && state.secondaryAddress && (
<>
{state.chainLinks.map((row) => (
<ChainLinkRow
key={row.chainId}
row={row}
address={state.secondaryAddress as `0x${string}`}
onConnect={() => actions.connectChain(row.chainId)}
onDisconnect={() => actions.disconnectChain(row.chainId)}
/>
))}
<Heading level={6} marginBottom="$1" fontSize="$1">Connect or Disconnect Address</Heading>
<XStack
backgroundColor="$backgroundHover"
padding="$3"
borderRadius="$3"
flexDirection="row"
alignItems="center"
gap="$4"
borderWidth={1}
borderColor="$borderColor"
>
<YStack
width={20}
height={20}
borderRadius="$full"
backgroundColor="$successMuted"
alignItems="center"
justifyContent="center"
flexShrink={0}
marginLeft={3}
>
<Icon name="check" size="xs" color="success" />
</YStack>
<XStack flex={1} justifyContent="center">
<AddressDisplay address={state.secondaryAddress} size="sm" copyable={false} truncate={true} />
</XStack>
</XStack>

<Card padding="$0" borderRadius="$3" overflow="hidden" borderWidth={1} borderColor="$borderColor">
{state.chainLinks.map((row) => (
<ChainLinkRow
key={row.chainId}
row={row}
onConnect={() => actions.connectChain(row.chainId)}
onDisconnect={() => actions.disconnectChain(row.chainId)}
/>
))}
</Card>
</>
)}

{footer}
</WidgetContent>
)
}
Loading