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
15 changes: 15 additions & 0 deletions src/shared/core/useIsOrbyEnabled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useIsOrbySupportedChain } from '@orb-labs/orby-react';
import { useMemo } from 'react';
import _ from 'lodash';
import { useIsOneClickTransactionsAndGasAbstractionEnabled } from './useIsOneClickTransactionsAndGasAbstractionEnabled';

export function useIsOrbyEnabled(chainId?: bigint | undefined) {
const oneClickTransactionsAndGasAbstractionEnabled =
useIsOneClickTransactionsAndGasAbstractionEnabled();

const { isSupported } = useIsOrbySupportedChain(chainId);

return useMemo(() => {
return oneClickTransactionsAndGasAbstractionEnabled && isSupported;
}, [oneClickTransactionsAndGasAbstractionEnabled, isSupported]);
}
89 changes: 89 additions & 0 deletions src/ui/components/TokenStateSection/TokenStateSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React from 'react';
import { Surface } from 'src/ui/ui-kit/Surface';
import { UIText } from 'src/ui/ui-kit/UIText';
import { VStack } from 'src/ui/ui-kit/VStack';
import { HStack } from 'src/ui/ui-kit/HStack';
import type { State } from '@orb-labs/orby-core';
import { TokenIcon } from 'src/ui/ui-kit/TokenIcon';
import { Media } from 'src/ui/ui-kit/Media';
import { formatTokenValue } from 'src/shared/units/formatTokenValue';
import { AssetQuantity } from 'src/ui/components/AssetQuantity';
import { BigNumber } from 'bignumber.js';

export function TokenStateSection({
title,
tokens,
}: {
title: string;
tokens: State | undefined;
}) {
const fungibleTokens = tokens?.getFungibleTokens();
if (!fungibleTokens) {
return null;
}

if (fungibleTokens.length === 0) {
return (
<Surface>
<VStack gap={12} style={{ paddingBlock: 24, paddingLeft: 16 }}>
<UIText kind="small/regular" color="var(--neutral-500)">
{title}
</UIText>
<UIText kind="body/regular" color="white">
No tokens moving
</UIText>
</VStack>
</Surface>
);
}

return (
<Surface>
<VStack gap={12}>
<UIText kind="small/regular" color="var(--neutral-500)">
{title}
</UIText>
{fungibleTokens.map((tokenAmount, index) => {
// Convert raw amount to decimal based on token decimals
const rawAmount = BigNumber(tokenAmount.toRawAmount().toString());
const decimals = tokenAmount.token.currency().decimals;
const decimalAmount = rawAmount.dividedBy(
BigNumber(10).pow(decimals)
);

return (
<Media
key={`${tokenAmount.token.identifier()}-${index}`}
gap={12}
vGap={0}
image={
<TokenIcon
size={36}
src={tokenAmount.token.currency().logoUrl}
symbol={tokenAmount.token.currency().symbol}
title={tokenAmount.token.currency().name}
/>
}
text={
<UIText kind="headline/h3">
<HStack gap={4} alignItems="center">
<AssetQuantity commonQuantity={decimalAmount} />
<span>{tokenAmount.token.currency().symbol}</span>
</HStack>
</UIText>
}
detailText={
<UIText kind="small/regular" color="var(--neutral-500)">
{formatTokenValue(
decimalAmount,
tokenAmount.token.currency().symbol
)}
</UIText>
}
/>
);
})}
</VStack>
</Surface>
);
}
1 change: 1 addition & 0 deletions src/ui/components/TokenStateSection/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { TokenStateSection } from './TokenStateSection';
157 changes: 156 additions & 1 deletion src/ui/pages/SendTransaction/NetworkFee/NetworkFee.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,26 @@ import type { IncomingTransactionWithFrom } from 'src/modules/ethereum/types/Inc
import { useNetworks } from 'src/modules/networks/useNetworks';
import { useCurrency } from 'src/modules/currency/useCurrency';
import { formatCurrencyValueExtra } from 'src/shared/units/formatCurrencyValue';
import type { StandardizedBalance } from '@orb-labs/orby-core';
import { TokenIcon } from 'src/ui/ui-kit/TokenIcon';
import { BottomSheetDialog } from 'src/ui/ui-kit/ModalDialogs/BottomSheetDialog';
import { DialogTitle } from 'src/ui/ui-kit/ModalDialogs/DialogTitle';
import { SurfaceList } from 'src/ui/ui-kit/SurfaceList';
import { VStack } from 'src/ui/ui-kit/VStack';
import { createPortal } from 'react-dom';
import { getRootDomNode } from 'src/ui/shared/getRootDomNode';
import DownIcon from 'jsx:src/ui/assets/chevron-down.svg';
import { useIsOrbyEnabled } from 'src/shared/core/useIsOrbyEnabled';
import type { TransactionFee } from '../TransactionConfiguration/useTransactionFee';
import { NetworkFeeDialog } from './NetworkFeeDialog';
import { NETWORK_SPEED_TO_TITLE } from './constants';
import { NetworkFeeDialog } from './NetworkFeeDialog';

export interface GasTokenInput {
name: string;
standardizedTokenId?: string;
isDefault: boolean;
url?: string;
}

function getFeeTypeTitle(
type: Exclude<keyof ChainGasPrice['fast'], 'eta'> | undefined
Expand All @@ -32,6 +49,128 @@ function getFeeTypeTitle(
return labels[type];
}

export function GasTokenSelector({
selectedGasToken,
setSelectedGasToken,
fungibleTokens,
}: {
selectedGasToken?: GasTokenInput;
setSelectedGasToken?: (gasToken?: GasTokenInput) => void;
fungibleTokens?: StandardizedBalance[] | undefined;
}) {
const dialogRef = useRef<HTMLDialogElementInterface | null>(null);
const rootNode = getRootDomNode();

const handleSelectGasToken = (token: StandardizedBalance) => {
const gasToken: GasTokenInput = {
name: token.total.currency.symbol,
standardizedTokenId: token.standardizedTokenId,
isDefault: false,
url: token.total.currency.logoUrl,
};
setSelectedGasToken?.(gasToken);
dialogRef.current?.close();
};

const handleSelectDefault = () => {
setSelectedGasToken?.({
name: 'Native Token',
standardizedTokenId: undefined,
isDefault: true,
});
dialogRef.current?.close();
};

const items = [
{
key: 'default',
component: (
<HStack gap={8} alignItems="center">
<TokenIcon
size={20}
src={undefined}
symbol="No Gas Abstraction"
title="No Gas Abstraction"
/>
<UIText kind="body/regular">No Gas Abstraction</UIText>
</HStack>
),
onClick: handleSelectDefault,
isInteractive: true,
},
...(fungibleTokens
? fungibleTokens.map((token, index) => ({
key: token.standardizedTokenId || `token-${index}`,
component: (
<HStack gap={8} alignItems="center">
<TokenIcon
size={20}
src={token.total.currency.logoUrl}
symbol={token.total.currency.symbol}
title={token.total.currency.symbol}
/>
<UIText kind="body/regular">{token.total.currency.symbol}</UIText>
</HStack>
),
onClick: () => handleSelectGasToken(token),
isInteractive: true,
}))
: []),
];

return (
<>
{createPortal(
<BottomSheetDialog
ref={dialogRef}
height="70vh"
containerStyle={{ paddingTop: 16 }}
>
<DialogTitle
title={<UIText kind="headline/h3">Select Gas Token</UIText>}
alignTitle="start"
/>
<VStack gap={8} style={{ marginTop: 16 }}>
<SurfaceList items={items} />
</VStack>
</BottomSheetDialog>,
rootNode
)}

<UnstyledButton
type="button"
className={helperStyles.hoverUnderline}
style={{
color: 'var(--primary)',
cursor: 'pointer',
}}
onClick={() => {
dialogRef.current?.showModal();
}}
>
<HStack gap={8} alignItems="center">
<TokenIcon
size={16}
src={selectedGasToken?.url}
symbol={selectedGasToken?.name || 'TOKEN'}
title={selectedGasToken?.name}
/>
<UIText kind="small/accent">
{selectedGasToken?.name || 'Select Token'}
</UIText>
<DownIcon
width={16}
height={16}
style={{
color: 'var(--primary)',
}}
/>
</HStack>
</UnstyledButton>
</>
);
}

export function NetworkFee({
transaction,
transactionFee,
Expand All @@ -43,6 +182,9 @@ export function NetworkFee({
label,
renderDisplayValue,
displayValueEnd,
selectedGasToken,
setSelectedGasToken,
fungibleTokens,
}: {
transaction: IncomingTransactionWithFrom;
transactionFee: TransactionFee;
Expand All @@ -57,9 +199,15 @@ export function NetworkFee({
displayValue: string;
}) => React.ReactNode;
displayValueEnd?: React.ReactNode;
selectedGasToken?: GasTokenInput;
setSelectedGasToken?: (gasToken?: GasTokenInput) => void;
fungibleTokens?: StandardizedBalance[] | undefined;
}) {
const { networks } = useNetworks();
const { currency } = useCurrency();
const isOrbyEnabled = useIsOrbyEnabled(
transaction.chainId ? BigInt(transaction.chainId) : undefined
);
const dialogRef = useRef<HTMLDialogElementInterface | null>(null);
const { time, feeEstimation, feeEstimationQuery, costs, costsQuery } =
transactionFee;
Expand Down Expand Up @@ -138,6 +286,13 @@ export function NetworkFee({
) : (
<UIText kind="small/regular">Network Fee</UIText>
)}
{selectedGasToken && isOrbyEnabled ? (
<GasTokenSelector
selectedGasToken={selectedGasToken}
setSelectedGasToken={setSelectedGasToken}
fungibleTokens={fungibleTokens}
/>
) : null}
{isLoading ? (
<CircleSpinner />
) : displayValue ? (
Expand Down
Loading
Loading