mobile-app/app/screens/AppNavigator/screens/Auctions/screens/AuctionDetailScreen.tsx
import { useState, useEffect } from "react";
import { View } from "react-native";
import { useSelector, batch as reduxBatch } from "react-redux";
import { NumericFormat as NumberFormat } from "react-number-format";
import BigNumber from "bignumber.js";
import * as Progress from "react-native-progress";
import {
ThemedIcon,
ThemedTextV2,
ThemedViewV2,
ThemedScrollViewV2,
ThemedTouchableOpacityV2,
} from "@components/themed";
import { getColor, tailwind } from "@tailwind";
import { translate } from "@translations";
import {
NavigationProp,
useNavigation,
useIsFocused,
} from "@react-navigation/native";
import { StackScreenProps } from "@react-navigation/stack";
import { RootState } from "@store";
import { fetchAuctions, fetchBidHistory } from "@store/auctions";
import { useBottomSheet } from "@hooks/useBottomSheet";
import { LoanVaultLiquidationBatch } from "@defichain/whale-api-client/dist/api/loan";
import { ButtonV2 } from "@components/ButtonV2";
import { TextRowV2 } from "@components/TextRowV2";
import { NumberRowV2 } from "@components/NumberRowV2";
import { PriceRateProps, PricesSection } from "@components/PricesSection";
import { useAppDispatch } from "@hooks/useAppDispatch";
import { useWalletContext } from "@shared-contexts/WalletContext";
import { useWhaleApiClient } from "@waveshq/walletkit-ui/dist/contexts";
import { useThemeContext } from "@waveshq/walletkit-ui";
import { BidInfo } from "@screens/AppNavigator/screens/Auctions/components/BatchCard";
import { TokenIconGroupV2 } from "@components/TokenIconGroupV2";
import { useAuctionTime } from "../hooks/AuctionTimeLeft";
import { useAuctionBidValue } from "../hooks/AuctionBidValue";
import {
getPrecisedCurrencyValue,
getPrecisedTokenValue,
} from "../helpers/precision-token-value";
import { useTokenPrice } from "../../Portfolio/hooks/TokenPrice";
import { AuctionVaultDetails } from "../components/AuctionVaultDetails";
import { AuctionsParamList } from "../AuctionNavigator";
type BatchDetailScreenProps = StackScreenProps<
AuctionsParamList,
"AuctionDetailScreen"
>;
export function AuctionDetailScreen(
props: BatchDetailScreenProps
): JSX.Element {
const { batch: batchFromParam, vault } = props.route.params;
const [batch, setBatch] = useState<LoanVaultLiquidationBatch>(batchFromParam);
const { address } = useWalletContext();
const { isLight } = useThemeContext();
const navigation = useNavigation<NavigationProp<AuctionsParamList>>();
const client = useWhaleApiClient();
const dispatch = useAppDispatch();
const { auctions } = useSelector((state: RootState) => state.auctions);
const blockCount = useSelector((state: RootState) => state.block.count) ?? 0;
const bidHistory = useSelector(
(state: RootState) => state.auctions.bidHistory
);
const { minNextBidInToken, totalCollateralsValueInUSD, minNextBidInUSD } =
useAuctionBidValue(batch, vault.liquidationPenalty);
const {
timeRemaining,
blocksRemaining,
normalizedBlocks,
timeRemainingThemedColor,
} = useAuctionTime(vault.liquidationHeight, blockCount);
const isFocused = useIsFocused();
const { containerRef } = useBottomSheet();
const { getTokenPrice } = useTokenPrice();
useEffect(() => {
if (isFocused) {
reduxBatch(() => {
dispatch(fetchAuctions({ client }));
dispatch(
fetchBidHistory({
vaultId: vault.vaultId,
liquidationHeight: vault.liquidationHeight,
batchIndex: batch.index,
client: client,
size: 200,
})
);
});
}
}, [blockCount, isFocused]);
useEffect(() => {
const _vault = auctions.find(
(auction) => auction.vaultId === vault.vaultId
);
if (_vault === undefined) {
return;
}
const _batch = _vault.batches.find(
(batch) => batch.index === batchFromParam.index
);
if (_batch === undefined) {
return;
}
setBatch(_batch);
}, [auctions]);
const onPlaceBid = (): void => {
navigation.navigate("PlaceBidScreen", { batch, vault });
};
const onBidHistory = (): void => {
navigation.navigate("BidHistoryScreen", { batch, vault });
};
const totalPrecisedCollateralsValue = getPrecisedTokenValue(
totalCollateralsValueInUSD
);
const precisedMinNextBidInUSD = getPrecisedTokenValue(minNextBidInUSD);
// List of collaterals
const loanCollaterals: PriceRateProps[] = batch.collaterals.map(
(collateral) => {
const value = getTokenPrice(
collateral.symbol,
new BigNumber(collateral.amount)
);
return {
label: collateral.displaySymbol,
bSymbol: collateral.displaySymbol,
value: collateral.amount,
symbolUSDValue: value,
usdTextStyle: tailwind("text-sm"),
};
}
);
const timeRemainingThemedProps = {
light: tailwind(`text-${timeRemainingThemedColor} text-xs font-normal-v2`),
dark: tailwind(`text-${timeRemainingThemedColor} text-xs font-normal-v2`),
};
const isHighestBidder = batch.highestBid?.owner === address;
const isOutbid =
batch.highestBid?.owner !== address && batch.froms.includes(address);
const displayBidStatus = isHighestBidder || isOutbid;
return (
<View style={tailwind("flex-1")} ref={containerRef}>
<ThemedScrollViewV2
contentContainerStyle={tailwind("pt-8 px-5 pb-14")}
style={tailwind("flex-1")}
testID="auction_details_screen"
>
{/* Loan collaterals summary */}
<View style={tailwind("flex flex-row justify-between")}>
<ThemedTextV2
style={tailwind("text-xs font-normal-v2")}
light={tailwind("text-mono-light-v2-500")}
dark={tailwind("text-mono-dark-v2-500")}
testID="text_auction_detail_collaterals"
>
{translate("components/AuctionDetailScreen", "Collaterals")}
</ThemedTextV2>
{displayBidStatus && (
<BidInfo
hasFirstBid={bidHistory.length > 0}
isOutBid={isOutbid}
isHighestBidder={isHighestBidder}
testID="auction_detail_bid_status"
/>
)}
</View>
<View style={tailwind("flex-row items-center mt-2")}>
<TokenIconGroupV2
testID="auction_detail_collateral_group"
size={36}
symbols={batch.collaterals.map(
(collateral) => collateral.displaySymbol
)}
maxIconToDisplay={6}
/>
<NumberFormat
displayType="text"
prefix="$"
renderText={(value: string) => (
<ThemedTextV2
light={tailwind("text-mono-light-v2-1000")}
dark={tailwind("text-mono-dark-v2-1000")}
style={[
tailwind("font-semibold-v2 text-right text-xl"),
{
marginLeft:
loanCollaterals.length === 1
? 8
: -((loanCollaterals.length - 1) * 16 - 8),
},
]}
>
{value}
</ThemedTextV2>
)}
thousandSeparator
value={getPrecisedCurrencyValue(totalPrecisedCollateralsValue)}
/>
</View>
{/* Loan collaterals Breakdown */}
<ThemedViewV2
light={tailwind("border-mono-light-v2-300")}
dark={tailwind("border-mono-dark-v2-300")}
style={tailwind("pt-5 px-5 mt-8 border-0.5 rounded-lg-v2")}
>
<PricesSection
priceRates={loanCollaterals}
testID="auction_detail_loan_collaterals"
/>
<ThemedViewV2
style={tailwind("py-5 border-t-0.5")}
light={tailwind("border-mono-light-v2-300")}
dark={tailwind("border-mono-dark-v2-300")}
>
<NumberRowV2
lhs={{
value: translate(
"components/AuctionDetailScreen",
"Total value"
),
testID: "auction_detail_total_label",
themedProps: {
light: tailwind("text-mono-light-v2-500"),
dark: tailwind("text-mono-dark-v2-500"),
},
}}
rhs={{
value: totalPrecisedCollateralsValue,
testID: "auction_detail_total_value",
prefix: "$",
themedProps: {
style: tailwind("font-normal-v2 text-sm"),
light: tailwind("text-mono-light-v2-900"),
dark: tailwind("text-mono-dark-v2-900"),
},
usdTextStyle: tailwind("text-sm"),
}}
/>
</ThemedViewV2>
</ThemedViewV2>
{/* Remaining time progress bar */}
<View style={tailwind("mt-8")}>
<Progress.Bar
progress={normalizedBlocks.toNumber()}
color={getColor(timeRemainingThemedColor)}
unfilledColor={getColor(
isLight ? "mono-light-v2-200" : "mono-dark-v2-200"
)}
borderWidth={0}
width={null}
/>
<TextRowV2
containerStyle={{
style: tailwind(
"flex-row items-start w-full bg-transparent mt-3"
),
light: tailwind("bg-transparent border-mono-light-v2-300"),
dark: tailwind("bg-transparent border-mono-dark-v2-300"),
}}
lhs={{
value: translate(
"components/AuctionDetailScreen",
timeRemaining ? "Time remaining" : "No time remaining"
),
testID: "text_time_remaining_label",
themedProps: timeRemainingThemedProps,
}}
rhs={{
value: `${timeRemaining || "0s"} (${blocksRemaining} Blks)`,
testID: "text_time_remaining_value",
themedProps: timeRemainingThemedProps,
}}
/>
</View>
{/* Vault info */}
<ThemedViewV2
style={tailwind("border-t-0.5 mt-12")}
light={tailwind("bg-transparent border-mono-light-v2-300")}
dark={tailwind("bg-transparent border-mono-dark-v2-300")}
>
<AuctionVaultDetails
testID="auction_details"
vault={vault}
showLinkToVault
/>
<NumberRowV2
containerStyle={{
style: tailwind(
"flex-row items-start w-full bg-transparent mt-6"
),
light: tailwind("bg-transparent border-mono-light-v2-300"),
dark: tailwind("bg-transparent border-mono-dark-v2-300"),
}}
lhs={{
value: translate(
"components/AuctionDetailScreen",
"Min. next bid"
),
testID: "min_next_bid_label",
themedProps: {
light: tailwind("text-mono-light-v2-500"),
dark: tailwind("text-mono-dark-v2-500"),
},
}}
rhs={{
value: minNextBidInToken.toNumber(),
testID: "min_next_bid_amount",
prefixSymbol: batch.loan.displaySymbol,
suffix: ` ${batch.loan.displaySymbol}`,
usdAmount: new BigNumber(precisedMinNextBidInUSD),
usdTextStyle: tailwind("text-sm"),
themedProps: {
style: tailwind("font-semibold-v2 text-base"),
light: tailwind("text-mono-light-v2-900 "),
dark: tailwind("text-mono-dark-v2-900"),
},
}}
/>
</ThemedViewV2>
{/* Bid History button */}
{bidHistory.length > 0 && (
<ThemedTouchableOpacityV2
light={tailwind("bg-mono-light-v2-00")}
dark={tailwind("bg-mono-dark-v2-00")}
style={tailwind(
"border-0 py-4.5 px-5 mt-7 mb-5 flex-row items-center justify-between rounded-lg-v2"
)}
onPress={onBidHistory}
testID="auction_detail_bid_history_btn"
>
<ThemedTextV2
style={tailwind("font-normal-v2 text-sm text-red-v2")}
>
{translate(
"components/AuctionDetailScreen",
"Bid History ({{bidHistoryCount}})",
{ bidHistoryCount: bidHistory.length }
)}
</ThemedTextV2>
<ThemedIcon
iconType="MaterialIcons"
light={tailwind("text-mono-light-v2-700")}
dark={tailwind("text-mono-dark-v2-700")}
name="access-time"
size={20}
testID="auction_detail_bid_history_btn_icon"
/>
</ThemedTouchableOpacityV2>
)}
{/* Bid button and auction close message */}
<View style={tailwind("mt-7")}>
{blocksRemaining === 0 && (
<ThemedTextV2
light={tailwind("text-red-v2")}
dark={tailwind("text-red-v2")}
style={tailwind(
"text-red-v2 text-center text-xs font-normal-v2 mb-6"
)}
>
{translate("components/AuctionDetailScreen", "Auction closed")}
</ThemedTextV2>
)}
<ButtonV2
fillType="fill"
label={translate("components/AuctionDetailScreen", "Bid")}
disabled={blocksRemaining === 0}
styleProps="mx-7"
onPress={onPlaceBid}
testID="auction_detail_place_bid_btn"
/>
</View>
</ThemedScrollViewV2>
</View>
);
}