Skip to content

Commit

Permalink
feat: notify users when arbitrum bridge claim is ready (#7876)
Browse files Browse the repository at this point in the history
* chore: make arbitrum bridge claim status hook not rely on tx history

* feat: placeholder toast

* feat: update design of claims notification

* feat: redirect to claim route on notification click

* feat: close the notification when clicked

* fix: app crash due to provider shenanigans

* fix: glitchiness when opening claims

* fix: consolidate slide transitions around trade page

* chore: skip better than isDisabled

* feat: stop polling arbitrum bridge txs when they are known to be executed

* fix: re-enable arbitrum bridge tx polling when wallet changes
  • Loading branch information
woodenfurniture authored Oct 7, 2024
1 parent 3cd8986 commit e78c095
Show file tree
Hide file tree
Showing 9 changed files with 238 additions and 156 deletions.
3 changes: 3 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useSelector } from 'react-redux'
import { Routes } from 'Routes/Routes'
import { ConsentBanner } from 'components/ConsentBanner'
import { IconCircle } from 'components/IconCircle'
import { useBridgeClaimNotification } from 'hooks/useBridgeClaimNotification/useBridgeClaimNotification'
import { useHasAppUpdated } from 'hooks/useHasAppUpdated/useHasAppUpdated'
import { useModal } from 'hooks/useModal/useModal'
import { selectShowConsentBanner, selectShowWelcomeModal } from 'state/slices/selectors'
Expand All @@ -26,6 +27,8 @@ export const App = () => {
const showConsentBanner = useSelector(selectShowConsentBanner)
const { isOpen: isNativeOnboardOpen, open: openNativeOnboard } = useModal('nativeOnboard')

useBridgeClaimNotification()

const handleCtaClick = useCallback(() => window.location.reload(), [])

useEffect(() => {
Expand Down
4 changes: 3 additions & 1 deletion src/assets/translations/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -802,7 +802,9 @@
"pendingClaims": "Pending",
"noPendingClaims": "No claims pending",
"availableIn": "Available in %{time}",
"confirmAndClaim": "Confirm & Claim"
"confirmAndClaim": "Confirm & Claim",
"viewClaims": "View Claims",
"availableClaimsNotification": "You have bridge claims ready!"
},
"trade": {
"trade": "Trade",
Expand Down
21 changes: 19 additions & 2 deletions src/components/MultiHopTrade/MultiHopTrade.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { AssetId } from '@shapeshiftoss/caip'
import { AnimatePresence } from 'framer-motion'
import { memo, useEffect, useMemo, useRef } from 'react'
import { memo, useEffect, useMemo, useRef, useState } from 'react'
import { FormProvider, useForm } from 'react-hook-form'
import { MemoryRouter, Route, Switch, useLocation, useParams } from 'react-router-dom'
import { selectAssetById } from 'state/slices/assetsSlice/selectors'
Expand All @@ -10,6 +10,7 @@ import { useAppDispatch, useAppSelector } from 'state/store'

import { MultiHopTradeConfirm } from './components/MultiHopTradeConfirm/MultiHopTradeConfirm'
import { QuoteListRoute } from './components/QuoteList/QuoteListRoute'
import { Claim } from './components/TradeInput/components/Claim/Claim'
import { TradeInput } from './components/TradeInput/TradeInput'
import { VerifyAddresses } from './components/VerifyAddresses/VerifyAddresses'
import { useGetTradeQuotes } from './hooks/useGetTradeQuotes/useGetTradeQuotes'
Expand All @@ -20,6 +21,7 @@ const TradeRouteEntries = [
TradeRoutePaths.Confirm,
TradeRoutePaths.VerifyAddresses,
TradeRoutePaths.QuoteList,
TradeRoutePaths.Claim,
]

export type TradeCardProps = {
Expand All @@ -39,22 +41,34 @@ const GetTradeQuotes = () => {
}

export const MultiHopTrade = memo(({ defaultBuyAssetId, isCompact }: TradeCardProps) => {
const location = useLocation()
const dispatch = useAppDispatch()
const methods = useForm({ mode: 'onChange' })
const { assetSubId, chainId } = useParams<MatchParams>()
const [initialIndex, setInitialIndex] = useState<number | undefined>()

const routeBuyAsset = useAppSelector(state => selectAssetById(state, `${chainId}/${assetSubId}`))
const defaultBuyAsset = useAppSelector(state => selectAssetById(state, defaultBuyAssetId ?? ''))

// Handle deep linked route from other pages
useEffect(() => {
if (initialIndex !== undefined) return
const incomingIndex = TradeRouteEntries.indexOf(location.pathname as TradeRoutePaths)
setInitialIndex(incomingIndex === -1 ? 0 : incomingIndex)
}, [initialIndex, location])

useEffect(() => {
dispatch(tradeInput.actions.clear())
if (routeBuyAsset) dispatch(tradeInput.actions.setBuyAsset(routeBuyAsset))
else if (defaultBuyAsset) dispatch(tradeInput.actions.setBuyAsset(defaultBuyAsset))
}, [defaultBuyAsset, defaultBuyAssetId, dispatch, routeBuyAsset])

// Prevent default behavior overriding deep linked route
if (initialIndex === undefined) return null

return (
<FormProvider {...methods}>
<MemoryRouter initialEntries={TradeRouteEntries} initialIndex={0}>
<MemoryRouter initialEntries={TradeRouteEntries} initialIndex={initialIndex}>
<TradeRoutes isCompact={isCompact} />
</MemoryRouter>
</FormProvider>
Expand Down Expand Up @@ -107,6 +121,9 @@ const TradeRoutes = memo(({ isCompact }: TradeRoutesProps) => {
width={tradeInputRef.current?.offsetWidth ?? 'full'}
/>
</Route>
<Route key={TradeRoutePaths.Claim} path={TradeRoutePaths.Claim}>
<Claim isCompact={isCompact} />
</Route>
</Switch>
</AnimatePresence>
{/* Stop polling for quotes by unmounting the hook. This prevents trade execution getting */}
Expand Down
157 changes: 78 additions & 79 deletions src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import {
import { MessageOverlay } from 'components/MessageOverlay/MessageOverlay'
import { getMixpanelEventData } from 'components/MultiHopTrade/helpers'
import { useReceiveAddress } from 'components/MultiHopTrade/hooks/useReceiveAddress'
import { TradeSlideTransition } from 'components/MultiHopTrade/TradeSlideTransition'
import { TradeInputTab, TradeRoutePaths } from 'components/MultiHopTrade/types'
import { SlideTransition } from 'components/SlideTransition'
import { WalletActions } from 'context/WalletProvider/actions'
import { useErrorHandler } from 'hooks/useErrorToast/useErrorToast'
import { useWallet } from 'hooks/useWallet/useWallet'
Expand All @@ -43,7 +43,6 @@ import { useAppDispatch, useAppSelector } from 'state/store'
import { breakpoints } from 'theme/theme'

import { useAccountIds } from '../../hooks/useAccountIds'
import { Claim } from './components/Claim/Claim'
import { CollapsibleQuoteList } from './components/CollapsibleQuoteList'
import { ConfirmSummary } from './components/ConfirmSummary'
import { TradeInputBody } from './components/TradeInputBody'
Expand Down Expand Up @@ -73,7 +72,6 @@ export const TradeInput = ({ isCompact, tradeInputRef }: TradeInputProps) => {
const mixpanel = getMixPanel()
const history = useHistory()
const { showErrorToast } = useErrorHandler()
const [selectedTab, setSelectedTab] = useState<TradeInputTab>(TradeInputTab.Trade)
const [isConfirmationLoading, setIsConfirmationLoading] = useState(false)
const [shouldShowWarningAcknowledgement, setShouldShowWarningAcknowledgement] = useState(false)
const [shouldShowStreamingAcknowledgement, setShouldShowStreamingAcknowledgement] =
Expand Down Expand Up @@ -210,6 +208,15 @@ export const TradeInput = ({ isCompact, tradeInputRef }: TradeInputProps) => {

const handleFormSubmit = useMemo(() => handleSubmit(onSubmit), [handleSubmit, onSubmit])

const handleChangeTab = useCallback(
(newTab: TradeInputTab) => {
if (newTab === TradeInputTab.Claim) {
history.push(TradeRoutePaths.Claim)
}
},
[history],
)

// If the warning acknowledgement is shown, we need to handle the submit differently because we might want to show the streaming acknowledgement
const handleWarningAcknowledgementSubmit = useCallback(() => {
if (activeQuote?.isStreaming && isEstimatedExecutionTimeOverThreshold)
Expand Down Expand Up @@ -249,84 +256,76 @@ export const TradeInput = ({ isCompact, tradeInputRef }: TradeInputProps) => {
})()

return (
<TradeSlideTransition>
<MessageOverlay show={isKeplr} title={overlayTitle}>
<Flex
width='full'
justifyContent='center'
maxWidth={isCompact || isSmallerThanXl ? '500px' : undefined}
>
<Center width='inherit'>
<Card flex={1} width='full' maxWidth='500px' ref={tradeInputRef}>
<ArbitrumBridgeAcknowledgement
<MessageOverlay show={isKeplr} title={overlayTitle}>
<Flex
width='full'
justifyContent='center'
maxWidth={isCompact || isSmallerThanXl ? '500px' : undefined}
>
<Center width='inherit'>
<Card flex={1} width='full' maxWidth='500px' ref={tradeInputRef}>
<ArbitrumBridgeAcknowledgement
onAcknowledge={handleFormSubmit}
shouldShowAcknowledgement={shouldShowArbitrumBridgeAcknowledgement}
setShouldShowAcknowledgement={setShouldShowArbitrumBridgeAcknowledgement}
>
<StreamingAcknowledgement
onAcknowledge={handleFormSubmit}
shouldShowAcknowledgement={shouldShowArbitrumBridgeAcknowledgement}
setShouldShowAcknowledgement={setShouldShowArbitrumBridgeAcknowledgement}
shouldShowAcknowledgement={shouldShowStreamingAcknowledgement}
setShouldShowAcknowledgement={setShouldShowStreamingAcknowledgement}
estimatedTimeMs={
tradeQuoteStep?.estimatedExecutionTimeMs
? tradeQuoteStep.estimatedExecutionTimeMs
: 0
}
>
<StreamingAcknowledgement
onAcknowledge={handleFormSubmit}
shouldShowAcknowledgement={shouldShowStreamingAcknowledgement}
setShouldShowAcknowledgement={setShouldShowStreamingAcknowledgement}
estimatedTimeMs={
tradeQuoteStep?.estimatedExecutionTimeMs
? tradeQuoteStep.estimatedExecutionTimeMs
: 0
}
<WarningAcknowledgement
message={warningAcknowledgementMessage}
onAcknowledge={handleWarningAcknowledgementSubmit}
shouldShowAcknowledgement={shouldShowWarningAcknowledgement}
setShouldShowAcknowledgement={setShouldShowWarningAcknowledgement}
>
<WarningAcknowledgement
message={warningAcknowledgementMessage}
onAcknowledge={handleWarningAcknowledgementSubmit}
shouldShowAcknowledgement={shouldShowWarningAcknowledgement}
setShouldShowAcknowledgement={setShouldShowWarningAcknowledgement}
>
<Stack spacing={0} as='form' onSubmit={handleTradeQuoteConfirm}>
<TradeInputHeader
initialTab={selectedTab}
onChangeTab={setSelectedTab}
isLoading={isLoading}
isCompact={isCompact}
/>
{selectedTab === TradeInputTab.Trade && (
<Box ref={bodyRef}>
<TradeInputBody
isLoading={isLoading}
manualReceiveAddress={manualReceiveAddress}
initialSellAssetAccountId={initialSellAssetAccountId}
initialBuyAssetAccountId={initialBuyAssetAccountId}
setSellAssetAccountId={setSellAssetAccountId}
setBuyAssetAccountId={setBuyAssetAccountId}
/>
<ConfirmSummary
isCompact={isCompact}
isLoading={isLoading}
receiveAddress={manualReceiveAddress ?? walletReceiveAddress}
/>
</Box>
)}
{selectedTab === TradeInputTab.Claim && <Claim />}
</Stack>
</WarningAcknowledgement>
</StreamingAcknowledgement>
</ArbitrumBridgeAcknowledgement>
</Card>

<WithLazyMount
shouldUse={!isCompact && !isSmallerThanXl}
component={CollapsibleQuoteList}
isOpen={
selectedTab === TradeInputTab.Trade &&
!isCompact &&
!isSmallerThanXl &&
hasUserEnteredAmount
}
isLoading={isLoading}
width={tradeInputRef.current?.offsetWidth ?? 'full'}
height={totalHeight ?? 'full'}
ml={4}
/>
</Center>
</Flex>
</MessageOverlay>
</TradeSlideTransition>
<Stack spacing={0} as='form' onSubmit={handleTradeQuoteConfirm}>
<TradeInputHeader
initialTab={TradeInputTab.Trade}
onChangeTab={handleChangeTab}
isLoading={isLoading}
isCompact={isCompact}
/>
<SlideTransition>
<Box ref={bodyRef}>
<TradeInputBody
isLoading={isLoading}
manualReceiveAddress={manualReceiveAddress}
initialSellAssetAccountId={initialSellAssetAccountId}
initialBuyAssetAccountId={initialBuyAssetAccountId}
setSellAssetAccountId={setSellAssetAccountId}
setBuyAssetAccountId={setBuyAssetAccountId}
/>
<ConfirmSummary
isCompact={isCompact}
isLoading={isLoading}
receiveAddress={manualReceiveAddress ?? walletReceiveAddress}
/>
</Box>
</SlideTransition>
</Stack>
</WarningAcknowledgement>
</StreamingAcknowledgement>
</ArbitrumBridgeAcknowledgement>
</Card>

<WithLazyMount
shouldUse={!isCompact && !isSmallerThanXl}
component={CollapsibleQuoteList}
isOpen={!isCompact && !isSmallerThanXl && hasUserEnteredAmount}
isLoading={isLoading}
width={tradeInputRef.current?.offsetWidth ?? 'full'}
height={totalHeight ?? 'full'}
ml={4}
/>
</Center>
</Flex>
</MessageOverlay>
)
}
Original file line number Diff line number Diff line change
@@ -1,53 +1,35 @@
import { Card } from '@chakra-ui/react'
import type { TxStatus } from '@shapeshiftoss/unchained-client'
import { AnimatePresence } from 'framer-motion'
import { lazy, Suspense, useCallback, useState } from 'react'
import { MemoryRouter, Route, Switch, useLocation } from 'react-router'
import { makeSuspenseful } from 'utils/makeSuspenseful'
import { useCallback, useState } from 'react'
import { MemoryRouter, Route, Switch, useHistory, useLocation } from 'react-router'
import { TradeInputTab, TradeRoutePaths } from 'components/MultiHopTrade/types'

import { TradeInputHeader } from '../TradeInputHeader'
import { ClaimConfirm } from './ClaimConfirm'
import { ClaimSelect } from './ClaimSelect'
import { ClaimStatus } from './ClaimStatus'
import type { ClaimDetails } from './hooks/useArbitrumClaimsByStatus'
import { ClaimRoutePaths } from './types'

const ClaimSelect = makeSuspenseful(
lazy(() =>
import('./ClaimSelect').then(({ ClaimSelect }) => ({
default: ClaimSelect,
})),
),
)

const ClaimConfirm = makeSuspenseful(
lazy(() =>
import('./ClaimConfirm').then(({ ClaimConfirm }) => ({
default: ClaimConfirm,
})),
),
)

const ClaimStatus = makeSuspenseful(
lazy(() =>
import('./ClaimStatus').then(({ ClaimStatus }) => ({
default: ClaimStatus,
})),
),
)

const ClaimRouteEntries = [ClaimRoutePaths.Select, ClaimRoutePaths.Confirm, ClaimRoutePaths.Status]

export const Claim: React.FC = () => {
return (
<MemoryRouter initialEntries={ClaimRouteEntries} initialIndex={0}>
<ClaimRoutes />
</MemoryRouter>
)
}

export const ClaimRoutes: React.FC = () => {
export const Claim = ({ isCompact }: { isCompact?: boolean }) => {
const location = useLocation()
const history = useHistory()

const [activeClaim, setActiveClaim] = useState<ClaimDetails | undefined>()
const [claimTxHash, setClaimTxHash] = useState<string | undefined>()
const [claimTxStatus, setClaimTxStatus] = useState<TxStatus | undefined>()

const handleChangeTab = useCallback(
(newTab: TradeInputTab) => {
if (newTab === TradeInputTab.Trade) {
history.push(TradeRoutePaths.Input)
}
},
[history],
)

const renderClaimSelect = useCallback(() => {
return <ClaimSelect setActiveClaim={setActiveClaim} />
}, [])
Expand Down Expand Up @@ -79,9 +61,15 @@ export const ClaimRoutes: React.FC = () => {
}, [activeClaim, claimTxHash, claimTxStatus])

return (
<AnimatePresence mode='wait' initial={false}>
<MemoryRouter initialEntries={ClaimRouteEntries} initialIndex={0}>
<Switch location={location}>
<Suspense>
<Card flex={1} width='full' maxWidth='500px'>
<TradeInputHeader
initialTab={TradeInputTab.Claim}
onChangeTab={handleChangeTab}
isLoading={false}
isCompact={isCompact}
/>
<Route
key={ClaimRoutePaths.Select}
path={ClaimRoutePaths.Select}
Expand All @@ -97,8 +85,8 @@ export const ClaimRoutes: React.FC = () => {
path={ClaimRoutePaths.Status}
render={renderClaimStatus}
/>
</Suspense>
</Card>
</Switch>
</AnimatePresence>
</MemoryRouter>
)
}
Loading

0 comments on commit e78c095

Please sign in to comment.