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
+});