diff --git a/.env.development b/.env.development index 5e5aec8..a9ee309 100644 --- a/.env.development +++ b/.env.development @@ -1,7 +1,7 @@ REACT_APP_BASE_URL=http://localhost:9002 # REACT_APP_BASE_URL=https://11561ba2d27f.ngrok.app REACT_APP_ASSETS_BUCKET=http://localhost -REACT_APP_DEMO_MODE=false +REACT_APP_DEMO_MODE=true # more info https://create-react-app.dev/docs/advanced-configuration ESLINT_NO_DEV_ERRORS=true diff --git a/src/components/layouts/main/MainContent/MainContent.tsx b/src/components/layouts/main/MainContent/MainContent.tsx index 0d02cf5..a9e4426 100644 --- a/src/components/layouts/main/MainContent/MainContent.tsx +++ b/src/components/layouts/main/MainContent/MainContent.tsx @@ -4,6 +4,7 @@ import { BaseLayout } from '@app/components/common/BaseLayout/BaseLayout'; interface HeaderProps { $isTwoColumnsLayout: boolean; + $isDesktop: boolean; } export default styled(BaseLayout.Content)` @@ -13,9 +14,14 @@ export default styled(BaseLayout.Content)` flex-direction: column; justify-content: space-between; + ${(props) => + props?.$isDesktop && + css` + z-index: 105; + `} + @media only screen and ${media.md} { - padding: ${LAYOUT.desktop.paddingVertical} 1.8rem 0 - ${LAYOUT.desktop.paddingHorizontal}; + padding: ${LAYOUT.desktop.paddingVertical} 1.8rem 0 ${LAYOUT.desktop.paddingHorizontal}; } @media only screen and ${media.xl} { diff --git a/src/components/layouts/main/MainLayout/MainLayout.tsx b/src/components/layouts/main/MainLayout/MainLayout.tsx index 7d3f9ea..79d7f31 100644 --- a/src/components/layouts/main/MainLayout/MainLayout.tsx +++ b/src/components/layouts/main/MainLayout/MainLayout.tsx @@ -74,6 +74,7 @@ const MainLayout: React.FC = () => { style={isDesktop ? { overflowY: 'hidden' } : { overflowY: 'auto' }} id="main-content" $isTwoColumnsLayout={isTwoColumnsLayout} + $isDesktop={isDesktop} >
diff --git a/src/components/nft-dashboard/Balance/Balance.styles.ts b/src/components/nft-dashboard/Balance/Balance.styles.ts index 68ecc43..5432e3f 100644 --- a/src/components/nft-dashboard/Balance/Balance.styles.ts +++ b/src/components/nft-dashboard/Balance/Balance.styles.ts @@ -1,6 +1,6 @@ import { BaseTypography } from '@app/components/common/BaseTypography/BaseTypography'; import { FONT_FAMILY, FONT_SIZE, FONT_WEIGHT } from '@app/styles/themes/constants'; -import { BaseSwitch } from '@app/components/common/BaseSwitch/BaseSwitch'; +import { BaseRow } from '@app/components/common/BaseRow/BaseRow'; import styled from 'styled-components'; export const TitleText = styled(BaseTypography.Title)` @@ -48,3 +48,7 @@ export const LabelSpan = styled.span` color: #fff; // Adjust color based on your theme font-size: 1rem; // Adjust size as needed `; +export const BalanceButtonsContainers = styled(BaseRow)` + width: 100%; + padding: 0 1rem; +` diff --git a/src/components/nft-dashboard/Balance/Balance.tsx b/src/components/nft-dashboard/Balance/Balance.tsx index 135f501..4adbbe6 100644 --- a/src/components/nft-dashboard/Balance/Balance.tsx +++ b/src/components/nft-dashboard/Balance/Balance.tsx @@ -66,13 +66,14 @@ export const Balance: React.FC = () => { - - - - - - - + + + + + + + + diff --git a/src/components/nft-dashboard/Balance/components/SendForm/SendForm.styles.ts b/src/components/nft-dashboard/Balance/components/SendForm/SendForm.styles.ts index f91d73d..8161ed6 100644 --- a/src/components/nft-dashboard/Balance/components/SendForm/SendForm.styles.ts +++ b/src/components/nft-dashboard/Balance/components/SendForm/SendForm.styles.ts @@ -1,4 +1,4 @@ -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import { BaseCard } from '@app/components/common/BaseCard/BaseCard'; import { BaseButton } from '@app/components/common/BaseButton/BaseButton'; import { BaseRow } from '@app/components/common/BaseRow/BaseRow'; @@ -13,7 +13,13 @@ export const TextRow = styled.div` gap: 1rem; `; -export const SubCard = styled(BaseCard)` +export const SubCard = styled(BaseCard)<{ $isMobile?: boolean }>` + width: 30%; + ${(props) => + props.$isMobile && + css` + width: 100%; + `} background-color: var(--additional-background-color); cursor: pointer; box-shadow: 0px 0px 10px 0px var(--shadow-color); @@ -44,13 +50,15 @@ export const SubCardHeader = styled.span` `; export const InputHeader = styled.span` - font-size: 1.3rem; + font-size: 1.5rem; `; export const SubCardAmount = styled.span` font-size: 1.5rem; `; export const SubCardContent = styled.div` + font-size: 1.3rem; + height: 100%; display: flex; justify-content: space-around; flex-direction: column; @@ -127,7 +135,6 @@ export const RateValue = styled.span` color: green; `; export const RBFWrapper = styled.div` - display: flex; flex-direction: row; gap: 1rem; diff --git a/src/components/nft-dashboard/Balance/components/SendForm/SendForm.tsx b/src/components/nft-dashboard/Balance/components/SendForm/SendForm.tsx index 9ec189f..0337738 100644 --- a/src/components/nft-dashboard/Balance/components/SendForm/SendForm.tsx +++ b/src/components/nft-dashboard/Balance/components/SendForm/SendForm.tsx @@ -1,4 +1,4 @@ -import React, { memo, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { BaseInput } from '@app/components/common/inputs/BaseInput/BaseInput'; import { BaseRow } from '@app/components/common/BaseRow/BaseRow'; import { BaseButton } from '@app/components/common/BaseButton/BaseButton'; @@ -8,34 +8,31 @@ import * as S from './SendForm.styles'; import { truncateString } from '@app/utils/utils'; import useBalanceData from '@app/hooks/useBalanceData'; import { BaseCheckbox } from '@app/components/common/BaseCheckbox/BaseCheckbox'; +import config from '@app/config/config'; + interface SendFormProps { - onSend: (status: boolean, address: string, amount: number) => void; + onSend: (status: boolean, address: string, amount: number, txid?: string, message?: string) => void; } -interface SuccessScreenProps { - isSuccess: boolean; - amount: number; - address: string; + +interface FeeRecommendation { + fastestFee: number; + halfHourFee: number; + hourFee: number; + economyFee: number; + minimumFee: number; } -const testTiers = [ - { - id: 'low', - rate: 4, - }, - { - id: 'med', - rate: 5, - }, - { - id: 'high', - rate: 5, - }, -]; +interface PendingTransaction { + txid: string; + feeRate: number; + timestamp: string; // ISO format string +} type tiers = 'low' | 'med' | 'high'; type Fees = { [key in tiers]: number; }; + const SendForm: React.FC = ({ onSend }) => { const { balanceData, isLoading } = useBalanceData(); const { isTablet, isDesktop } = useResponsive(); @@ -53,6 +50,7 @@ const SendForm: React.FC = ({ onSend }) => { address: '', amount: '1', }); + const [fees, setFees] = useState({ low: 0, med: 0, high: 0 }); const handleTierChange = (tier: any) => { @@ -60,14 +58,10 @@ const SendForm: React.FC = ({ onSend }) => { }; const isValidAddress = (address: string) => { - if (address.length > 0) { - return true; - } - return false; + return address.length > 0; }; const handleAddressSubmit = () => { - //check if valid address const isValid = isValidAddress(formData.address); if (isValid) { @@ -77,41 +71,93 @@ const SendForm: React.FC = ({ onSend }) => { setAddressError(true); } }; + const handleInputChange = (e: React.ChangeEvent) => { e.preventDefault(); const { name, value } = e.target; setFormData({ ...formData, [name]: value }); }; - const handleSend = () => { - if (loading) return; - if (inValidAmount) return; + + const handleSend = async () => { + if (loading || inValidAmount) return; setLoading(true); - //send request here (simulating request for now) - console.log('Sending data', formData); - setTimeout(() => { + const selectedFee = selectedTier ? fees[selectedTier] : fees.low; // Default to low if not selected + + const transactionRequest = { + choice: 1, // Default to choice 1 for new transactions + recipient_address: formData.address, + spend_amount: parseInt(formData.amount), + priority_rate: selectedFee, + }; + + try { + const response = await fetch('http://localhost:9003/transaction', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(transactionRequest), + }); + + const result = await response.json(); + setLoading(false); + + if (result.status === 'success') { + // Prepare the transaction details to send to the pending-transactions endpoint + const pendingTransaction: PendingTransaction = { + txid: result.txid, + feeRate: selectedFee, + timestamp: new Date().toISOString(), // Capture the current time in ISO format + }; + + // Send the transaction details to the pending-transactions endpoint + await fetch(`${config.baseURL}/pending-transactions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(pendingTransaction), + }); + + // Call the onSend callback with the result + onSend(true, formData.address, transactionRequest.spend_amount, result.txid, result.message); + } else { + onSend(false, formData.address, 0, '', result.message); + } + } catch (error) { + console.error('Transaction failed:', error); setLoading(false); - onSend(true, formData.address, amountWithFee ? amountWithFee : 0); - }, 2000); + onSend(false, formData.address, 0, '', 'Transaction failed due to a network error.'); + } }; useEffect(() => { - if (selectedTier) { - const vB = parseInt(formData.amount) / 50; - const lowFee = Math.ceil(vB * testTiers[0].rate); - const medFee = Math.ceil(vB * testTiers[1].rate); - const highFee = Math.ceil(vB * testTiers[2].rate); + const fetchFees = async () => { + try { + const response = await fetch('https://mempool.space/api/v1/fees/recommended'); + const data: FeeRecommendation = await response.json(); - setFees({ low: lowFee, med: medFee, high: highFee }); - } - }, [formData.amount, selectedTier]); //fetched fees should be used here + setFees({ + low: data.economyFee, + med: data.halfHourFee, + high: data.fastestFee, + }); + } catch (error) { + console.error('Failed to fetch fees:', error); + } + }; + + fetchFees(); + }, []); useEffect(() => { if (selectedTier) { setAmountWithFee(parseInt(formData.amount) + fees[selectedTier]); } }, [fees]); + useEffect(() => { if (formData.amount.length <= 0 || (balanceData && parseInt(formData.amount) > balanceData.latest_balance)) { setInvalidAmount(true); @@ -120,111 +166,123 @@ const SendForm: React.FC = ({ onSend }) => { } }, [formData.amount]); - const receiverPanel = () => { - return ( - <> - - Address - - - Continue - - ); - }; - const TieredFees = () => { - return ( - <> - handleTierChange(testTiers[0])} - className={`tier-hover ${selectedTier == testTiers[0].id ? 'selected' : ' '} ${ - selectedTier == testTiers[0].id && inValidAmount ? 'invalidAmount' : ' ' - } `} - > - - {`Low Priority`} - - {`${testTiers[0].rate} sat/vB`} - {`${fees?.low} Sats`} - - - - - handleTierChange(testTiers[1])} - className={`tier-hover ${selectedTier == testTiers[1].id ? 'selected' : ' '} ${ - selectedTier == testTiers[1].id && inValidAmount ? 'invalidAmount' : ' ' - } `} - > - - {`Medium Priority`} - - {`${testTiers[1].rate} sat/vB`} - {`${fees?.med} Sats`} - - - - - handleTierChange(testTiers[2])} - className={`tier-hover ${selectedTier == testTiers[2].id ? 'selected' : ' '} ${ - selectedTier == testTiers[2].id && inValidAmount ? 'invalidAmount' : ' ' - } `} + const receiverPanel = () => ( + <> + + Address + + + Continue + + ); + + const TieredFees = () => ( + <> + handleTierChange({ id: 'low', rate: fees.low })} + className={`tier-hover ${selectedTier === 'low' ? 'selected' : ''} ${ + selectedTier === 'low' && inValidAmount ? 'invalidAmount' : '' + } `} + > + + + {' '} + {`Low`} +
+ {`Priority`} +
+ + {`${fees.low} sat/vB`} + {`${fees.low} Sats`} + +
+
+ + handleTierChange({ id: 'med', rate: fees.med })} + className={`tier-hover ${selectedTier === 'med' ? 'selected' : ''} ${ + selectedTier === 'med' && inValidAmount ? 'invalidAmount' : '' + } `} + > + + {`Medium`} +
+ {`Priority`} + + {`${fees.med} sat/vB`} + {`${fees.med} Sats`} + +
+
+ + handleTierChange({ id: 'high', rate: fees.high })} + className={`tier-hover ${selectedTier === 'high' ? 'selected' : ''} ${ + selectedTier === 'high' && inValidAmount ? 'invalidAmount' : '' + } `} + > + + + {' '} + {`High`} +
+ {`Priority`} +
+ + {`${fees.high} sat/vB`} + {`${fees.high} Sats`} + +
+
+ + ); + + const detailsPanel = () => ( + + + + {`Amount = ${selectedTier && amountWithFee ? amountWithFee : ''}`} + + {inValidAmount && selectedTier && Invalid Amount} + + +
+ + {`Balance: ${balanceData ? balanceData.latest_balance : 0}`} +
+
+ + Tiered Fees + + + RBF Opt In + + {isDesktop || isTablet ? ( + + + + ) : ( + + + + )} + + + - - {`High Priority`} - - {`${testTiers[2].rate} sat/vB`} - {`${fees?.high} Sats`} - - -
- - ); - }; - const detailsPanel = () => { - return ( - - - - {`Amount = ${selectedTier && amountWithFee ? amountWithFee : ''} `} - - {inValidAmount && selectedTier && Invalid Amount} - - -
- - {` Balance: ${balanceData ? balanceData.latest_balance : 0}`} -
-
- - Tiered Fees - - - RBF Opt In - - {isDesktop || isTablet ? ( - - - - ) : ( - - - - )} - - - - Send - - -
- ); - }; + Send + + + + ); + return ( @@ -233,8 +291,8 @@ const SendForm: React.FC = ({ onSend }) => { {isDetailsOpen ? ( <> - {`To:`} -

+ To: +
{truncateString(formData.address, 65)}
{detailsPanel()} diff --git a/src/components/nft-dashboard/Balance/components/SendForm/components/ResultScreen.tsx b/src/components/nft-dashboard/Balance/components/SendForm/components/ResultScreen.tsx index 4d2987c..b73b007 100644 --- a/src/components/nft-dashboard/Balance/components/SendForm/components/ResultScreen.tsx +++ b/src/components/nft-dashboard/Balance/components/SendForm/components/ResultScreen.tsx @@ -1,28 +1,35 @@ import React from 'react'; -import { BaseButton } from '@app/components/common/BaseButton/BaseButton'; import { truncateString } from '@app/utils/utils'; import * as S from './ResultScreen.styles'; +import { useResponsive } from '@app/hooks/useResponsive'; interface ResultScreenProps { isSuccess: boolean; amount: number; receiver: string; + txid: string; // New field to display the transaction ID + message?: string; // Optional message from the transaction response } -const ResultScreen: React.FC = ({ isSuccess, amount, receiver }) => { +const ResultScreen: React.FC = ({ isSuccess, amount, receiver, txid, message }) => { + const { isTablet } = useResponsive(); return ( {isSuccess ? ( <> - Success! + Success! - {' '} - You have successfully sent
{amount} Sats
to
{truncateString(receiver, 10)} + You have successfully sent
{amount} Sats
to
{truncateString(receiver, isTablet ? 25 : 10)} +
+ Transaction ID:
{truncateString(txid, 15)}
) : ( <> - Failed - This transaction was unable to send. + Failed + + This transaction was unable to send.
+ {message && Error: {message}} +
)}
@@ -30,3 +37,4 @@ const ResultScreen: React.FC = ({ isSuccess, amount, receiver }; export default ResultScreen; + diff --git a/src/components/nft-dashboard/Balance/components/SendModal/SendModal.styles.ts b/src/components/nft-dashboard/Balance/components/SendModal/SendModal.styles.ts index 10a0908..4bd6d25 100644 --- a/src/components/nft-dashboard/Balance/components/SendModal/SendModal.styles.ts +++ b/src/components/nft-dashboard/Balance/components/SendModal/SendModal.styles.ts @@ -2,10 +2,13 @@ import styled from 'styled-components'; import { BaseModal } from '@app/components/common/BaseModal/BaseModal'; export const SendModal = styled(BaseModal)` - .ant-modal-content { - min-height: 30vh; - padding-left:2rem; - padding-right:2rem; + max-width: fit-content !important; + width: fit-content !important; + min-width: 50vw; - } + .ant-modal-content { + width: 100%; + min-width: 50vw; + padding: 2rem; + } `; diff --git a/src/components/nft-dashboard/Balance/components/SendModal/SendModal.tsx b/src/components/nft-dashboard/Balance/components/SendModal/SendModal.tsx index 7006558..573d5a8 100644 --- a/src/components/nft-dashboard/Balance/components/SendModal/SendModal.tsx +++ b/src/components/nft-dashboard/Balance/components/SendModal/SendModal.tsx @@ -4,22 +4,27 @@ import { BaseSpin } from '@app/components/common/BaseSpin/BaseSpin'; import SendForm from '../SendForm/SendForm'; import * as S from './SendModal.styles'; import ResultScreen from '../SendForm/components/ResultScreen'; + interface SendModalProps { isOpen: boolean; onOpenChange: () => void; } + interface SuccessScreenProps { isSuccess: boolean; amount: number; address: string; + txid?: string; // New field for transaction ID + message?: string; // Optional message from the transaction response } + const SendModal: React.FC = ({ isOpen, onOpenChange }) => { const [isLoading, setIsLoading] = useState(false); const [isFinished, setIsFinished] = useState(false); const [successScreenState, setSuccessScreenState] = useState(null); - const onFinish = (status: boolean, address: string, amount: number) => { - setSuccessScreenState({ isSuccess: status, address, amount }); + const onFinish = (status: boolean, address: string, amount: number, txid?: string, message?: string) => { + setSuccessScreenState({ isSuccess: status, address, amount, txid, message }); setIsFinished(true); }; @@ -28,15 +33,19 @@ const SendModal: React.FC = ({ isOpen, onOpenChange }) => { setIsFinished(false); onOpenChange(); }; + return ( - + {isFinished && successScreenState ? ( + txid={successScreenState.txid || ""} // Provide a default value + message={successScreenState.message} + /> + ) : ( )} @@ -46,3 +55,4 @@ const SendModal: React.FC = ({ isOpen, onOpenChange }) => { }; export default SendModal; + diff --git a/src/components/nft-dashboard/Balance/components/TopUpBalanceModal/TopUpBalanceModal.tsx b/src/components/nft-dashboard/Balance/components/TopUpBalanceModal/TopUpBalanceModal.tsx index 0e6ce89..d23bfbb 100644 --- a/src/components/nft-dashboard/Balance/components/TopUpBalanceModal/TopUpBalanceModal.tsx +++ b/src/components/nft-dashboard/Balance/components/TopUpBalanceModal/TopUpBalanceModal.tsx @@ -43,7 +43,7 @@ export const TopUpBalanceModal: React.FC = ({ }, [isOpen]); return ( - + diff --git a/src/hooks/usePendingTransactions.ts b/src/hooks/usePendingTransactions.ts new file mode 100644 index 0000000..99dac64 --- /dev/null +++ b/src/hooks/usePendingTransactions.ts @@ -0,0 +1,42 @@ +import { useState, useEffect } from 'react'; +import config from '@app/config/config'; + +interface PendingTransaction { + txid: string; + feeRate: number; + timestamp: string; +} + +const usePendingTransactions = () => { + const [pendingTransactions, setPendingTransactions] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const fetchPendingTransactions = async () => { + setIsLoading(true); + try { + const response = await fetch(`${config.baseURL}/pending-transactions`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + if (!response.ok) { + throw new Error(`Network response was not ok (status: ${response.status})`); + } + const data: PendingTransaction[] = await response.json(); + setPendingTransactions(data); + } catch (error) { + console.error('Error fetching pending transactions:', error); + } finally { + setIsLoading(false); + } + }; + + fetchPendingTransactions(); + }, []); + + return { pendingTransactions, isLoading }; +}; + +export default usePendingTransactions;