Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

/send checks UI mvp #643

Open
wants to merge 7 commits into
base: dev
Choose a base branch
from
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
50 changes: 50 additions & 0 deletions apps/next/pages/checks/claim.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { ClaimSendCheckPayload } from 'app/features/checks/types'
import { ClaimSendCheck } from 'app/features/checks/components/claim/ClaimSendCheck'
import { decodeClaimCheckUrl } from 'app/features/checks/utils/checkUtils'
import Head from 'next/head'
import { useEffect, useState } from 'react'
import { Text, XStack, useToastController } from '@my/ui'
import { useRouter } from 'solito/router'

export const ClaimSendCheckPage = (props: ClaimSendCheckPayload) => {
const [claimCheckPayload, setClaimCheckPayload] = useState<ClaimSendCheckPayload>()

const toast = useToastController()
const router = useRouter()

useEffect(() => {
const payload = window.location.hash.substring(1)
const claimCheckPayload: ClaimSendCheckPayload = decodeClaimCheckUrl(payload)
setClaimCheckPayload(claimCheckPayload)
}, [])

const onSuccess = () => {
toast.show('Successfully claimed /send check', { type: 'success' })
setTimeout(() => {
router.push('/')
}, 1000)
}

const onError = (e: Error) => {
console.log(e)
}

return (
<>
<Head>
<title>Send | Claim Send Check</title>
</Head>
<XStack justifyContent="center">
<Text mt="$4" fontWeight="bold" position="absolute" fontSize="$12">
/send
</Text>
</XStack>

{claimCheckPayload && (
<ClaimSendCheck onSuccess={onSuccess} onError={onError} payload={claimCheckPayload} />
)}
</>
)
}

export default ClaimSendCheckPage
20 changes: 20 additions & 0 deletions apps/next/pages/checks/create.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Head from 'next/head'
import { userProtectedGetSSP } from 'utils/userProtected'
import type { NextPageWithLayout } from '../_app'
import { CreateSendCheck } from 'app/features/checks/components/create/CreateSendCheck'
import { CoinField } from 'app/components/FormFields'

export const CreateSendCheckPage: NextPageWithLayout = () => {
return (
<>
<Head>
<title>Send | Checks</title>
</Head>
<CreateSendCheck />
<CoinField />
</>
)
}

export const getServerSideProps = userProtectedGetSSP()
export default CreateSendCheckPage
8 changes: 5 additions & 3 deletions packages/app/components/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { SchemaForm } from 'app/utils/SchemaForm'
import { shorten } from 'app/utils/strings'
import { useSearchResultHref } from 'app/utils/useSearchResultHref'
import * as Linking from 'expo-linking'
import { useEffect, useState } from 'react'
import { type PropsWithChildren, useEffect, useState } from 'react'
import { FormProvider } from 'react-hook-form'
import { Link } from 'solito/link'
import { useRouter } from 'solito/router'
Expand All @@ -45,7 +45,7 @@ const formatResultsKey = (str: string): string => {
return str.replace(/_matches/g, '').replace(/_/g, ' ')
}

function SearchResults() {
function SearchResults(props: PropsWithChildren) {
const { results, isLoading, error } = useTagSearch()
const [queryParams] = useRootScreenParams()
const { search: query } = queryParams
Expand All @@ -58,8 +58,10 @@ function SearchResults() {
</YStack>
)
}

// if there are no results or an error, show the children
if (!results || error) {
return null
return props.children
}

if (isAddress(query ?? '')) {
Expand Down
16 changes: 16 additions & 0 deletions packages/app/features/checks/components/ManageChecksBtn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Button, ButtonText } from '@my/ui'
import { useRouter } from 'solito/router'

export const ManageChecksBtn = () => {
const router = useRouter()

const onPress = () => {
router.push('/checks')
}

return (
<Button onPress={onPress} theme="green" px="$15" br={12} disabledStyle={{ opacity: 0.5 }}>
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
<Button onPress={onPress} theme="green" px="$15" br={12} disabledStyle={{ opacity: 0.5 }}>
import { useLink } from 'solito/link'
// ...
<Button {...useLink({ href: '/checks' })} theme="green" px="$15" br={12} disabledStyle={{ opacity: 0.5 }}>

Copy link
Collaborator

Choose a reason for hiding this comment

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

We also have a <LinkableButton/> Component that does the same thing
https://github.com/0xsend/sendapp/blob/dev/packages/ui/src/components/LinkableButton.tsx

<ButtonText textTransform="uppercase">Manage checks</ButtonText>
</Button>
)
}
145 changes: 145 additions & 0 deletions packages/app/features/checks/components/claim/ClaimSendCheck.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import type { ClaimSendCheckPayload } from 'app/features/checks/types'
import { ClaimButtonGuest } from 'app/features/checks/components/claim/btn/ClaimButtonGuest'
import { ClaimButtonUser } from 'app/features/checks/components/claim/btn/ClaimButtonUser'
import { ShowCheckData } from 'app/features/checks/components/claim/check/check-data/ShowCheckData'
import { useProfileLookup } from 'app/utils/useProfileLookup'
import { Spinner, YStack, Text, Button, ButtonText, XStack, Card, Label } from '@my/ui'
import { useMemo, useState } from 'react'
import { useSendCheckData } from 'app/features/checks/utils/useSendCheckData'
import { IconError } from 'app/components/icons'
import { useRouter } from 'next/navigation'
import { useCoinGeckoTokenId, useTokenMarketData } from 'app/utils/coin-gecko'
import { checkExists } from 'app/features/checks/utils/checkUtils'
import { GreenSquare } from 'app/features/home/TokenBalanceCard'

interface Props {
payload: ClaimSendCheckPayload
onSuccess: () => void
onError: (error: Error) => void
}

export const ClaimSendCheck = (props: Props) => {
const [error, setError] = useState<Error>()

const {
data: sendCheckData,
isLoading: sendCheckDataLoading,
isSuccess,
} = useSendCheckData(props.payload.ephemeralKeypair.ephemeralAddress, {
retry: 3,
})
const { data: tokenId } = useCoinGeckoTokenId(sendCheckData?.token as `0x${string}`, {
retry: 3,
})
const { data: tokenData } = useTokenMarketData(tokenId)

const { data: profileData } = useProfileLookup('sendid', props.payload.senderSendId)

const router = useRouter()
const isError = !!error
const signedIn = !!profileData

useMemo(() => {
if (isSuccess && !checkExists(sendCheckData)) {
setError(new Error("Check doesn't exist or has been claimed already."))
}
}, [isSuccess, sendCheckData])

const showSpinner = () => {
return (
<Spinner
animation="medium"
enterStyle={{ opacity: 1 }}
exitStyle={{ opacity: 0 }}
color="$primary"
size="large"
/>
)
}

const showError = (error: Error) => {
return (
<YStack gap="$4" animation="medium" enterStyle={{ opacity: 1 }} exitStyle={{ opacity: 0 }}>
<XStack justifyContent="center" alignItems="center" gap="$2">
<IconError size={50} alignSelf="center" color="red" />
<Text fontSize="$9" color="$black" textAlign="center">
Unable to claim check
</Text>
</XStack>
<Text fontSize="$7" color="$darkGrayTextField" textAlign="center">
{error.message ?? 'Please try again'}
</Text>
<Button backgroundColor="$primary" onPress={() => router.refresh()}>
<ButtonText fontSize="$6">Try again</ButtonText>
</Button>
</YStack>
)
}

const showHeader = () => {
return (
<XStack gap="$2.5" ai="center" jc="center">
<GreenSquare />
<Label
fontSize={'$4'}
zIndex={1}
fontWeight={'500'}
textTransform={'uppercase'}
col={'$color10'}
>
/send Check
</Label>
</XStack>
)
}

const showCheckData = () => {
return (
<Card
$lg={{ bc: 'transparent' }}
px="$12"
py="$6"
w={'100%'}
jc="space-between"
br={12}
gap="$6"
maxWidth={'40%'}
my="auto"
mx="auto"
>
<YStack gap="$3">
{showHeader()}
{sendCheckData && (
<ShowCheckData
sendCheckData={sendCheckData}
tokenMetadata={{
name: tokenData?.[0]?.name as string,
imageUrl: tokenData?.[0]?.image as string,
coinGeckoTokenId: tokenId as string,
}}
senderSendId={props.payload.senderSendId}
onError={props.onError}
/>
)}
</YStack>
{signedIn ? (
<ClaimButtonUser
onError={onError}
onSuccess={props.onSuccess}
payload={props.payload}
tokenId={tokenId}
tokenAmount={sendCheckData?.amount}
/>
) : (
<ClaimButtonGuest tokenId={tokenId} tokenAmount={sendCheckData?.amount} />
)}
</Card>
)
}

const onError = (error: Error) => {
setError(error)
}

return isError ? showError(error) : sendCheckDataLoading ? showSpinner() : showCheckData()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Button, ButtonText, Spinner, XStack } from '@my/ui'
import { CheckValue } from 'app/features/checks/components/claim/check/check-data/CheckValue'
import { useState } from 'react'

export interface ClaimSendCheckBtnProps {
tokenId?: string
tokenAmount?: bigint
}

interface Props extends ClaimSendCheckBtnProps {
onPress: () => void
}

export const ClaimButton = (props: Props) => {
const [loading, setLoading] = useState<boolean>(false)
const onPress = () => {
setLoading(true)
props.onPress()
}

const showCheckValue = () => {
return (
props.tokenAmount &&
props.tokenId && <CheckValue tokenId={props.tokenId} tokenAmount={props.tokenAmount} />
)
}
return (
<Button onPress={onPress} backgroundColor="$primary">
<XStack justifyContent="center" alignItems="center" gap="$2">
{!loading && (
<>
<ButtonText fontSize="$6">CLAIM</ButtonText>
{showCheckValue()}
</>
)}
{loading && <Spinner size="large" color="black" />}
</XStack>
</Button>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {
ClaimButton,
type ClaimSendCheckBtnProps,
} from 'app/features/checks/components/claim/btn/ClaimButton'
import { useRouter } from 'solito/router'

interface Props extends ClaimSendCheckBtnProps {}

export const ClaimButtonGuest = (props: Props) => {
const router = useRouter()

const onPress = () => {
router.push('/auth/onboarding')
}

return <ClaimButton tokenId={props.tokenId} tokenAmount={props.tokenAmount} onPress={onPress} />
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {
ClaimButton,
type ClaimSendCheckBtnProps,
} from 'app/features/checks/components/claim/btn/ClaimButton'
import type { ClaimSendCheckPayload } from 'app/features/checks/types'
import { useClaimSendCheck } from 'app/features/checks/utils/useClaimSendCheck'

interface Props extends ClaimSendCheckBtnProps {
payload: ClaimSendCheckPayload
onSuccess: () => void
onError: (error: Error) => void
}

export const ClaimButtonUser = (props: Props) => {
const claimSendCheck = useClaimSendCheck(props.payload)
const claimCheck = () => {
claimSendCheck()
.then((receipt) => {
if (!receipt.success) {
props.onError(new Error('Error claiming send check'))
return
}
props.onSuccess()
})
.catch((error) => {
props.onError(error)
})
}

const onPress = () => {
claimCheck()
}

return <ClaimButton tokenId={props.tokenId} tokenAmount={props.tokenAmount} onPress={onPress} />
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { XStack, Text } from '@my/ui'
import { TokenIcon } from 'app/features/checks/components/claim/check/check-data/TokenIcon'

interface Props {
tokenAmount: bigint
tokenName: string
tokenIconSize: number
tokenImageUrl?: string
}

export const CheckTokenAmount = (props: Props) => {
const showTokenIcon = () => {
if (props.tokenImageUrl) {
return (
<TokenIcon
tokenImageUrl={props.tokenImageUrl}
tokenName={props.tokenName}
tokenIconSize={props.tokenIconSize}
/>
)
}
}
return (
<XStack gap="$3" justifyContent="center" alignItems="center">
{showTokenIcon()}
<Text fontSize="$9" fontWeight="bold">
{props.tokenAmount.toLocaleString()} {props.tokenName}
</Text>
</XStack>
)
}
Loading
Loading