From 21810ad7b42cccecc5f59f7d50f2c3ef459e3f9a Mon Sep 17 00:00:00 2001 From: MusabShakeel576 <46605319+MusabShakeel576@users.noreply.github.com> Date: Mon, 2 Sep 2024 16:03:25 +0000 Subject: [PATCH 01/10] Add total balance and Billing & Plan nav menu --- app/build/Home.tsx | 2 +- app/dashboard/Home.tsx | 43 +++++++++++++++++------------------- components/NavMenu.tsx | 4 +--- lib/api.ts | 5 ++++- lib/helpers.ts | 4 ++++ lib/types.ts | 12 ++++++++++ store/operatorSlice/index.ts | 38 ++++++++++++++++++++++++++++++- 7 files changed, 79 insertions(+), 29 deletions(-) diff --git a/app/build/Home.tsx b/app/build/Home.tsx index e084d738..6543b3ef 100644 --- a/app/build/Home.tsx +++ b/app/build/Home.tsx @@ -64,7 +64,7 @@ const Home = () => {
- +
diff --git a/app/dashboard/Home.tsx b/app/dashboard/Home.tsx index 3b50e2a3..bcb89294 100644 --- a/app/dashboard/Home.tsx +++ b/app/dashboard/Home.tsx @@ -6,7 +6,7 @@ import { useAppDispatch, useAppSelector } from "@/store/store"; import { BalanceStateType, fetchUsdPrice, selectBalanceSlice } from "@/store/balanceSlice"; import { useAccount, useBalance, useBlockNumber, useSignMessage } from "wagmi"; import { fuse } from "wagmi/chains"; -import { checkIsActivated, fetchSponsorIdBalance, fetchSponsoredTransactions, generateSecretApiKey, selectOperatorSlice, setIsContactDetailsModalOpen, setIsRollSecretKeyModalOpen, setIsTopupAccountModalOpen, setIsWithdrawModalOpen, validateOperator } from "@/store/operatorSlice"; +import { OperatorStateType, checkIsActivated, fetchSponsorIdBalance, fetchSponsoredTransactions, fetchTokenBalances, generateSecretApiKey, selectOperatorSlice, setIsContactDetailsModalOpen, setIsRollSecretKeyModalOpen, setIsTopupAccountModalOpen, setIsWithdrawModalOpen, validateOperator } from "@/store/operatorSlice"; import TopupAccountModal from "@/components/dashboard/TopupAccountModal"; import Image from "next/image"; import copy from "@/assets/copy-black.svg"; @@ -49,7 +49,7 @@ type OperatorAccountBalanceProps = { chain: any; balanceSlice: BalanceStateType; balance: any; - isActivated: boolean; + operatorSlice: OperatorStateType; dispatch: ThunkDispatch & Dispatch; } @@ -126,12 +126,12 @@ const ConnectEoaWallet = () => { ) } -const OperatorAccountBalance = ({ chain, balanceSlice, balance, isActivated, dispatch }: OperatorAccountBalanceProps) => { +const OperatorAccountBalance = ({ chain, balanceSlice, balance, operatorSlice, dispatch }: OperatorAccountBalanceProps) => { useEffect(() => { const fiveSecondInMillisecond = 5000; const intervalId = setInterval(() => { - if (isActivated) { + if (operatorSlice.isActivated) { dispatch(fetchSponsoredTransactions()); } else { dispatch(checkIsActivated()); @@ -141,7 +141,13 @@ const OperatorAccountBalance = ({ chain, balanceSlice, balance, isActivated, dis return () => { clearInterval(intervalId); } - }, [dispatch, isActivated]) + }, [dispatch, operatorSlice.isActivated]) + + useEffect(() => { + if (operatorSlice.operator.user.smartContractAccountAddress) { + dispatch(fetchTokenBalances({ address: operatorSlice.operator.user.smartContractAccountAddress })); + } + }, [dispatch, operatorSlice.operator.user.smartContractAccountAddress]) return (
@@ -161,24 +167,15 @@ const OperatorAccountBalance = ({ chain, balanceSlice, balance, isActivated, dis
-

- {(chain && chain.id === fuse.id) ? + {operatorSlice.isFetchingTokenBalances || balanceSlice.isUsdPriceLoading ? + : +

+ ${(chain && chain.id === fuse.id) ? new Intl.NumberFormat().format( - parseFloat(formatUnits(balance?.value ?? BigInt(0), balance?.decimals ?? evmDecimals) ?? "0") + (parseFloat(balance?.formatted ?? "0") * balanceSlice.price) + operatorSlice.totalTokenBalance ) : - 0 - } FUSE -

- {balanceSlice.isUsdPriceLoading ? - : -

- ${(chain && chain.id === fuse.id) ? - new Intl.NumberFormat().format( - parseFloat((parseFloat(formatUnits(balance?.value ?? BigInt(0), balance?.decimals ?? evmDecimals) ?? "0.00") * balanceSlice.price).toString()) - ) : - "0.00" - } -

+ "0.00"} + }
@@ -293,7 +290,7 @@ const Home = () => { {operatorSlice.isAccountCreationModalOpen && } {operatorSlice.isCongratulationModalOpen && }
- +

Operator Dashboard @@ -351,7 +348,7 @@ const Home = () => { chain={chain} balanceSlice={balanceSlice} balance={balance} - isActivated={operatorSlice.isActivated} + operatorSlice={operatorSlice} dispatch={dispatch} /> : operatorSlice.isOperatorExist ? diff --git a/components/NavMenu.tsx b/components/NavMenu.tsx index c7ab8320..9318abb5 100644 --- a/components/NavMenu.tsx +++ b/components/NavMenu.tsx @@ -14,7 +14,6 @@ type NavMenuProps = { selected?: string; isResponsive?: boolean; className?: string; - liClassName?: string; }; type OpenMenuItemEvent = { @@ -32,7 +31,6 @@ const NavMenu = ({ selected = "", isResponsive = false, className = `items-center justify-between w-auto order-1 absolute left-[50%] -translate-x-[50%] rounded-md ${isResponsive ? "md:w-full md:translate-y-8 md:top-1/2 md:bg-black" : ""}`, - liClassName = "w-20", }: NavMenuProps) => { const matches = useMediaQuery("(min-width: 768px)"); const { address, connector } = useAccount(); @@ -47,7 +45,7 @@ const NavMenu = ({ @@ -154,3 +154,6 @@ export const postConsensusDelegatedAmounts = async (delegatedAmounts: DelegatedA ) return response.data } + +export const fetchAddressTokenBalances = (address: Address): Promise => + axios.get(`https://explorer.fuse.io/api/v2/addresses/${address}/token-balances`).then(response => response.data) \ No newline at end of file diff --git a/lib/helpers.ts b/lib/helpers.ts index 8d14468f..770c30b2 100644 --- a/lib/helpers.ts +++ b/lib/helpers.ts @@ -94,6 +94,10 @@ export const buildSubMenuItems = [ title: "Dashboard", link: "/dashboard", }, + { + title: "Billing & Plan", + link: "#", + }, ]; export const splitSecretKey = (secretKey: string) => { diff --git a/lib/types.ts b/lib/types.ts index 776b830a..83c98342 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -383,3 +383,15 @@ export interface ValidatorResponse { pendingValidators: Address[] validatorsMetadata: Record } + +export type Token = { + decimals: string; + exchange_rate: string; +} + +export type TokenBalance = { + value: string; + token: Token; +} + +export type TokenBalances = TokenBalance[]; diff --git a/store/operatorSlice/index.ts b/store/operatorSlice/index.ts index c01906d3..238953ee 100644 --- a/store/operatorSlice/index.ts +++ b/store/operatorSlice/index.ts @@ -4,7 +4,7 @@ import { Signer, ethers } from "ethers"; import { FuseSDK } from "@fuseio/fusebox-web-sdk"; import { hex, splitSecretKey } from "@/lib/helpers"; import { Operator, OperatorContactDetail, SignData, Withdraw } from "@/lib/types"; -import { checkActivated, checkOperatorExist, fetchCurrentOperator, fetchSponsoredTransactionCount, postCreateApiSecretKey, postCreateOperator, postCreatePaymaster, postValidateOperator, updateApiSecretKey } from "@/lib/api"; +import { checkActivated, checkOperatorExist, fetchAddressTokenBalances, fetchCurrentOperator, fetchSponsoredTransactionCount, postCreateApiSecretKey, postCreateOperator, postCreatePaymaster, postValidateOperator, updateApiSecretKey } from "@/lib/api"; import { RootState } from "../store"; import { Address } from "abitype"; import { parseEther, parseUnits } from "ethers/lib/utils"; @@ -14,6 +14,7 @@ import { getSponsorIdBalance } from "@/lib/contractInteract"; import * as amplitude from "@amplitude/analytics-browser"; import { getERC20Balance } from "@/lib/erc20"; import { ERC20ABI } from "@/lib/abi/ERC20"; +import { formatUnits } from "viem"; const initOperator: Operator = { user: { @@ -86,6 +87,8 @@ export interface OperatorStateType { withdraw: Withdraw; operatorContactDetail: OperatorContactDetail; operator: Operator; + isFetchingTokenBalances: boolean; + totalTokenBalance: number; } const INIT_STATE: OperatorStateType = { @@ -127,6 +130,8 @@ const INIT_STATE: OperatorStateType = { withdraw: initWithdraw, operatorContactDetail: initOperatorContactDetail, operator: initOperator, + isFetchingTokenBalances: false, + totalTokenBalance: 0, }; export const checkOperator = createAsyncThunk( @@ -575,6 +580,27 @@ export const fetchSponsoredTransactions = createAsyncThunk< } ); +export const fetchTokenBalances = createAsyncThunk( + "OPERATOR/FETCH_TOKEN_BALANCES", + async ({ + address, + }: { + address: Address, + }) => { + try { + const tokenBalances = await fetchAddressTokenBalances(address); + let totalTokenBalance = 0; + tokenBalances.map((tokenBalance) => { + totalTokenBalance += parseFloat(formatUnits(BigInt(tokenBalance.value), parseFloat(tokenBalance.token.decimals))) * parseFloat(tokenBalance.token.exchange_rate); + }); + return totalTokenBalance; + } catch (error) { + console.log(error); + throw error; + } + } +); + const operatorSlice = createSlice({ name: "OPERATOR_STATE", initialState: INIT_STATE, @@ -828,6 +854,16 @@ const operatorSlice = createSlice({ .addCase(fetchSponsoredTransactions.rejected, (state) => { state.isFetchingSponsoredTransactions = false; }) + .addCase(fetchTokenBalances.pending, (state) => { + state.isFetchingTokenBalances = true; + }) + .addCase(fetchTokenBalances.fulfilled, (state, action) => { + state.isFetchingTokenBalances = false; + state.totalTokenBalance = action.payload; + }) + .addCase(fetchTokenBalances.rejected, (state) => { + state.isFetchingTokenBalances = false; + }) }, }); From 6ace351fde27720ee2a70a905555b078afe92e10 Mon Sep 17 00:00:00 2001 From: Nikolay Date: Wed, 18 Sep 2024 15:55:14 +0300 Subject: [PATCH 02/10] fixed operators usd balances --- store/operatorSlice/index.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/store/operatorSlice/index.ts b/store/operatorSlice/index.ts index 238953ee..c35c10d9 100644 --- a/store/operatorSlice/index.ts +++ b/store/operatorSlice/index.ts @@ -590,8 +590,13 @@ export const fetchTokenBalances = createAsyncThunk( try { const tokenBalances = await fetchAddressTokenBalances(address); let totalTokenBalance = 0; - tokenBalances.map((tokenBalance) => { - totalTokenBalance += parseFloat(formatUnits(BigInt(tokenBalance.value), parseFloat(tokenBalance.token.decimals))) * parseFloat(tokenBalance.token.exchange_rate); + tokenBalances.forEach((tokenBalance) => { + const value = parseFloat(tokenBalance.value) || 0; + const decimals = parseInt(tokenBalance.token.decimals) || 18; + const exchangeRate = parseFloat(tokenBalance.token.exchange_rate) || 0; + + const tokenValue = value / Math.pow(10, decimals); + totalTokenBalance += tokenValue * exchangeRate; }); return totalTokenBalance; } catch (error) { From f592d812c90457bc02e5402e624664061a222c80 Mon Sep 17 00:00:00 2001 From: Nikolay Date: Thu, 19 Sep 2024 14:26:14 +0300 Subject: [PATCH 03/10] update now button --- app/dashboard/Home.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/dashboard/Home.tsx b/app/dashboard/Home.tsx index bcb89294..89ea4b72 100644 --- a/app/dashboard/Home.tsx +++ b/app/dashboard/Home.tsx @@ -32,6 +32,7 @@ import hide from "@/assets/hide.svg"; import { formatUnits } from "viem"; import { SignMessageVariables } from "wagmi/query"; import contactSupport from "@/assets/contact-support.svg"; +import router from "next/router"; type CreateOperatorWalletProps = { accessToken: string; @@ -340,7 +341,12 @@ const Home = () => {

}
-
+
+
+
+ {planDetails.map((detail, index) => ( +
+

+ {detail.title} +

+

+ {detail.value} +

+
+ ))} +
+ + +
+

+ Payment +

+
+
+
+ +
+

+ Invoices +

+ + + + + + + + + + + {invoices.map((invoice, index) => ( + + + + + + + ))} + +
DateMonthsInvoice TotalStatus
{invoice.date}{invoice.months}{invoice.total} +

{invoice.status}

+

+ View Invoice +

+
+
+
+
+ ); +}; + +export default Home; diff --git a/app/billing/layout.tsx b/app/billing/layout.tsx new file mode 100644 index 00000000..29b13934 --- /dev/null +++ b/app/billing/layout.tsx @@ -0,0 +1,14 @@ +import type { Metadata } from 'next' + +export const metadata: Metadata = { + title: 'Billing & Plan - Fuse Console', + description: 'Upgrade your billing & plan and view your invoices', +} + +export default function BillingLayout({ + children, +}: { + children: React.ReactNode +}) { + return
{children}
+} diff --git a/app/billing/page.tsx b/app/billing/page.tsx new file mode 100644 index 00000000..0a598256 --- /dev/null +++ b/app/billing/page.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { useEffect } from "react"; +import Home from "./Home"; + +import { useAppDispatch } from "@/store/store"; +import { setSelectedNavbar } from "@/store/navbarSlice"; +import Footer from "@/components/Footer"; +import Topbar from "@/components/Topbar"; +import ChainModal from "@/components/ChainModal"; + +const Billing = () => { + const dispatch = useAppDispatch(); + + useEffect(() => { + dispatch(setSelectedNavbar("billing")); + }, [dispatch]) + + return ( +
+
+ + + +
+
+
+ ); +}; + +export default Billing; diff --git a/app/dashboard/Home.tsx b/app/dashboard/Home.tsx index 598da042..db4f58b2 100644 --- a/app/dashboard/Home.tsx +++ b/app/dashboard/Home.tsx @@ -31,8 +31,7 @@ import hide from "@/assets/hide.svg"; import { formatUnits } from "viem"; import { SignMessageVariables } from "wagmi/query"; import contactSupport from "@/assets/contact-support.svg"; -import router from "next/router"; - +import { useRouter } from "next/navigation"; type CreateOperatorWalletProps = { isValidated: boolean; signMessage: (variables: SignMessageVariables) => void; @@ -171,10 +170,10 @@ const OperatorAccountBalance = ({ chain, balanceSlice, balance, operatorSlice, d :

${(chain && chain.id === fuse.id) ? - new Intl.NumberFormat().format( - (parseFloat(balance?.formatted ?? "0") * balanceSlice.price) + operatorSlice.totalTokenBalance - ) : - "0.00"} + new Intl.NumberFormat().format( + (parseFloat(balance?.formatted ?? "0") * balanceSlice.price) + operatorSlice.totalTokenBalance + ) : + "0.00"}

}
@@ -207,6 +206,7 @@ const Home = () => { const operatorSlice = useAppSelector(selectOperatorSlice); const [showSecretKey, setShowSecretKey] = useState(false); const controller = useMemo(() => new AbortController(), []); + const router = useRouter(); const { isConnected, address, chain } = useAccount(); const signer = useEthersSigner(); const { data: blockNumber } = useBlockNumber({ watch: true }); @@ -341,11 +341,13 @@ const Home = () => { }
- + + {isOpen && ( + +
+

+ Tokens +

+
+ {Object.keys(coins).map((coin) => ( + + ))} +
+
+
+ )} +
+
+
+ + ) +} + +const Detail = ({ title, description }: DetailProps) => { + return ( +
+

+ {title} +

+
+

+ {description} +

+
+
+ ) +} + +const PayModal = (): JSX.Element => { + const operatorSlice = useAppSelector(selectOperatorSlice); + const dispatch = useAppDispatch(); + + useEffect(() => { + window.addEventListener("click", (e) => { + if ((e.target as HTMLElement).id === "pay-modal-bg") { + dispatch(setIsPayModalOpen(false)); + } + }); + }, [dispatch]); + + return ( + + {operatorSlice.isPayModalOpen && ( + + + close dispatch(setIsPayModalOpen(false))} + /> +
+
+

+ Pay for Pro Plan +

+

+ Prepay Pro plan for a period of 1 to 12 months. We currently only accept USDC and USDT. +

+
+ +
+ + + + +
+
+
+ )} +
+ ); +}; +export default PayModal; diff --git a/store/operatorSlice/index.ts b/store/operatorSlice/index.ts index 45573041..2d28338e 100644 --- a/store/operatorSlice/index.ts +++ b/store/operatorSlice/index.ts @@ -86,6 +86,7 @@ export interface OperatorStateType { operator: Operator; isFetchingTokenBalances: boolean; totalTokenBalance: number; + isPayModalOpen: boolean; } const INIT_STATE: OperatorStateType = { @@ -127,6 +128,7 @@ const INIT_STATE: OperatorStateType = { operator: initOperator, isFetchingTokenBalances: false, totalTokenBalance: 0, + isPayModalOpen: false, }; export const checkOperator = createAsyncThunk( @@ -624,6 +626,9 @@ const operatorSlice = createSlice({ setOperator: (state, action: PayloadAction) => { state.operator = action.payload }, + setIsPayModalOpen: (state, action: PayloadAction) => { + state.isPayModalOpen = action.payload + }, setLogout: (state) => { state.isOperatorExist = false; state.isValidated = false; @@ -848,6 +853,7 @@ export const { setRedirect, setOperatorContactDetail, setOperator, + setIsPayModalOpen, setLogout, setHydrate } = operatorSlice.actions; From d62572550a150a73be39c43fb1ef26b79733358b Mon Sep 17 00:00:00 2001 From: MusabShakeel576 <46605319+MusabShakeel576@users.noreply.github.com> Date: Thu, 14 Nov 2024 15:09:32 +0000 Subject: [PATCH 07/10] Add Billing modal --- app/billing/Home.tsx | 3 +- app/billing/page.tsx | 2 + assets/chevron-gray-down.svg | 3 + assets/search-big.svg | 3 + components/billing/BillingModal.tsx | 297 ++++++++++++++++++++++++++++ components/billing/PayModal.tsx | 2 +- package-lock.json | 6 + package.json | 1 + store/operatorSlice/index.ts | 6 + tailwind.config.ts | 2 + 10 files changed, 323 insertions(+), 2 deletions(-) create mode 100644 assets/chevron-gray-down.svg create mode 100644 assets/search-big.svg create mode 100644 components/billing/BillingModal.tsx diff --git a/app/billing/Home.tsx b/app/billing/Home.tsx index 14dd56a6..83543b39 100644 --- a/app/billing/Home.tsx +++ b/app/billing/Home.tsx @@ -1,7 +1,7 @@ import NavMenu from "@/components/NavMenu"; import Button from "@/components/ui/Button"; import { buildSubMenuItems } from "@/lib/helpers"; -import { setIsPayModalOpen } from "@/store/operatorSlice"; +import { setIsBillingModalOpen, setIsPayModalOpen } from "@/store/operatorSlice"; import { useAppDispatch } from "@/store/store"; const planDetails = [ @@ -66,6 +66,7 @@ const Home = () => { text="Enter Billing Info" className="transition ease-in-out bg-success text-lg leading-none text-black font-semibold rounded-full hover:bg-black hover:text-white" padding="py-3 px-9" + onClick={() => dispatch(setIsBillingModalOpen(true))} />
diff --git a/app/billing/page.tsx b/app/billing/page.tsx index 849f119c..60748dae 100644 --- a/app/billing/page.tsx +++ b/app/billing/page.tsx @@ -9,6 +9,7 @@ import Footer from "@/components/Footer"; import Topbar from "@/components/Topbar"; import ChainModal from "@/components/ChainModal"; import PayModal from "@/components/billing/PayModal"; +import BillingModal from "@/components/billing/BillingModal"; const Billing = () => { const dispatch = useAppDispatch(); @@ -22,6 +23,7 @@ const Billing = () => {
+