diff --git a/apps/deploy-web/src/context/PaymentPollingProvider/PaymentPollingProvider.spec.tsx b/apps/deploy-web/src/context/PaymentPollingProvider/PaymentPollingProvider.spec.tsx new file mode 100644 index 0000000000..60aad26800 --- /dev/null +++ b/apps/deploy-web/src/context/PaymentPollingProvider/PaymentPollingProvider.spec.tsx @@ -0,0 +1,293 @@ +import React from "react"; +import { mock } from "jest-mock-extended"; + +import type { AnalyticsService } from "@src/services/analytics/analytics.service"; +import { DEPENDENCIES, PaymentPollingProvider, usePaymentPolling } from "./PaymentPollingProvider"; + +import { act, render, screen, waitFor } from "@testing-library/react"; +import { buildManagedWallet, buildWallet, buildWalletBalance } from "@tests/seeders"; + +describe(PaymentPollingProvider.name, () => { + it("provides polling context to children", () => { + setup({ + isTrialing: false, + balance: { totalUsd: 100 }, + isWalletBalanceLoading: false + }); + + expect(screen.queryByTestId("is-polling")).toHaveTextContent("false"); + expect(screen.queryByTestId("start-polling")).toBeInTheDocument(); + expect(screen.queryByTestId("stop-polling")).toBeInTheDocument(); + }); + + it("prevents multiple polling instances", async () => { + const { refetchBalance } = setup({ + isTrialing: false, + balance: { totalUsd: 100 }, + isWalletBalanceLoading: false + }); + + await act(async () => { + screen.getByTestId("start-polling").click(); + }); + + const initialCallCount = refetchBalance.mock.calls.length; + + await act(async () => { + screen.getByTestId("start-polling").click(); + }); + + expect(refetchBalance.mock.calls.length).toBe(initialCallCount); + }); + + it("shows loading snackbar when polling starts", async () => { + const { enqueueSnackbar } = setup({ + isTrialing: false, + balance: { totalUsd: 100 }, + isWalletBalanceLoading: false + }); + + await act(async () => { + screen.getByTestId("start-polling").click(); + }); + + expect(enqueueSnackbar).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ + variant: "info", + autoHideDuration: null, + persist: true + }) + ); + }); + + it("stops polling when stopPolling is called", async () => { + setup({ + isTrialing: false, + balance: { totalUsd: 100 }, + isWalletBalanceLoading: false + }); + + await act(async () => { + screen.getByTestId("start-polling").click(); + }); + + await waitFor(() => { + expect(screen.queryByTestId("is-polling")).toHaveTextContent("true"); + }); + + await act(async () => { + screen.getByTestId("stop-polling").click(); + }); + + expect(screen.queryByTestId("is-polling")).toHaveTextContent("false"); + }); + + it("verifies polling starts correctly for non-trial users", async () => { + const { refetchBalance, cleanup } = setup({ + isTrialing: false, + balance: { totalUsd: 100 }, + isWalletBalanceLoading: false + }); + + await act(async () => { + screen.getByTestId("start-polling").click(); + }); + + await waitFor(() => { + expect(screen.queryByTestId("is-polling")).toHaveTextContent("true"); + }); + + await act(async () => { + jest.advanceTimersByTime(1000); + }); + + expect(refetchBalance).toHaveBeenCalled(); + + cleanup(); + }); + + it("verifies polling starts correctly for trial users", async () => { + const { refetchBalance, refetchManagedWallet, cleanup } = setup({ + isTrialing: true, + balance: { totalUsd: 100 }, + isWalletBalanceLoading: false + }); + + await act(async () => { + screen.getByTestId("start-polling").click(); + }); + + await waitFor(() => { + expect(screen.queryByTestId("is-polling")).toHaveTextContent("true"); + }); + + await act(async () => { + jest.advanceTimersByTime(1000); + }); + + expect(refetchBalance).toHaveBeenCalled(); + expect(refetchManagedWallet).toHaveBeenCalled(); + + cleanup(); + }); + + it("verifies analytics service is properly configured for trial users", async () => { + const { analyticsService } = setup({ + isTrialing: true, + balance: { totalUsd: 100 }, + isWalletBalanceLoading: false + }); + + await act(async () => { + screen.getByTestId("start-polling").click(); + }); + + expect(analyticsService.track).toBeDefined(); + expect(typeof analyticsService.track).toBe("function"); + }); + + it("handles zero initial balance correctly", async () => { + const { cleanup } = setup({ + isTrialing: false, + balance: { totalUsd: 0 }, + isWalletBalanceLoading: false + }); + + await act(async () => { + screen.getByTestId("start-polling").click(); + }); + + await waitFor(() => { + expect(screen.queryByTestId("is-polling")).toHaveTextContent("true"); + }); + + expect(screen.queryByTestId("is-polling")).toHaveTextContent("true"); + + cleanup(); + }); + + it("cleans up polling on unmount", async () => { + const { unmount, cleanup } = setup({ + isTrialing: false, + balance: { totalUsd: 100 }, + isWalletBalanceLoading: false + }); + + await act(async () => { + screen.getByTestId("start-polling").click(); + }); + + await waitFor(() => { + expect(screen.queryByTestId("is-polling")).toHaveTextContent("true"); + }); + + unmount(); + + expect(screen.queryByTestId("is-polling")).not.toBeInTheDocument(); + + cleanup(); + }); + + it("throws error when used outside provider", () => { + const TestComponent = () => { + usePaymentPolling(); + return
Test
; + }; + + // Suppress console.error for this test + const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => {}); + + expect(() => { + render(); + }).toThrow("usePaymentPolling must be used within a PaymentPollingProvider"); + + consoleSpy.mockRestore(); + }); + + function setup(input: { isTrialing: boolean; balance: { totalUsd: number } | null; isWalletBalanceLoading: boolean }) { + jest.useFakeTimers(); + + const refetchBalance = jest.fn(); + const refetchManagedWallet = jest.fn(); + const analyticsService = mock(); + const mockEnqueueSnackbar = jest.fn(); + const mockCloseSnackbar = jest.fn(); + const wallet = buildWallet({ isTrialing: input.isTrialing }); + const managedWallet = buildManagedWallet({ isTrialing: input.isTrialing }); + const walletBalance = input.balance ? buildWalletBalance(input.balance) : null; + + const mockSnackbar = ({ title, subTitle, iconVariant, showLoading }: { title: string; subTitle: string; iconVariant?: string; showLoading?: boolean }) => ( +
+ ); + + const mockManagedWallet = { + ...managedWallet, + username: "Managed Wallet" as const, + isWalletConnected: true, + isWalletLoaded: true, + selected: true, + creditAmount: 0 + }; + + const dependencies = { + ...DEPENDENCIES, + useWallet: jest.fn(() => wallet), + useWalletBalance: jest.fn(() => ({ + balance: walletBalance, + refetch: refetchBalance, + isLoading: input.isWalletBalanceLoading + })), + useManagedWallet: jest.fn(() => ({ + wallet: mockManagedWallet, + isLoading: false, + isFetching: false, + createError: null, + refetch: refetchManagedWallet, + create: jest.fn() + })), + useServices: jest.fn(() => ({ + analyticsService + })), + useSnackbar: jest.fn(() => ({ + enqueueSnackbar: mockEnqueueSnackbar, + closeSnackbar: mockCloseSnackbar + })), + Snackbar: mockSnackbar + } as unknown as typeof DEPENDENCIES; + + const TestComponent = () => { + const { pollForPayment, stopPolling, isPolling } = usePaymentPolling(); + return ( +
+
{isPolling.toString()}
+ + +
+ ); + }; + + const { rerender, unmount } = render( + + + + ); + + return { + refetchBalance, + refetchManagedWallet, + analyticsService, + enqueueSnackbar: mockEnqueueSnackbar, + closeSnackbar: mockCloseSnackbar, + rerender, + unmount, + cleanup: () => { + jest.useRealTimers(); + } + }; + } +}); diff --git a/apps/deploy-web/src/context/PaymentPollingProvider/PaymentPollingProvider.tsx b/apps/deploy-web/src/context/PaymentPollingProvider/PaymentPollingProvider.tsx new file mode 100644 index 0000000000..64f6bc9b3f --- /dev/null +++ b/apps/deploy-web/src/context/PaymentPollingProvider/PaymentPollingProvider.tsx @@ -0,0 +1,225 @@ +"use client"; +import React, { createContext, useCallback, useContext, useEffect, useRef } from "react"; +import { Snackbar } from "@akashnetwork/ui/components"; +import { useSnackbar } from "notistack"; + +import { useServices } from "@src/context/ServicesProvider"; +import { useWallet } from "@src/context/WalletProvider"; +import { useManagedWallet } from "@src/hooks/useManagedWallet"; +import { useWalletBalance } from "@src/hooks/useWalletBalance"; + +const POLLING_INTERVAL_MS = 2000; +const MAX_POLLING_DURATION_MS = 30000; +const MAX_ATTEMPTS = MAX_POLLING_DURATION_MS / POLLING_INTERVAL_MS; + +export const DEPENDENCIES = { + useWallet, + useWalletBalance, + useManagedWallet, + useServices, + useSnackbar, + Snackbar +}; + +export interface PaymentPollingContextType { + /** + * Start polling for balance updates after payment. + */ + pollForPayment: (initialBalance?: number | null) => void; + /** + * Stop polling (optional - usually not needed) + */ + stopPolling: () => void; + /** + * Whether currently polling + */ + isPolling: boolean; +} + +const PaymentPollingContext = createContext(null); + +export interface PaymentPollingProviderProps { + children: React.ReactNode; + dependencies?: typeof DEPENDENCIES; +} + +export const PaymentPollingProvider: React.FC = ({ children, dependencies: d = DEPENDENCIES }) => { + const { isTrialing: wasTrialing } = d.useWallet(); + const { balance: currentBalance, refetch: refetchBalance, isLoading: isBalanceLoading } = d.useWalletBalance(); + const { refetch: refetchManagedWallet, isFetching: isManagedWalletFetching } = d.useManagedWallet(); + const { enqueueSnackbar, closeSnackbar } = d.useSnackbar(); + const { analyticsService } = d.useServices(); + + const [isPolling, setIsPolling] = React.useState(false); + const pollingTimeoutRef = useRef | null>(null); + const isPollingRef = useRef(false); + const attemptCountRef = useRef(0); + const initialBalanceRef = useRef(null); + const wasTrialingRef = useRef(wasTrialing); + const initialTrialingRef = useRef(wasTrialing); + const loadingSnackbarKeyRef = useRef(null); + + const stopPolling = useCallback(() => { + if (pollingTimeoutRef.current) { + clearTimeout(pollingTimeoutRef.current); + pollingTimeoutRef.current = null; + } + isPollingRef.current = false; + attemptCountRef.current = 0; + setIsPolling(false); + initialBalanceRef.current = null; + initialTrialingRef.current = wasTrialing; + + if (loadingSnackbarKeyRef.current) { + closeSnackbar(loadingSnackbarKeyRef.current); + loadingSnackbarKeyRef.current = null; + } + }, [closeSnackbar, wasTrialing]); + + const executePoll = useCallback(() => { + attemptCountRef.current++; + + if (attemptCountRef.current > MAX_ATTEMPTS) { + stopPolling(); + enqueueSnackbar(, { + variant: "warning" + }); + return; + } + + try { + refetchBalance(); + refetchManagedWallet(); + } catch (error) { + console.error("Error during polling:", error); + } + }, [stopPolling, enqueueSnackbar, refetchBalance, refetchManagedWallet, d]); + + const pollForPayment = useCallback( + (initialBalance?: number | null) => { + if (isPolling) { + return; + } + + const balanceToUse = initialBalance ?? currentBalance?.totalUsd ?? null; + initialBalanceRef.current = balanceToUse; + initialTrialingRef.current = wasTrialing; + isPollingRef.current = true; + attemptCountRef.current = 0; + setIsPolling(true); + + const loadingSnackbarKey = enqueueSnackbar(, { + variant: "info", + autoHideDuration: null, + persist: true + }); + loadingSnackbarKeyRef.current = loadingSnackbarKey; + + // Start the first poll immediately + executePoll(); + }, + [isPolling, currentBalance, executePoll, enqueueSnackbar, wasTrialing, d] + ); + + useEffect( + function updateWasTrialingRef() { + wasTrialingRef.current = wasTrialing; + }, + [wasTrialing] + ); + + useEffect( + function handleRefetchCompletion() { + if (!isPolling) { + return; + } + + if (!isBalanceLoading && !isManagedWalletFetching) { + // Schedule next poll if still polling + if (isPollingRef.current) { + // Clear any existing timeout to prevent multiple timers + if (pollingTimeoutRef.current) { + clearTimeout(pollingTimeoutRef.current); + pollingTimeoutRef.current = null; + } + + pollingTimeoutRef.current = setTimeout(() => { + if (isPollingRef.current) { + executePoll(); + } + }, POLLING_INTERVAL_MS); + } + } + }, + [isPolling, isBalanceLoading, isManagedWalletFetching, executePoll] + ); + + useEffect( + function checkForPaymentCompletion() { + if (!isPolling || !currentBalance || initialBalanceRef.current == null) { + return; + } + + const currentTotalBalance = currentBalance.totalUsd; + const initialBalanceValue = initialBalanceRef.current; + + if (currentTotalBalance > initialBalanceValue) { + enqueueSnackbar(, { variant: "success" }); + + stopPolling(); + + // Track analytics for trial users after stopping polling + if (initialTrialingRef.current) { + analyticsService.track("trial_completed", { + category: "user", + label: "First payment completed" + }); + } + } + }, + [isPolling, currentBalance, stopPolling, enqueueSnackbar, analyticsService, d] + ); + + useEffect( + function checkForTrialStatusChange() { + if (!isPolling || !initialTrialingRef.current) { + return; + } + + if (initialTrialingRef.current && !wasTrialing) { + stopPolling(); + + enqueueSnackbar( + , + { variant: "success", autoHideDuration: 10_000 } + ); + } + }, + [isPolling, wasTrialing, stopPolling, enqueueSnackbar, d] + ); + + useEffect( + function stopPollingOnUnmount() { + return () => { + stopPolling(); + }; + }, + [stopPolling] + ); + + const contextValue: PaymentPollingContextType = { + pollForPayment, + stopPolling, + isPolling + }; + + return {children}; +}; + +export const usePaymentPolling = (): PaymentPollingContextType => { + const context = useContext(PaymentPollingContext); + if (!context) { + throw new Error("usePaymentPolling must be used within a PaymentPollingProvider"); + } + return context; +}; diff --git a/apps/deploy-web/src/context/PaymentPollingProvider/index.ts b/apps/deploy-web/src/context/PaymentPollingProvider/index.ts new file mode 100644 index 0000000000..6c654907c4 --- /dev/null +++ b/apps/deploy-web/src/context/PaymentPollingProvider/index.ts @@ -0,0 +1,2 @@ +export { PaymentPollingProvider, usePaymentPolling, DEPENDENCIES } from "./PaymentPollingProvider"; +export type { PaymentPollingContextType, PaymentPollingProviderProps } from "./PaymentPollingProvider"; diff --git a/apps/deploy-web/src/hooks/useManagedWallet.ts b/apps/deploy-web/src/hooks/useManagedWallet.ts index ad17d7c94e..7f39925fea 100644 --- a/apps/deploy-web/src/hooks/useManagedWallet.ts +++ b/apps/deploy-web/src/hooks/useManagedWallet.ts @@ -18,10 +18,10 @@ export const useManagedWallet = () => { const { user: signedInUser } = useCustomUser(); const userWallet = useSelectedChain(); const [selectedWalletType, setSelectedWalletType] = useAtom(walletStore.selectedWalletType); - const { data: queried, isFetched, isLoading: isFetching, refetch } = useManagedWalletQuery(isBillingEnabled ? user?.id : undefined); + const { data: queried, isFetched, isLoading: isInitialLoading, isFetching, refetch } = useManagedWalletQuery(isBillingEnabled ? user?.id : undefined); const { mutate: create, data: created, isPending: isCreating, isSuccess: isCreated, error: createError } = useCreateManagedWalletMutation(); const wallet = useMemo(() => (queried || created) as ApiManagedWalletOutput, [queried, created]); - const isLoading = isFetching || isCreating; + const isLoading = isInitialLoading || isCreating; const [, setIsSignedInWithTrial] = useAtom(walletStore.isSignedInWithTrial); const selected = getSelectedStorageWallet(); @@ -35,7 +35,7 @@ export const useManagedWallet = () => { if (signedInUser?.id && (!!queried || !!created)) { setIsSignedInWithTrial(true); } - }, [signedInUser?.id, queried, created]); + }, [signedInUser?.id, queried, created, setIsSignedInWithTrial]); useEffect(() => { if (!isBillingEnabled) { @@ -81,8 +81,9 @@ export const useManagedWallet = () => { } : undefined, isLoading, + isFetching, createError, refetch }; - }, [wallet, selected?.address, isLoading, createError, refetch, user?.id, create]); + }, [wallet, selected?.address, isLoading, isFetching, createError, refetch, user?.id, create]); }; diff --git a/apps/deploy-web/src/pages/_app.tsx b/apps/deploy-web/src/pages/_app.tsx index a411886765..a2374be781 100644 --- a/apps/deploy-web/src/pages/_app.tsx +++ b/apps/deploy-web/src/pages/_app.tsx @@ -31,6 +31,7 @@ import { CustomChainProvider } from "@src/context/CustomChainProvider"; import { ColorModeProvider } from "@src/context/CustomThemeContext"; import { FlagProvider } from "@src/context/FlagProvider/FlagProvider"; import { LocalNoteProvider } from "@src/context/LocalNoteProvider"; +import { PaymentPollingProvider } from "@src/context/PaymentPollingProvider"; import { PricingProvider } from "@src/context/PricingProvider/PricingProvider"; import { ServicesProvider } from "@src/context/ServicesProvider"; import { RootContainerProvider, useRootContainer } from "@src/context/ServicesProvider/RootContainerProvider"; @@ -77,13 +78,15 @@ const App: React.FunctionComponent = props => { - - - - - - - + + + + + + + + + diff --git a/apps/deploy-web/src/pages/payment.tsx b/apps/deploy-web/src/pages/payment.tsx index 5dc2851cb2..7e6a87a643 100644 --- a/apps/deploy-web/src/pages/payment.tsx +++ b/apps/deploy-web/src/pages/payment.tsx @@ -10,6 +10,7 @@ import { PaymentMethodsList } from "@src/components/shared/PaymentMethodsList"; import { Title } from "@src/components/shared/Title"; import { AddPaymentMethodPopup, DeletePaymentMethodPopup, PaymentForm } from "@src/components/user/payment"; import { PaymentSuccessAnimation } from "@src/components/user/payment/PaymentSuccessAnimation"; +import { usePaymentPolling } from "@src/context/PaymentPollingProvider"; import { useWallet } from "@src/context/WalletProvider"; import { use3DSecure } from "@src/hooks/use3DSecure"; import { useUser } from "@src/hooks/useUser"; @@ -44,8 +45,10 @@ const PayPage: React.FunctionComponent = () => { applyCoupon: { isPending: isApplyingCoupon, mutateAsync: applyCoupon }, removePaymentMethod } = usePaymentMutations(); + const { pollForPayment, isPolling } = usePaymentPolling(); const threeDSecure = use3DSecure({ onSuccess: () => { + pollForPayment(); setShowPaymentSuccess({ amount: submittedAmountRef.current, show: true }); setAmount(""); setCoupon(""); @@ -108,6 +111,7 @@ const PayPage: React.FunctionComponent = () => { paymentMethodId }); } else if (response.success) { + pollForPayment(); setShowPaymentSuccess({ amount: submittedAmountRef.current, show: true }); setAmount(""); setCoupon(""); @@ -149,6 +153,7 @@ const PayPage: React.FunctionComponent = () => { } if (response.amountAdded && response.amountAdded > 0) { + pollForPayment(); setShowPaymentSuccess({ amount: response.amountAdded.toString(), show: true }); } @@ -308,7 +313,7 @@ const PayPage: React.FunctionComponent = () => { onClaimCoupon={handleClaimCoupon} discounts={discounts} getFinalAmount={getFinalAmount} - processing={isConfirmingPayment} + processing={isConfirmingPayment || isPolling} selectedPaymentMethodId={selectedPaymentMethodId} onPayment={handlePayment} isApplyingCoupon={isApplyingCoupon} diff --git a/apps/deploy-web/src/queries/queryKeys.ts b/apps/deploy-web/src/queries/queryKeys.ts index 674c9e181c..d7632cd9d9 100644 --- a/apps/deploy-web/src/queries/queryKeys.ts +++ b/apps/deploy-web/src/queries/queryKeys.ts @@ -73,6 +73,8 @@ export class QueryKeys { static getPaymentMethodsKey = () => ["PAYMENT_METHODS"]; static getPaymentDiscountsKey = () => ["PAYMENT_DISCOUNTS"]; + static getManagedWalletKey = (userId?: string) => ["MANAGED_WALLET", userId || ""]; + static getPaymentTransactionsKey = (options?: { limit?: number; startingAfter?: string | null; diff --git a/apps/deploy-web/src/queries/useManagedWalletQuery.ts b/apps/deploy-web/src/queries/useManagedWalletQuery.ts index 3736f5e412..2867a53999 100644 --- a/apps/deploy-web/src/queries/useManagedWalletQuery.ts +++ b/apps/deploy-web/src/queries/useManagedWalletQuery.ts @@ -2,13 +2,12 @@ import type { QueryKey } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useServices } from "@src/context/ServicesProvider/ServicesProvider"; - -const MANAGED_WALLET = "MANAGED_WALLET"; +import { QueryKeys } from "./queryKeys"; export function useManagedWalletQuery(userId?: string) { const { managedWalletService } = useServices(); return useQuery({ - queryKey: [MANAGED_WALLET, userId || ""] as QueryKey, + queryKey: QueryKeys.getManagedWalletKey(userId) as QueryKey, queryFn: async () => { if (userId) { return await managedWalletService.getWallet(userId); @@ -28,7 +27,7 @@ export function useCreateManagedWalletMutation() { onSuccess: response => { // Only update cache if it's a wallet response, not a 3D Secure response if (!response.requires3DS) { - queryClient.setQueryData([MANAGED_WALLET, response.userId], () => response); + queryClient.setQueryData(QueryKeys.getManagedWalletKey(response.userId), () => response); } } }); diff --git a/apps/deploy-web/src/services/analytics/analytics.service.ts b/apps/deploy-web/src/services/analytics/analytics.service.ts index f1ab8d177f..9ae21c6cab 100644 --- a/apps/deploy-web/src/services/analytics/analytics.service.ts +++ b/apps/deploy-web/src/services/analytics/analytics.service.ts @@ -69,6 +69,7 @@ export type AnalyticsEvent = | "user_settings_save" | "anonymous_user_created" | "trial_started" + | "trial_completed" | "create_api_key" | "delete_api_key" | "close_deposit_modal" diff --git a/apps/deploy-web/tests/seeders/index.ts b/apps/deploy-web/tests/seeders/index.ts index 4bda9c599b..3d5048c171 100644 --- a/apps/deploy-web/tests/seeders/index.ts +++ b/apps/deploy-web/tests/seeders/index.ts @@ -5,6 +5,7 @@ export * from "./block"; export * from "./deployment"; export * from "./deploymentAlert"; export * from "./deploymentBid"; +export * from "./managedWallet"; export * from "./manifest"; export * from "./notificationChannel"; export * from "./payment"; @@ -12,4 +13,5 @@ export * from "./provider"; export * from "./usage"; export * from "./user"; export * from "./wallet"; +export * from "./walletBalance"; export * from "./sdlService"; diff --git a/apps/deploy-web/tests/seeders/managedWallet.ts b/apps/deploy-web/tests/seeders/managedWallet.ts new file mode 100644 index 0000000000..a34709e82e --- /dev/null +++ b/apps/deploy-web/tests/seeders/managedWallet.ts @@ -0,0 +1,13 @@ +import type { ApiManagedWalletOutput } from "@akashnetwork/http-sdk"; +import { faker } from "@faker-js/faker"; + +export const buildManagedWallet = (overrides: Partial = {}): ApiManagedWalletOutput => ({ + id: faker.string.uuid(), + userId: faker.string.uuid(), + address: `akash${faker.string.alphanumeric({ length: 39 })}`, + isTrialing: faker.datatype.boolean(), + creditAmount: faker.number.int({ min: 0, max: 1000 }), + username: "Managed Wallet" as const, + isWalletConnected: true, + ...overrides +}); diff --git a/apps/deploy-web/tests/seeders/walletBalance.ts b/apps/deploy-web/tests/seeders/walletBalance.ts new file mode 100644 index 0000000000..6720a447dd --- /dev/null +++ b/apps/deploy-web/tests/seeders/walletBalance.ts @@ -0,0 +1,18 @@ +import { faker } from "@faker-js/faker"; + +import type { WalletBalance } from "@src/hooks/useWalletBalance"; + +export const buildWalletBalance = (overrides: Partial = {}): WalletBalance => ({ + totalUsd: faker.number.float({ min: 0, max: 10000, fractionDigits: 2 }), + balanceUAKT: faker.number.int({ min: 0, max: 1000000 }), + balanceUUSDC: faker.number.int({ min: 0, max: 1000000 }), + totalUAKT: faker.number.int({ min: 0, max: 1000000 }), + totalUUSDC: faker.number.int({ min: 0, max: 1000000 }), + totalDeploymentEscrowUAKT: faker.number.int({ min: 0, max: 100000 }), + totalDeploymentEscrowUUSDC: faker.number.int({ min: 0, max: 100000 }), + totalDeploymentEscrowUSD: faker.number.float({ min: 0, max: 1000, fractionDigits: 2 }), + totalDeploymentGrantsUAKT: faker.number.int({ min: 0, max: 100000 }), + totalDeploymentGrantsUUSDC: faker.number.int({ min: 0, max: 100000 }), + totalDeploymentGrantsUSD: faker.number.float({ min: 0, max: 1000, fractionDigits: 2 }), + ...overrides +});