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
7 changes: 7 additions & 0 deletions src/apps/gas-tank/assets/gas-tank-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/apps/gas-tank/assets/history.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
136 changes: 136 additions & 0 deletions src/apps/gas-tank/components/App/AppWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { useMemo, useState, useEffect } from 'react';
import { HomeScreen } from './HomeScreen';
import { useGasTankBalance } from '../../../pulse/hooks/useGasTankBalance';
import Search from '../../../pulse/components/Search/Search';
import {
useGetWalletPortfolioQuery,
convertPortfolioAPIResponseToToken,
} from '../../../../services/pillarXApiWalletPortfolio';
import { SelectedToken } from '../../../pulse/types/tokens';
import { MobulaChainNames } from '../../../pulse/utils/constants';
import useTransactionKit from '../../../../hooks/useTransactionKit';

/**
* AppWrapper component
* Main wrapper for the Gas Tank app
* Follows the same pattern as Pulse AppWrapper
*/
export const AppWrapper: React.FC = () => {
const { walletAddress: accountAddress } = useTransactionKit();
const {
totalBalance,
isLoading: isBalanceLoading,
refetch: refetchBalance,
} = useGasTankBalance(accountAddress || null);

// Smart loading state to prevent flickering on refetch
const [hasInitialLoad, setHasInitialLoad] = useState(false);

useEffect(() => {
if (!isBalanceLoading) {
setHasInitialLoad(true);
}
}, [isBalanceLoading]);

// Show loading only on initial fetch
const displayLoading = isBalanceLoading && !hasInitialLoad;

// State management
const [searching, setSearching] = useState(false);
const [isBuy, setIsBuy] = useState(true);
const [chains, setChains] = useState<MobulaChainNames>(MobulaChainNames.All);
const [buyToken, setBuyToken] = useState<SelectedToken | null>(null);
const [sellToken, setSellToken] = useState<SelectedToken | null>(null);
const [topupToken, setTopupToken] = useState<SelectedToken | null>(null);
const [isSearchingFromTopup, setIsSearchingFromTopup] = useState(false);
const [onboardingScreen, setOnboardingScreen] = useState<
'welcome' | 'topup' | null
>(null);

// Fetch wallet portfolio
const {
data: walletPortfolioData,
isLoading: walletPortfolioLoading,
isFetching: walletPortfolioFetching,
error: walletPortfolioError,
refetch: refetchWalletPortfolio,
} = useGetWalletPortfolioQuery(
{ wallet: accountAddress || '', isPnl: false },
{
skip: !accountAddress,
refetchOnFocus: false,
}
);

// Convert portfolio data to tokens format
const portfolioTokens = useMemo(() => {
if (!walletPortfolioData?.result?.data) return [];
return convertPortfolioAPIResponseToToken(walletPortfolioData.result.data);
}, [walletPortfolioData]);

// Smart loading state for portfolio to prevent flickering on refetch
const [hasPortfolioLoaded, setHasPortfolioLoaded] = useState(false);

useEffect(() => {
if (portfolioTokens.length > 0 || (!walletPortfolioLoading && walletPortfolioData)) {
setHasPortfolioLoaded(true);
}
}, [portfolioTokens, walletPortfolioLoading, walletPortfolioData]);

// Show loading only on initial fetch
const displayPortfolioLoading = walletPortfolioLoading && !hasPortfolioLoaded;

// Sync sellToken to topupToken when coming from search in topup mode
useEffect(() => {
if (isSearchingFromTopup && sellToken) {
setTopupToken(sellToken);
setIsSearchingFromTopup(false);
}
}, [isSearchingFromTopup, sellToken]);

// Render Search if active
if (searching) {
return (
<Search
setSearching={setSearching}
isBuy={false}
setBuyToken={setBuyToken}
setSellToken={setSellToken}
chains={chains}
setChains={setChains}
walletPortfolioData={walletPortfolioData?.result?.data}
walletPortfolioLoading={walletPortfolioLoading}
walletPortfolioFetching={walletPortfolioFetching}
walletPortfolioError={!!walletPortfolioError}
refetchWalletPortfolio={refetchWalletPortfolio}
isSearchingFromTopup={isSearchingFromTopup}
/>
);
}

// Render HomeScreen - core Gas Tank functionality
return (
<HomeScreen
accountAddress={accountAddress || null}
setSearching={setSearching}
buyToken={buyToken}
setBuyToken={setBuyToken}
sellToken={sellToken}
setSellToken={setSellToken}
isBuy={isBuy}
setIsBuy={setIsBuy}
refetchWalletPortfolio={refetchWalletPortfolio}
refetchGasTankBalance={refetchBalance}
setIsSearchingFromTopup={setIsSearchingFromTopup}
portfolioTokens={portfolioTokens}
isPortfolioLoading={displayPortfolioLoading}
hasPortfolioLoaded={hasPortfolioLoaded}
topupToken={topupToken}
setTopupToken={setTopupToken}
onboardingScreen={onboardingScreen}
setOnboardingScreen={setOnboardingScreen}
totalBalance={totalBalance}
isBalanceLoading={displayLoading}
/>
);
};
140 changes: 140 additions & 0 deletions src/apps/gas-tank/components/App/HomeScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { Dispatch, SetStateAction } from 'react';
import { useGasTankHistory } from '../../hooks/useGasTankHistory';
import { GasTankBalanceCard } from '../Balance/GasTankBalanceCard';
import { GasTankHistoryCard } from '../History/GasTankHistoryCard';
import { SkeletonBalanceCard, SkeletonHistoryCard } from '../History/GasTankSkeleton';
import TopUpScreen from '../../../pulse/components/Onboarding/TopUpScreen';
import { SelectedToken } from '../../../pulse/types/tokens';
import { PortfolioToken } from '../../../../services/tokensData';
import PillarXLogo from '../../../../assets/images/pillarX_full_white.png';

interface HomeScreenProps {
accountAddress: string | null;
setSearching: Dispatch<SetStateAction<boolean>>;
buyToken: SelectedToken | null;
setBuyToken: Dispatch<SetStateAction<SelectedToken | null>>;
sellToken: SelectedToken | null;
setSellToken: Dispatch<SetStateAction<SelectedToken | null>>;
isBuy: boolean;
setIsBuy: Dispatch<SetStateAction<boolean>>;
refetchWalletPortfolio: () => void;
refetchGasTankBalance: () => void;
setIsSearchingFromTopup: Dispatch<SetStateAction<boolean>>;
portfolioTokens: PortfolioToken[];
isPortfolioLoading: boolean;
topupToken: SelectedToken | null;
setTopupToken: Dispatch<SetStateAction<SelectedToken | null>>;
onboardingScreen: 'welcome' | 'topup' | null;
setOnboardingScreen: Dispatch<SetStateAction<'welcome' | 'topup' | null>>;
totalBalance: number;
isBalanceLoading: boolean;
hasPortfolioLoaded: boolean;
}

/**
* Main gas tank home screen
* Displays balance card and transaction history card
* Manages top-up flow
*/
export const HomeScreen: React.FC<HomeScreenProps> = ({
accountAddress,
setSearching,
buyToken,
setBuyToken,
sellToken,
setSellToken,
isBuy,
setIsBuy,
refetchWalletPortfolio,
refetchGasTankBalance,
setIsSearchingFromTopup,
portfolioTokens,
isPortfolioLoading,
topupToken,
setTopupToken,
onboardingScreen,
setOnboardingScreen,
totalBalance,
isBalanceLoading,
hasPortfolioLoaded,
}) => {



const {
transactions,
isLoading: isHistoryLoading,
error: historyError,
refetch: refetchHistory,
} = useGasTankHistory(accountAddress);

// Show TopUpScreen if in top-up mode
if (onboardingScreen === 'topup') {
return (
<div className="min-h-screen bg-[#121116] flex items-center justify-center p-4">
<TopUpScreen
onBack={() => {
setOnboardingScreen(null);
refetchGasTankBalance();
}}
Comment on lines +76 to +79
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Refetch history after top-up to show new transactions.

When returning from TopUpScreen, only refetchGasTankBalance() is called. If the user completed a top-up, the transaction history should also be refreshed to display the new transaction.

🔎 Suggested fix
 onBack={() => {
   setOnboardingScreen(null);
   refetchGasTankBalance();
+  refetchHistory();
 }}
🤖 Prompt for AI Agents
In @src/apps/gas-tank/components/App/HomeScreen.tsx around lines 76-79, The
onBack handler only calls setOnboardingScreen(null) and refetchGasTankBalance();
update it to also refresh the transaction history after returning from
TopUpScreen by calling the transaction-refresh function (e.g.,
refetchTransactionHistory or refetchGasTankTransactions) in the same block so
the new top-up appears in the history; modify the onBack callback where
setOnboardingScreen and refetchGasTankBalance are called to invoke that refetch
function as well.

initialBalance={totalBalance}
setSearching={() => {
setIsSearchingFromTopup(true);
setSearching(true);
}}
selectedToken={topupToken}
portfolioTokens={portfolioTokens}
setOnboardingScreen={setOnboardingScreen}
markOnboardingComplete={() => {}}
isPortfolioLoading={isPortfolioLoading}
hasPortfolioData={hasPortfolioLoaded}
showCloseButton={true}
/>
</div>
);
}


// ... existing code ...
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove misleading comment.

The comment references "existing code" but this is a new file. Remove or update the comment to avoid confusion.

🔎 Suggested fix
-
-// ... existing code ...
-

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @src/apps/gas-tank/components/App/HomeScreen.tsx around line 98, The
top-of-file comment that reads "// ... existing code ..." in the HomeScreen
component is misleading because this is a new file; remove or replace that
comment with a clear, accurate description (or delete it entirely). Locate the
placeholder comment in src/apps/gas-tank/components/App/HomeScreen.tsx around
the HomeScreen component declaration and either delete the "// ... existing code
..." line or replace it with a concise comment describing the component's
purpose.


// Main Gas Tank display
return (
<div className="min-h-screen bg-[#121116] overflow-y-auto font-['Poppins']">

{/* PillarX Logo */}
<div className="flex justify-center mt-[42px] mb-[40px]">
<img src={PillarXLogo} alt="PillarX" className="h-[24px]" />
</div>

{/* Container with specific gradient background */}
<div className="relative w-full max-w-[1320px] mx-auto min-h-[380px] bg-[#1E1D24] rounded-[24px]">
{/* Main content - Flex column on mobile, Row on larger screens */}
<div className="flex flex-row justify-center gap-[36px] p-[36px]">
{/* Left card - Balance */}
{isBalanceLoading ? (
<SkeletonBalanceCard />
) : (
<GasTankBalanceCard
balance={totalBalance}
isLoading={isBalanceLoading}
transactions={transactions}
onTopUpClick={() => setOnboardingScreen('topup')}
/>
)}

{/* Right card - History */}
{isHistoryLoading && transactions.length === 0 ? (
<SkeletonHistoryCard />
) : (
<GasTankHistoryCard
transactions={transactions}
isLoading={isHistoryLoading}
error={historyError}
onRetry={refetchHistory}
/>
)}
</div>
</div>
</div>
);
};
100 changes: 100 additions & 0 deletions src/apps/gas-tank/components/Balance/GasTankBalanceCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { ProcessedGasTankTransaction } from '../../types/gasTank';
import { TailSpin } from 'react-loader-spinner';
import GasTankIcon from '../../../pulse/assets/gas-tank-icon.svg';

interface GasTankBalanceCardProps {
balance: number;
isLoading: boolean;
transactions: ProcessedGasTankTransaction[];
onTopUpClick: () => void;
}

/**
* Gas tank balance card component
* Displays: balance, "On All Networks" subtitle, total spend, and top-up button
*/
export const GasTankBalanceCard: React.FC<GasTankBalanceCardProps> = ({
balance,
isLoading,
transactions,
onTopUpClick,
}) => {
// Calculate total spend from transactions (sum of all Spend type transactions)
const totalSpend = transactions
.filter((tx) => tx.type === 'Spend')
.reduce((sum, tx) => sum + parseFloat(tx.usdcAmount), 0);

return (
<div className="w-full md:w-auto md:flex-1 max-w-[606px] min-w-[300px] h-[460px] bg-[#1E1D24] border border-[#25232D] rounded-[12px] overflow-hidden p-[16px] flex flex-col relative">
{/* Background Vectors Removed */}

{/* Header with icon */}
<div className="flex items-center gap-[5px] mb-[10px]">
{/* Group 1171278651 */}
<div className="w-[25px] h-[22px] relative flex items-center justify-center">
<img src={GasTankIcon} alt="Gas Tank" className="w-full h-full" />
</div>
<span className="font-['Poppins'] font-normal text-[16px] leading-[16px] text-white">
Universal Gas Tank
</span>
</div>

{/* Balance display */}
<div className="mb-[20px]">
{isLoading ? (
<div className="flex items-center justify-start h-[30px]">
<TailSpin color="#FFFFFF" height={24} width={24} />
</div>
) : (
<div className="flex items-end gap-2">
<span className="font-['Poppins'] font-medium text-[36px] leading-[36px] tracking-[-0.02em] text-white">
${balance.toFixed(2)}
</span>
<span className="font-['Poppins'] font-normal text-[14px] leading-[26px] tracking-[-0.02em] text-[#8A77FF]">
On All Networks
</span>
</div>
)}
</div>

{/* Top up button */}
<div className="mb-[20px] w-[106px] h-[43px] bg-[#121116] rounded-[10px] pt-[2px] pr-[2px] pb-[6px] pl-[2px] flex flex-col gap-[10px] flex-shrink-0">
<button
onClick={onTopUpClick}
className="w-[102px] h-[35px] bg-[#4E448A] rounded-[8px] flex items-center justify-center gap-[10px] hover:bg-[#5A52A0] transition-colors px-[6px] py-[1px]"
>
<span className="font-['Poppins'] font-semibold text-[14px] leading-[21px] text-center tracking-[-0.02em] text-white">
Top up
</span>
</button>
</div>

{/* Subtitle */}
<div className="mb-[12px]">
<span className="font-['Poppins'] font-normal text-[13px] leading-[20px] tracking-[-0.02em] text-white">
Top up your Gas Tank so you pay for network fees on every chain.
</span>
</div>

{/* Total spend badge - If user meant "balance needs to be on bottom", maybe they meant "Total Spend"? Positioned here. */}
{totalSpend > 0 && (
<div className="mb-[12px] self-start inline-flex items-center justify-center gap-2 px-[6px] py-[4px] bg-[rgba(92,255,147,0.1)] rounded-[4px]">
<span className="font-['Poppins'] font-normal text-[14px] leading-[14px] tracking-[-0.02em] text-[#5CFF93]">
Total Spend: ${totalSpend.toFixed(2)}
</span>
</div>
)}

{/* Description */}
<div className="w-full">
<p className="font-['Poppins'] font-light text-[13px] leading-[20px] tracking-[-0.02em] text-white opacity-50 m-0">
The PillarX Gas Tank is your universal balance for covering transaction
fees across all networks. When you top up your Tank, you’re allocating
tokens specifically for paying gas. You can increase your balance
anytime, and the tokens in your Tank can be used to pay network fees on
any supported chain.
</p>
</div>
</div>
);
};
Loading
Loading