Skip to content
Merged
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
22 changes: 18 additions & 4 deletions frontend/src/components/InputKeys/PoolListItem/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { OnChainToken, FiatToken, isOnChainToken, OnChainTokenDetails, isFiatToken } from 'shared';
import { useTranslation } from 'react-i18next';
import { CheckIcon } from '@heroicons/react/20/solid';
import { isFiatTokenDisabled, getTokenDisabledReason } from '../../../config/tokenAvailability';
import { useGetAssetIcon } from '../../../hooks/useGetAssetIcon';
import { OnChainToken, FiatToken, isOnChainToken, OnChainTokenDetails } from 'shared';
import { TokenDefinition } from '../SelectionModal';
import { UserBalance } from '../../UserBalance';
interface PoolListItemProps {
Expand All @@ -10,16 +12,22 @@ interface PoolListItemProps {
}

export function PoolListItem({ token, isSelected, onSelect }: PoolListItemProps) {
const { t } = useTranslation();
const tokenIcon = useGetAssetIcon(token.assetIcon);

const showBalance = isOnChainToken(token.type);

const isDisabled = isFiatToken(token.type) && isFiatTokenDisabled(token.type);
const disabledReason = isFiatToken(token.type) && isDisabled ? t(getTokenDisabledReason(token.type)) : undefined;

return (
<button
type="button"
key={token.assetSymbol}
onClick={() => onSelect(token.type)}
className="items-center justify-start w-full gap-4 px-3 py-3 text-left bg-gray-200 border-0 shadow-xs btn hover:opacity-80 hover:bg-gray-300"
onClick={() => !isDisabled && onSelect(token.type)}
className={`items-center justify-start w-full gap-4 px-3 py-3 text-left bg-gray-200 border-0 shadow-xs btn ${
isDisabled ? 'opacity-50 cursor-not-allowed' : 'hover:opacity-80 hover:bg-gray-300'
}`}
>
<span className="relative">
<div className="text-xs">
Expand All @@ -36,7 +44,13 @@ export function PoolListItem({ token, isSelected, onSelect }: PoolListItemProps)
<span className="text-lg leading-5">
<strong>{token.assetSymbol}</strong>
</span>
<span className="text-sm leading-5 text-neutral-500">{token.name || token.assetSymbol}</span>
<span className="text-sm leading-5 text-neutral-500">
{isDisabled ? (
<span className="text-red-500">{disabledReason || 'Unavailable'}</span>
) : (
token.name || token.assetSymbol
)}
</span>
</span>
<span className="text-base">
{showBalance && <UserBalance token={token.details as OnChainTokenDetails} className="font-bold" />}
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/components/InputKeys/SelectionModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
OnChainTokenDetails,
stellarTokenConfig,
} from 'shared';
import { isFiatTokenDisabled } from '../../config/tokenAvailability';

import { useOnchainTokenBalances } from '../../hooks/useOnchainTokenBalances';
import { useNetwork } from '../../contexts/network';
Expand Down Expand Up @@ -69,6 +70,12 @@ function TokenSelectionList() {
const rampDirection = useRampDirection();

const handleTokenSelect = (token: OnChainToken | FiatToken) => {

const isFiatToken = Object.values(FiatToken).includes(token as FiatToken);
if (isFiatToken && isFiatTokenDisabled(token as FiatToken)) {
Comment thread
Sharqiewicz marked this conversation as resolved.
return;
}

if (rampDirection === RampDirection.ONRAMP) {
if (tokenSelectModalType === 'from') {
setFiatToken(token as FiatToken);
Expand Down
1 change: 0 additions & 1 deletion frontend/src/components/Ramp/Onramp/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { RampTerms } from '../../RampTerms';
import { useValidateTerms } from '../../../stores/termsStore';
import { useRampModalActions } from '../../../stores/rampModalStore';
import { useInputAmount, useOnChainToken, useFiatToken } from '../../../stores/ramp/useRampFormStore';
import { useSetRampUrlParams } from '../../../hooks/useRampUrlParams';
import { RampFeeCollapse } from '../../RampFeeCollapse';
import { RampSubmitButtons } from '../../RampSubmitButtons';
import { useInitializeFailedMessage } from '../../../stores/rampStore';
Expand Down
46 changes: 46 additions & 0 deletions frontend/src/config/tokenAvailability.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { FiatToken } from 'shared';
import { DEFAULT_FIAT_TOKEN } from '../stores/ramp/useRampFormStore';

export interface TokenAvailabilityConfig {
enabled: boolean;
disabledReasonTranslationKey: string;
}

// This is our central configuration for token availability
export const fiatTokenAvailability: Record<FiatToken, TokenAvailabilityConfig> = {
[FiatToken.EURC]: {
enabled: false,
disabledReasonTranslationKey: 'pages.swap.error.EURC_tokenUnavailable',
},
[FiatToken.ARS]: {
enabled: true,
disabledReasonTranslationKey: 'pages.swap.error.ARS_tokenUnavailable',
},
[FiatToken.BRL]: {
enabled: true,
disabledReasonTranslationKey: 'pages.swap.error.BRL_tokenUnavailable',
},
};

export function isFiatTokenEnabled(token: FiatToken): boolean {
return fiatTokenAvailability[token]?.enabled ?? false;
}

export function isFiatTokenDisabled(token: FiatToken): boolean {
return fiatTokenAvailability[token]?.enabled === false;
}

export function getTokenDisabledReason(token: FiatToken): string {
return fiatTokenAvailability[token].disabledReasonTranslationKey;
}

export function getEnabledFiatTokens(): FiatToken[] {
return Object.entries(fiatTokenAvailability)
.filter(([_, config]) => config.enabled)
.map(([token]) => token as FiatToken);
}

export function getFirstEnabledFiatToken(): FiatToken {
const enabledTokens = getEnabledFiatTokens();
return enabledTokens.length > 0 ? enabledTokens[0] : DEFAULT_FIAT_TOKEN;
}
10 changes: 8 additions & 2 deletions frontend/src/contexts/events.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createContext } from 'react';
import { PropsWithChildren, useCallback, useContext, useEffect, useRef } from 'react';
import { PriceEndpoints } from 'shared';
import { FiatToken, PriceEndpoints } from 'shared';
import { useVortexAccount } from '../hooks/useVortexAccount';
import { getNetworkId, isNetworkEVM } from 'shared';
import { LocalStorageKeys } from '../hooks/useLocalStorage';
Expand Down Expand Up @@ -117,6 +117,11 @@ export interface InitializationErrorEvent {
error_message: InitializationErrorMessage;
}

export interface TokenUnavailableErrorEvent {
event: 'token_unavailable';
token: FiatToken;
}

type InitializationErrorMessage =
| 'node_connection_issue'
| 'signer_service_issue'
Expand All @@ -138,7 +143,8 @@ export type TrackableEvent =
| TransactionSignedEvent
| ProgressEvent
| NetworkChangeEvent
| InitializationErrorEvent;
| InitializationErrorEvent
| TokenUnavailableErrorEvent;

type EventType = TrackableEvent['event'];

Expand Down
26 changes: 24 additions & 2 deletions frontend/src/hooks/ramp/useRampValidation.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useMemo } from 'react';
import Big from 'big.js';
import {
FiatToken,
FiatTokenDetails,
getAnyFiatTokenDetails,
getOnChainTokenDetailsOrDefault,
Networks,
OnChainTokenDetails,
QuoteEndpoints,
} from 'shared';
import { isFiatTokenDisabled, getTokenDisabledReason } from '../../config/tokenAvailability';

import { useOnchainTokenBalance } from '../useOnchainTokenBalance';
import { useQuoteStore } from '../../stores/ramp/useQuoteStore';
Expand Down Expand Up @@ -132,6 +133,22 @@ function validateOfframp(
return null;
}

function validateTokenAvailability(
t: TFunction<'translation', undefined>,
fiatToken: FiatToken,
trackEvent: (event: TrackableEvent) => void
): string | null {
if (isFiatTokenDisabled(fiatToken)) {
const reason = getTokenDisabledReason(fiatToken);
Comment thread
Sharqiewicz marked this conversation as resolved.
trackEvent({
event: 'token_unavailable',
token: fiatToken,
});
return t(reason)
}
return null;
}

export const useRampValidation = () => {
const { t } = useTranslation();

Expand Down Expand Up @@ -160,6 +177,10 @@ export const useRampValidation = () => {
const getCurrentErrorMessage = useCallback(() => {
if (isDisconnected) return;

// First check if the fiat token is enabled
const tokenAvailabilityError = validateTokenAvailability(t, fiatToken, trackEvent);
if (tokenAvailabilityError) return tokenAvailabilityError;

let validationError = null;

if (isOnramp) {
Expand Down Expand Up @@ -194,6 +215,7 @@ export const useRampValidation = () => {
toToken,
quote,
userInputTokenBalance?.balance,
fiatToken,
]);

return {
Expand Down
29 changes: 23 additions & 6 deletions frontend/src/hooks/useRampUrlParams.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useEffect, useMemo, useRef } from 'react';
import { RampDirection } from '../components/RampToggle';
import { AssetHubToken, EvmToken, FiatToken, Networks, OnChainToken } from 'shared';
import { useRampDirection, useRampDirectionToggle } from '../stores/rampDirectionStore';
import { useRampFormStoreActions } from '../stores/ramp/useRampFormStore';
import { DEFAULT_RAMP_DIRECTION, useRampDirection, useRampDirectionToggle } from '../stores/rampDirectionStore';
import { DEFAULT_ARS_AMOUNT, DEFAULT_BRL_AMOUNT, DEFAULT_EURC_AMOUNT, useRampFormStoreActions } from '../stores/ramp/useRampFormStore';
import { useNetwork } from '../contexts/network';
import { isFiatTokenEnabled, getFirstEnabledFiatToken } from '../config/tokenAvailability';

interface RampUrlParams {
ramp: RampDirection;
Expand All @@ -13,7 +14,7 @@ interface RampUrlParams {
fromAmount?: string;
}

const defaultFiatTokenAmounts: Record<FiatToken, string> = { eurc: '20', ars: '20', brl: '5' };
const defaultFiatTokenAmounts: Record<FiatToken, string> = { eurc: DEFAULT_EURC_AMOUNT, ars: DEFAULT_ARS_AMOUNT, brl: DEFAULT_BRL_AMOUNT };

function findFiatToken(fiatToken?: string): FiatToken | undefined {
if (!fiatToken) {
Expand Down Expand Up @@ -83,7 +84,7 @@ export const useRampUrlParams = (): RampUrlParams => {
const inputAmountParam = params.get('fromAmount');

const ramp =
rampParam === undefined ? rampDirection : rampParam === 'buy' ? RampDirection.ONRAMP : RampDirection.OFFRAMP;
rampParam === undefined ? rampDirection : rampParam === 'sell' ? RampDirection.OFFRAMP : rampParam === 'buy' ? RampDirection.ONRAMP : DEFAULT_RAMP_DIRECTION;

const from =
ramp === RampDirection.OFFRAMP
Expand Down Expand Up @@ -120,17 +121,33 @@ export const useSetRampUrlParams = () => {

const hasInitialized = useRef(false);

const handleFiatToken = (token: FiatToken) => {
if (isFiatTokenEnabled(token)) {
setFiatToken(token);
} else {
setFiatToken(getFirstEnabledFiatToken());
}
};

useEffect(() => {
if (hasInitialized.current) return;

onToggle(ramp);

if (to) {
ramp === RampDirection.OFFRAMP ? setFiatToken(to as FiatToken) : setOnChainToken(to as OnChainToken);
if (ramp === RampDirection.OFFRAMP) {
handleFiatToken(to as FiatToken);
} else {
setOnChainToken(to as OnChainToken);
}
}

if (from) {
ramp === RampDirection.OFFRAMP ? setOnChainToken(from as OnChainToken) : setFiatToken(from as FiatToken);
if (ramp === RampDirection.OFFRAMP) {
setOnChainToken(from as OnChainToken);
} else {
handleFiatToken(from as FiatToken);
}
}

if (fromAmount) {
Expand Down
9 changes: 7 additions & 2 deletions frontend/src/stores/ramp/useRampFormStore.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { create } from 'zustand';
import { EvmToken, FiatToken, Networks, OnChainToken } from 'shared';

export const DEFAULT_FIAT_TOKEN = FiatToken.BRL;
export const DEFAULT_BRL_AMOUNT = '5';
export const DEFAULT_EURC_AMOUNT = '20';
export const DEFAULT_ARS_AMOUNT = '20';

interface RampFormState {
inputAmount?: string;
onChainToken: OnChainToken;
Expand All @@ -23,9 +28,9 @@ interface RampFormActions {
const storedNetwork = localStorage.getItem('SELECTED_NETWORK')

export const DEFAULT_RAMP_FORM_STORE_VALUES: RampFormState = {
inputAmount: '20',
inputAmount: DEFAULT_BRL_AMOUNT,
onChainToken: storedNetwork !== null && storedNetwork === Networks.AssetHub ? EvmToken.USDC : EvmToken.USDT,
fiatToken: FiatToken.EURC,
fiatToken: DEFAULT_FIAT_TOKEN,
taxId: undefined,
pixId: undefined,
};
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/stores/rampDirectionStore.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { create } from 'zustand';
import { RampDirection } from '../components/RampToggle';

export const DEFAULT_RAMP_DIRECTION = RampDirection.ONRAMP;

export interface RampDirectionStore {
activeDirection: RampDirection;
onToggle: (direction: RampDirection) => void;
}

export const useRampDirectionStore = create<RampDirectionStore>((set) => ({
activeDirection: RampDirection.ONRAMP,
activeDirection: DEFAULT_RAMP_DIRECTION,
onToggle: (direction: RampDirection) => set({ activeDirection: direction }),
}));

Expand Down
3 changes: 3 additions & 0 deletions frontend/src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@
},
"swap": {
"error": {
"EURC_tokenUnavailable": "Improving your EUR exit - back shortly! ",
"ARS_tokenUnavailable": "Improving your ARS exit - back shortly! ",
"BRL_tokenUnavailable": "Improving your BRL exit - back shortly! ",
"initializeFailed": {
"default": "We're experiencing a digital traffic jam. Please hold tight while we clear the road and get things moving again!",
"noAddress": "No address found.",
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/translations/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@
},
"swap": {
"error": {
"EURC_tokenUnavailable": "Ajustando sua saída EUR - em breve!",
"ARS_tokenUnavailable": "Ajustando sua saída ARS - em breve!",
"BRL_tokenUnavailable": "Ajustando sua saída BRL - em breve!",
"initializeFailed": {
"default": "Estamos enfrentando um congestionamento digital. Por favor, aguarde enquanto liberamos o caminho e fazemos as coisas funcionarem novamente!",
"noAddress": "Nenhum endereço encontrado.",
Expand All @@ -63,7 +66,7 @@
},
"insufficientFunds": "Saldo insuficiente. Seu saldo é {{userInputTokenBalance}} {{assetSymbol}}",
"moreThanMaximumWithdrawal": {
"sell": "O valor máximo de compra é {{maxAmountUnits}} {{assetSymbol}}.",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think the problem here is not a duplicate but the same key name, ie both being called "sell". Please re-add the key and call it "buy" instead.

"buy": "O valor máximo de compra é {{maxAmountUnits}} {{assetSymbol}}.",
"sell": "O valor máximo de venda é {{maxAmountUnits}} {{assetSymbol}}."
},
"lessThanMinimumWithdrawal": {
Expand Down
Loading