Skip to content

Commit 6ed7a67

Browse files
Show pending transfers from temporal in UI
1 parent 3b38cb0 commit 6ed7a67

File tree

13 files changed

+359
-218
lines changed

13 files changed

+359
-218
lines changed

CONTRIBUTING.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,13 @@ Here is a quick peek at the send stack. Quickly jump to any of the submodules by
3636
│   ├── <a href="https://github.com/0xsend/sendapp/tree/main/packages/daimo-expo-passkeys">daimo-expo-passkeys</a>
3737
│   ├── <a href="https://github.com/0xsend/sendapp/tree/main/packages/eslint-config-custom">eslint-config-customs</a>
3838
│   ├── <a href="https://github.com/0xsend/sendapp/tree/main/packages/playwright">playwright</a>
39+
| ├── <a href="https://github.com/0xsend/sendapp/tree/main/packages/shovel">shovel</a>
40+
│   ├── <a href="https://github.com/0xsend/sendapp/tree/main/packages/snaplet">snaplet</a>
41+
| ├── <a href="https://github.com/0xsend/sendapp/tree/main/packages/temporal">temporal</a>
3942
│   ├── <a href="https://github.com/0xsend/sendapp/tree/main/packages/ui">ui</a>
4043
│   ├── <a href="https://github.com/0xsend/sendapp/tree/main/packages/wagmi">wagmi</a>
41-
│   └── <a href="https://github.com/0xsend/sendapp/tree/main/packages/webauthn-authenticator">webauthn-authenticator</a>
44+
| ├── <a href="https://github.com/0xsend/sendapp/tree/main/packages/webauthn-authenticator">webauthn-authenticator</a>
45+
│   └── <a href="https://github.com/0xsend/sendapp/tree/main/packages/workflows">workflows</a>
4246
└── <a href="https://github.com/0xsend/sendapp/tree/main/supabase">supabase</a>
4347
</code>
4448
</pre>

packages/api/src/routers/transfer.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export const transferRouter = createTRPCRouter({
6161
const states: transferState[] = []
6262
const client = await getTemporalClient()
6363
const workflows = client.workflow.list({
64-
query: `ExecutionStatus = "Running" AND WorkflowId BETWEEN "transfer-workflow-${token}-${sender}-" AND "transfer-workflow-${token}-${sender}-~"`,
64+
query: `ExecutionStatus = "Running" AND WorkflowId BETWEEN "send-transfer-workflow-${token}-${sender}-" AND "send-transfer-workflow-${token}-${sender}-~"`,
6565
})
6666
for await (const workflow of workflows) {
6767
const handle = client.workflow.getHandle(workflow.workflowId)
@@ -89,7 +89,7 @@ export const transferRouter = createTRPCRouter({
8989
const states: transferState[] = []
9090
const client = await getTemporalClient()
9191
const workflows = client.workflow.list({
92-
query: `ExecutionStatus = "Failed" AND WorkflowId BETWEEN "transfer-workflow-${token}-${sender}-" AND "transfer-workflow-${token}-${sender}-~"`,
92+
query: `ExecutionStatus = "Failed" AND WorkflowId BETWEEN "send-transfer-workflow-${token}-${sender}-" AND "send-transfer-workflow-${token}-${sender}-~"`,
9393
})
9494
for await (const workflow of workflows) {
9595
const handle = client.workflow.getHandle(workflow.workflowId)

packages/app/features/home/TokenActivityRow.tsx

+92-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Paragraph, Text, XStack, YStack } from '@my/ui'
1+
import { Avatar, LinkableAvatar, Spinner, Paragraph, Text, XStack, YStack, Stack } from '@my/ui'
22
import { amountFromActivity, eventNameFromActivity, subtextFromActivity } from 'app/utils/activity'
33
import {
44
isSendAccountReceiveEvent,
@@ -8,8 +8,13 @@ import {
88
import { ActivityAvatar } from '../activity/ActivityAvatar'
99
import { CommentsTime } from 'app/utils/dateHelper'
1010
import { Link } from 'solito/link'
11-
11+
import type { CoinWithBalance } from 'app/data/coins'
1212
import { useUser } from 'app/utils/useUser'
13+
import type { transferState } from '@my/workflows'
14+
import { sendAccountAbi, erc20Abi } from '@my/wagmi'
15+
import { decodeFunctionData, formatUnits } from 'viem'
16+
import { useProfileLookup } from 'app/utils/useProfileLookup'
17+
import formatAmount from 'app/utils/formatAmount'
1318

1419
export function TokenActivityRow({ activity }: { activity: Activity }) {
1520
const { profile } = useUser()
@@ -81,3 +86,88 @@ export function TokenActivityRow({ activity }: { activity: Activity }) {
8186
</XStack>
8287
)
8388
}
89+
90+
export function PendingTransferActivityRow({
91+
coin,
92+
state,
93+
}: { coin: CoinWithBalance; state: transferState }) {
94+
const { userOp } = state
95+
const { args } = decodeFunctionData({ abi: sendAccountAbi, data: userOp.callData })
96+
97+
const decodedTokenTransfer =
98+
args?.[0]?.[0].data !== '0x'
99+
? decodeFunctionData({ abi: erc20Abi, data: args?.[0]?.[0].data })
100+
: undefined
101+
102+
const amount = decodedTokenTransfer
103+
? formatUnits(decodedTokenTransfer.args[1] as bigint, coin.decimals)
104+
: formatAmount(formatUnits(args?.[0]?.[0].value, 18), 5, 5)
105+
106+
const to = decodedTokenTransfer ? decodedTokenTransfer.args[0] : args?.[0]?.[0].dest
107+
108+
const { data: profile } = useProfileLookup('address', to)
109+
110+
return (
111+
<XStack
112+
width={'100%'}
113+
ai="center"
114+
jc="space-between"
115+
gap="$4"
116+
borderBottomWidth={1}
117+
pb="$5"
118+
borderBottomColor={'$decay'}
119+
$gtMd={{ borderBottomWidth: 0, pb: '0' }}
120+
>
121+
<XStack gap="$4.5" width={'100%'} f={1}>
122+
<LinkableAvatar size="$4.5" br="$4" gap="$2" href={`/profile/${profile?.sendid}`}>
123+
<Avatar.Image src={profile?.avatar_url ?? undefined} />
124+
<Avatar.Fallback jc="center" bc="$olive">
125+
<Avatar size="$4.5" br="$4">
126+
<Avatar.Image
127+
src={`https://ui-avatars.com/api/?name=${
128+
profile?.tag ?? profile?.sendid
129+
}&size=256&format=png&background=86ad7f`}
130+
/>
131+
</Avatar>
132+
</Avatar.Fallback>
133+
</LinkableAvatar>
134+
<YStack gap="$1.5" width={'100%'} f={1} overflow="hidden">
135+
<XStack fd="row" jc="space-between" gap="$1.5" f={1} width={'100%'}>
136+
<Text
137+
color="$color12"
138+
fontSize="$7"
139+
$gtMd={{ fontSize: '$5' }}
140+
textTransform="capitalize"
141+
>
142+
Sending...
143+
</Text>
144+
<Text color="$color12" fontSize="$7">
145+
{`${amount} ${coin.symbol}`}
146+
</Text>
147+
</XStack>
148+
<Stack
149+
gap="$1.5"
150+
fd="column"
151+
$gtSm={{ fd: 'row' }}
152+
alignItems="flex-start"
153+
justifyContent="space-between"
154+
width="100%"
155+
overflow="hidden"
156+
f={1}
157+
>
158+
<Text
159+
theme="alt2"
160+
color="$olive"
161+
fontFamily={'$mono'}
162+
maxWidth={'100%'}
163+
overflow={'hidden'}
164+
>
165+
{profile?.name ?? profile?.tag ?? profile?.sendid}
166+
</Text>
167+
<Spinner size="small" color={'$color12'} />
168+
</Stack>
169+
</YStack>
170+
</XStack>
171+
</XStack>
172+
)
173+
}
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,134 @@
1-
import { Button, Card, Label, Paragraph, Spinner } from '@my/ui'
1+
import { Button, Card, Label, Paragraph, Spinner, YStack } from '@my/ui'
22
import type { CoinWithBalance } from 'app/data/coins'
3-
import { hexToBytea } from 'app/utils/hexToBytea'
4-
import { Fragment } from 'react'
3+
import { Fragment, useEffect, useState } from 'react'
54
import { useTokenActivityFeed } from './utils/useTokenActivityFeed'
6-
import { AnimateEnter } from './TokenDetails'
7-
import { TokenActivityRow } from './TokenActivityRow'
5+
import { RowLabel, AnimateEnter } from './TokenDetails'
6+
import { PendingTransferActivityRow, TokenActivityRow } from './TokenActivityRow'
7+
import { useSendAccount } from 'app/utils/send-accounts'
8+
import { zeroAddress } from 'viem'
89

910
export const TokenDetailsHistory = ({ coin }: { coin: CoinWithBalance }) => {
10-
const result = useTokenActivityFeed({
11+
const { data: sendAccount } = useSendAccount()
12+
const [hasPendingTransfers, setHasPendingTransfers] = useState<boolean | undefined>(true)
13+
const { pendingTransfers, activityFeed } = useTokenActivityFeed({
14+
address: sendAccount?.address ?? zeroAddress,
15+
token: coin.token,
1116
pageSize: 10,
12-
address: coin.token === 'eth' ? undefined : hexToBytea(coin.token),
17+
refetchInterval: 1000,
18+
enabled:
19+
(hasPendingTransfers === undefined || hasPendingTransfers) &&
20+
sendAccount?.address !== undefined,
1321
})
22+
23+
const { data: pendingTransfersData, isError: pendingTransfersError } = pendingTransfers
24+
1425
const {
15-
data,
26+
data: activityFeedData,
1627
isLoading: isLoadingActivities,
1728
error: activitiesError,
1829
isFetching: isFetchingActivities,
1930
isFetchingNextPage: isFetchingNextPageActivities,
2031
fetchNextPage,
2132
hasNextPage,
22-
} = result
23-
const { pages } = data ?? {}
24-
if (isLoadingActivities) return <Spinner size="small" />
33+
} = activityFeed
34+
35+
const { pages } = activityFeedData ?? {}
36+
37+
// Check if there are any pending transfers in the temporal db. If not set hasPendingTransfers to false to control refetches
38+
useEffect(() => {
39+
if (Array.isArray(pendingTransfersData)) {
40+
setHasPendingTransfers(pendingTransfersData?.length > 0)
41+
} else if (pendingTransfersError) {
42+
setHasPendingTransfers(false)
43+
} else {
44+
setHasPendingTransfers(undefined)
45+
}
46+
}, [pendingTransfersData, pendingTransfersError])
47+
2548
return (
2649
<>
2750
<Label fontSize={'$6'} fontWeight={'500'} color="$color12">
2851
{!pages || !pages[0]?.length ? 'No Activity' : 'Activity'}
2952
</Label>
30-
{(() => {
31-
switch (true) {
32-
case activitiesError !== null:
33-
return (
34-
<Paragraph maxWidth={'600'} fontFamily={'$mono'} fontSize={'$5'} color={'$color12'}>
35-
{activitiesError?.message.split('.').at(0) ?? `${activitiesError}`}
36-
</Paragraph>
37-
)
38-
case !pages || !pages[0]?.length:
39-
return null
40-
default: {
41-
let lastDate: string | undefined
42-
return (
43-
<Card gap="$5" testID="TokenDetailsHistory" p="$5">
44-
{pages?.map((activities) => {
45-
return activities.map((activity) => {
46-
const date = activity.created_at.toLocaleDateString()
47-
const isNewDate = !lastDate || date !== lastDate
48-
if (isNewDate) {
49-
lastDate = date
50-
}
51-
return (
52-
<Fragment
53-
key={`${activity.event_name}-${activity.created_at}-${activity?.from_user?.id}-${activity?.to_user?.id}`}
54-
>
55-
<AnimateEnter>
56-
<TokenActivityRow activity={activity} />
57-
</AnimateEnter>
58-
</Fragment>
59-
)
60-
})
61-
})}
62-
<AnimateEnter>
63-
{!isLoadingActivities && (isFetchingNextPageActivities || hasNextPage) ? (
64-
<>
65-
{isFetchingNextPageActivities && <Spinner size="small" color={'$color12'} />}
66-
{hasNextPage && (
67-
<Button
68-
onPress={() => {
69-
fetchNextPage()
70-
}}
71-
disabled={isFetchingNextPageActivities || isFetchingActivities}
72-
color="$color0"
73-
width={200}
74-
mx="auto"
75-
mb="$6"
76-
bc="$color10"
53+
54+
<YStack gap="$5" testID="TokenDetailsHistory">
55+
{hasPendingTransfers && (
56+
<>
57+
<RowLabel>Pending Transfers</RowLabel>
58+
<AnimateEnter>
59+
{pendingTransfersData?.map((state) => (
60+
<Fragment key={`${state.userOp.nonce}-pending`}>
61+
<AnimateEnter>
62+
<PendingTransferActivityRow coin={coin} state={state} />
63+
</AnimateEnter>
64+
</Fragment>
65+
))}
66+
</AnimateEnter>
67+
</>
68+
)}
69+
70+
{(() => {
71+
switch (true) {
72+
case activitiesError !== null:
73+
return (
74+
<Paragraph maxWidth={'600'} fontFamily={'$mono'} fontSize={'$5'} color={'$color12'}>
75+
{activitiesError?.message.split('.').at(0) ?? `${activitiesError}`}
76+
</Paragraph>
77+
)
78+
case !pages || !pages[0]?.length:
79+
return null
80+
default: {
81+
let lastDate: string | undefined
82+
return (
83+
<Card gap="$5" testID="TokenDetailsHistory" p="$5">
84+
{pages?.map((activities) => {
85+
return activities.map((activity) => {
86+
const date = activity.created_at.toLocaleDateString()
87+
const isNewDate = !lastDate || date !== lastDate
88+
if (isNewDate) {
89+
lastDate = date
90+
}
91+
return (
92+
<Fragment
93+
key={`${activity.event_name}-${activity.created_at}-${activity?.from_user?.id}-${activity?.to_user?.id}`}
7794
>
78-
Load More
79-
</Button>
80-
)}
81-
</>
82-
) : null}
83-
</AnimateEnter>
84-
</Card>
85-
)
95+
<AnimateEnter>
96+
<TokenActivityRow activity={activity} />
97+
</AnimateEnter>
98+
</Fragment>
99+
)
100+
})
101+
})}
102+
<AnimateEnter>
103+
{!isLoadingActivities && (isFetchingNextPageActivities || hasNextPage) ? (
104+
<>
105+
{isFetchingNextPageActivities && (
106+
<Spinner size="small" color={'$color12'} />
107+
)}
108+
{hasNextPage && (
109+
<Button
110+
onPress={() => {
111+
fetchNextPage()
112+
}}
113+
disabled={isFetchingNextPageActivities || isFetchingActivities}
114+
color="$color0"
115+
width={200}
116+
mx="auto"
117+
mb="$6"
118+
bc="$color10"
119+
>
120+
Load More
121+
</Button>
122+
)}
123+
</>
124+
) : null}
125+
</AnimateEnter>
126+
</Card>
127+
)
128+
}
86129
}
87-
}
88-
})()}
130+
})()}
131+
</YStack>
89132
</>
90133
)
91134
}

packages/app/features/home/__snapshots__/TokenDetails.test.tsx.snap

+21
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,27 @@ exports[`TokenDetails 1`] = `
352352
}
353353
testID="TokenDetailsHistory"
354354
>
355+
<Text
356+
accessibilityRole="header"
357+
style={
358+
{
359+
"color": "#B2B2B2",
360+
"display": "none",
361+
"fontFamily": "System",
362+
"fontSize": 19.2,
363+
"fontWeight": "400",
364+
"lineHeight": 21,
365+
"marginBottom": 0,
366+
"marginLeft": 0,
367+
"marginRight": 0,
368+
"marginTop": 13,
369+
"userSelect": "auto",
370+
}
371+
}
372+
suppressHighlighting={true}
373+
>
374+
6/2/2024
375+
</Text>
355376
<View
356377
collapsable={false}
357378
forwardedRef={[Function]}

packages/app/features/home/screen.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export function HomeScreen() {
140140
<Paragraph theme="red_alt1">No send account found</Paragraph>
141141
</Stack>
142142
)
143-
case search !== undefined:
143+
case search !== undefined: //@todo remove this
144144
return <SendSearchBody />
145145
default:
146146
return (

0 commit comments

Comments
 (0)