= ({ dseq, dependencies
recommend signing up and adding funds to your account.
- handleCloseDeployment()} variant="outline" type="button" size="sm" className="mr-4">
+ handleCloseDeployment()}
+ variant="outline"
+ type="button"
+ size="sm"
+ className="mr-4"
+ disabled={settings.isBlockchainDown}
+ >
Close Deployment
diff --git a/apps/deploy-web/src/components/new-deployment/ManifestEdit.tsx b/apps/deploy-web/src/components/new-deployment/ManifestEdit.tsx
index e036997228..beefd2978c 100644
--- a/apps/deploy-web/src/components/new-deployment/ManifestEdit.tsx
+++ b/apps/deploy-web/src/components/new-deployment/ManifestEdit.tsx
@@ -351,7 +351,7 @@ export const ManifestEdit: React.FunctionComponent = ({
handleCreateDeployment()}
className="w-full whitespace-nowrap sm:w-auto"
data-testid="create-deployment-btn"
diff --git a/apps/deploy-web/src/components/settings/SettingsContainer.tsx b/apps/deploy-web/src/components/settings/SettingsContainer.tsx
index 78115ff6d8..3b14eccdd9 100644
--- a/apps/deploy-web/src/components/settings/SettingsContainer.tsx
+++ b/apps/deploy-web/src/components/settings/SettingsContainer.tsx
@@ -9,6 +9,7 @@ import { AutoTopUpSettingContainer } from "@src/components/settings/AutoTopUpSet
import { LocalDataManager } from "@src/components/settings/LocalDataManager";
import { Fieldset } from "@src/components/shared/Fieldset";
import { LabelValue } from "@src/components/shared/LabelValue";
+import { useSettings } from "@src/context/SettingsProvider";
import { useWallet } from "@src/context/WalletProvider";
import { useFlag } from "@src/hooks/useFlag";
import { useWhen } from "@src/hooks/useWhen";
@@ -21,6 +22,7 @@ import { SettingsForm } from "./SettingsForm";
import { SettingsLayout, SettingsTabs } from "./SettingsLayout";
export const SettingsContainer: React.FunctionComponent = () => {
+ const { settings } = useSettings();
const [isSelectingNetwork, setIsSelectingNetwork] = useState(false);
const selectedNetwork = networkStore.useSelectedNetwork();
const wallet = useWallet();
@@ -68,9 +70,11 @@ export const SettingsContainer: React.FunctionComponent = () => {
)}
-
-
-
+ {!settings.isBlockchainDown && (
+
+
+
+ )}
);
diff --git a/apps/deploy-web/src/components/wallet/ConnectManagedWalletButton.spec.tsx b/apps/deploy-web/src/components/wallet/ConnectManagedWalletButton.spec.tsx
new file mode 100644
index 0000000000..883b205b7f
--- /dev/null
+++ b/apps/deploy-web/src/components/wallet/ConnectManagedWalletButton.spec.tsx
@@ -0,0 +1,54 @@
+import { mock } from "jest-mock-extended";
+
+import type { useFlag } from "@src/hooks/useFlag";
+import { ConnectManagedWalletButton } from "./ConnectManagedWalletButton";
+
+import { render } from "@testing-library/react";
+
+describe(ConnectManagedWalletButton.name, () => {
+ it("renders button enabled when blockchain is up", () => {
+ const { getByText } = setup({ isBlockchainDown: false });
+
+ expect(getByText("Start Trial").parentElement).not.toHaveAttribute("disabled");
+ });
+
+ it("renders button disabled when blockchain is down", () => {
+ const { getByText } = setup({ isBlockchainDown: true });
+
+ expect(getByText("Start Trial").parentElement).toHaveAttribute("disabled");
+ });
+
+ function setup(input?: { isRegistered?: boolean; isBlockchainDown?: boolean }) {
+ const mockUseFlag = jest.fn((flag: string) => {
+ if (flag === "notifications_general_alerts_update") {
+ return true;
+ }
+ return false;
+ }) as unknown as ReturnType;
+
+ return render(
+ mockUseFlag,
+ useRouter: () => mock(),
+ useSettings: () => ({
+ settings: {
+ apiEndpoint: "https://api.example.com",
+ rpcEndpoint: "https://rpc.example.com",
+ isCustomNode: false,
+ nodes: [],
+ selectedNode: null,
+ customNode: null,
+ isBlockchainDown: input?.isBlockchainDown ?? false
+ },
+ setSettings: jest.fn(),
+ isLoadingSettings: false,
+ isSettingsInit: true,
+ refreshNodeStatuses: jest.fn(),
+ isRefreshingNodeStatus: false
+ })
+ }}
+ />
+ );
+ }
+});
diff --git a/apps/deploy-web/src/components/wallet/ConnectManagedWalletButton.tsx b/apps/deploy-web/src/components/wallet/ConnectManagedWalletButton.tsx
index 3d1087b411..803a86afb0 100644
--- a/apps/deploy-web/src/components/wallet/ConnectManagedWalletButton.tsx
+++ b/apps/deploy-web/src/components/wallet/ConnectManagedWalletButton.tsx
@@ -7,19 +7,28 @@ import { cn } from "@akashnetwork/ui/utils";
import { Rocket } from "iconoir-react";
import { useRouter } from "next/navigation";
+import { useSettings } from "@src/context/SettingsProvider";
import { useWallet } from "@src/context/WalletProvider";
import { useFlag } from "@src/hooks/useFlag";
import { UrlService } from "@src/utils/urlUtils";
+const DEPENDENCIES = {
+ useFlag,
+ useRouter,
+ useSettings
+};
+
interface Props extends ButtonProps {
children?: ReactNode;
className?: string;
+ dependencies?: typeof DEPENDENCIES;
}
-export const ConnectManagedWalletButton: React.FunctionComponent = ({ className = "", ...rest }) => {
+export const ConnectManagedWalletButton: React.FunctionComponent = ({ className = "", dependencies: d = DEPENDENCIES, ...rest }) => {
+ const { settings } = d.useSettings();
const { connectManagedWallet, hasManagedWallet, isWalletLoading } = useWallet();
- const allowAnonymousUserTrial = useFlag("anonymous_free_trial");
- const router = useRouter();
+ const allowAnonymousUserTrial = d.useFlag("anonymous_free_trial");
+ const router = d.useRouter();
const startTrial: React.MouseEventHandler = useCallback(() => {
if (allowAnonymousUserTrial) {
@@ -35,7 +44,7 @@ export const ConnectManagedWalletButton: React.FunctionComponent = ({ cla
onClick={startTrial}
className={cn("border-primary bg-primary/10 dark:bg-primary", className)}
{...rest}
- disabled={isWalletLoading}
+ disabled={settings.isBlockchainDown || isWalletLoading}
>
{isWalletLoading ? : }
{hasManagedWallet ? "Switch to USD Payments" : "Start Trial"}
diff --git a/apps/deploy-web/src/context/SettingsProvider/SettingsProviderContext.tsx b/apps/deploy-web/src/context/SettingsProvider/SettingsProviderContext.tsx
index 017f30efb0..5f33341e12 100644
--- a/apps/deploy-web/src/context/SettingsProvider/SettingsProviderContext.tsx
+++ b/apps/deploy-web/src/context/SettingsProvider/SettingsProviderContext.tsx
@@ -40,7 +40,7 @@ type ContextType = {
export type SettingsContextType = ContextType;
-const SettingsProviderContext = React.createContext({} as ContextType);
+export const SettingsProviderContext = React.createContext({} as ContextType);
const defaultSettings: Settings = {
apiEndpoint: "",
diff --git a/apps/deploy-web/src/context/TopBannerProvider/TopBannerProvider.tsx b/apps/deploy-web/src/context/TopBannerProvider/TopBannerProvider.tsx
new file mode 100644
index 0000000000..6c847eef57
--- /dev/null
+++ b/apps/deploy-web/src/context/TopBannerProvider/TopBannerProvider.tsx
@@ -0,0 +1,43 @@
+import React, { useMemo, useState } from "react";
+
+import { useHasCreditCardBanner } from "@src/hooks/useHasCreditCardBanner";
+import { useVariant } from "@src/hooks/useVariant";
+import { useWhen } from "@src/hooks/useWhen";
+import type { FCWithChildren } from "@src/types/component";
+import { useSettings } from "../SettingsProvider";
+
+interface ITopBannerContext {
+ hasBanner: boolean;
+ setIsMaintenanceBannerOpen: (isMaintenanceBannerOpen: boolean) => void;
+ isMaintenanceBannerOpen: boolean;
+ isBlockchainDown: boolean;
+ hasCreditCardBanner: boolean;
+}
+
+const TopBannerContext = React.createContext({} as ITopBannerContext);
+
+export const TopBannerProvider: FCWithChildren = ({ children }) => {
+ const maintenanceBannerFlag = useVariant("maintenance_banner");
+ const { settings } = useSettings();
+ const hasCreditCardBanner = useHasCreditCardBanner();
+
+ const [isMaintenanceBannerOpen, setIsMaintenanceBannerOpen] = useState(!!maintenanceBannerFlag.enabled);
+ useWhen(maintenanceBannerFlag.enabled, () => setIsMaintenanceBannerOpen(true));
+
+ const hasBanner = useMemo(
+ () => isMaintenanceBannerOpen || settings.isBlockchainDown || hasCreditCardBanner,
+ [isMaintenanceBannerOpen, settings.isBlockchainDown, hasCreditCardBanner]
+ );
+
+ const value = {
+ hasBanner,
+ isMaintenanceBannerOpen,
+ setIsMaintenanceBannerOpen,
+ isBlockchainDown: settings.isBlockchainDown,
+ hasCreditCardBanner
+ };
+
+ return {children} ;
+};
+
+export const useTopBanner = () => ({ ...React.useContext(TopBannerContext) });
diff --git a/apps/deploy-web/src/context/TopBannerProvider/index.ts b/apps/deploy-web/src/context/TopBannerProvider/index.ts
new file mode 100644
index 0000000000..4e2285d150
--- /dev/null
+++ b/apps/deploy-web/src/context/TopBannerProvider/index.ts
@@ -0,0 +1 @@
+export { useTopBanner, TopBannerProvider } from "./TopBannerProvider";
diff --git a/apps/deploy-web/src/hooks/useHasCreditCardBanner.ts b/apps/deploy-web/src/hooks/useHasCreditCardBanner.ts
index 9e9fa9aaa7..1591847adc 100644
--- a/apps/deploy-web/src/hooks/useHasCreditCardBanner.ts
+++ b/apps/deploy-web/src/hooks/useHasCreditCardBanner.ts
@@ -8,15 +8,15 @@ import { useUser } from "./useUser";
const withBilling = browserEnvConfig.NEXT_PUBLIC_BILLING_ENABLED;
-export function useHasCreditCardBanner(isMaintenanceBannerOpen: boolean) {
+export function useHasCreditCardBanner() {
const { user } = useUser();
const [isBannerVisible, setIsBannerVisible] = useState(false);
const [isInitialized, setIsInitialized] = useState(false);
const { hasManagedWallet, isWalletLoading } = useWallet();
const [isSignedInWithTrial] = useAtom(walletStore.isSignedInWithTrial);
const shouldShowBanner = useMemo(
- () => !isMaintenanceBannerOpen && isInitialized && withBilling && !hasManagedWallet && !isWalletLoading && !isSignedInWithTrial,
- [isInitialized, hasManagedWallet, isWalletLoading, isSignedInWithTrial, isMaintenanceBannerOpen]
+ () => isInitialized && withBilling && !hasManagedWallet && !isWalletLoading && !isSignedInWithTrial,
+ [isInitialized, hasManagedWallet, isWalletLoading, isSignedInWithTrial]
);
useEffect(() => {
diff --git a/apps/deploy-web/src/queries/useGrantsQuery.spec.tsx b/apps/deploy-web/src/queries/useGrantsQuery.spec.tsx
index e059724c3b..cb19f302f9 100644
--- a/apps/deploy-web/src/queries/useGrantsQuery.spec.tsx
+++ b/apps/deploy-web/src/queries/useGrantsQuery.spec.tsx
@@ -3,11 +3,37 @@ import { faker } from "@faker-js/faker";
import type { AxiosInstance } from "axios";
import { mock } from "jest-mock-extended";
+import type { SettingsContextType } from "@src/context/SettingsProvider/SettingsProviderContext";
+import { SettingsProviderContext } from "@src/context/SettingsProvider/SettingsProviderContext";
import { useAllowancesGranted, useAllowancesIssued, useGranteeGrants, useGranterGrants } from "./useGrantsQuery";
import { waitFor } from "@testing-library/react";
import { setupQuery } from "@tests/unit/query-client";
+const createMockSettingsContext = (): SettingsContextType =>
+ mock({
+ settings: {
+ apiEndpoint: "https://api.example.com",
+ rpcEndpoint: "https://rpc.example.com",
+ isCustomNode: false,
+ nodes: [],
+ selectedNode: null,
+ customNode: null,
+ isBlockchainDown: false
+ },
+ setSettings: jest.fn(),
+ isLoadingSettings: false,
+ isSettingsInit: true,
+ refreshNodeStatuses: jest.fn(),
+ isRefreshingNodeStatus: false
+ });
+
+const MockSettingsProvider = ({ children }: { children: React.ReactNode }) => {
+ const mockSettings = createMockSettingsContext();
+
+ return {children} ;
+};
+
describe("useGrantsQuery", () => {
describe(useGranterGrants.name, () => {
it("fetches granter grants when address is provided", async () => {
@@ -34,7 +60,8 @@ describe("useGrantsQuery", () => {
const { result } = setupQuery(() => useGranterGrants("test-address", 0, 1000), {
services: {
authzHttpService: () => authzHttpService
- }
+ },
+ wrapper: ({ children }) => {children}
});
await waitFor(() => {
@@ -52,7 +79,8 @@ describe("useGrantsQuery", () => {
setupQuery(() => useGranterGrants("", 0, 1000), {
services: {
authzHttpService: () => authzHttpService
- }
+ },
+ wrapper: ({ children }) => {children}
});
expect(authzHttpService.getPaginatedDepositDeploymentGrants).not.toHaveBeenCalled();
@@ -76,7 +104,8 @@ describe("useGrantsQuery", () => {
const { result } = setupQuery(() => useGranteeGrants("test-address"), {
services: {
authzHttpService: () => authzHttpService
- }
+ },
+ wrapper: ({ children }) => {children}
});
await waitFor(() => {
@@ -94,7 +123,8 @@ describe("useGrantsQuery", () => {
setupQuery(() => useGranteeGrants(""), {
services: {
authzHttpService: () => authzHttpService
- }
+ },
+ wrapper: ({ children }) => {children}
});
expect(authzHttpService.getAllDepositDeploymentGrants).not.toHaveBeenCalled();
@@ -115,7 +145,8 @@ describe("useGrantsQuery", () => {
const { result } = setupQuery(() => useAllowancesIssued("test-address", 0, 1000), {
services: {
authzHttpService: () => authzHttpService
- }
+ },
+ wrapper: ({ children }) => {children}
});
await waitFor(() => {
@@ -133,7 +164,8 @@ describe("useGrantsQuery", () => {
setupQuery(() => useAllowancesIssued("", 0, 1000), {
services: {
authzHttpService: () => authzHttpService
- }
+ },
+ wrapper: ({ children }) => {children}
});
expect(authzHttpService.getPaginatedFeeAllowancesForGranter).not.toHaveBeenCalled();
@@ -156,7 +188,8 @@ describe("useGrantsQuery", () => {
const { result } = setupQuery(() => useAllowancesGranted("test-address"), {
services: {
chainApiHttpClient: () => chainApiHttpClient
- }
+ },
+ wrapper: ({ children }) => {children}
});
await waitFor(() => {
@@ -178,7 +211,12 @@ describe("useGrantsQuery", () => {
}
})
} as any);
- setupQuery(() => useAllowancesGranted(""));
+ setupQuery(() => useAllowancesGranted(""), {
+ services: {
+ chainApiHttpClient: () => chainApiHttpClient
+ },
+ wrapper: ({ children }) => {children}
+ });
expect(chainApiHttpClient.get).not.toHaveBeenCalled();
});
diff --git a/apps/deploy-web/src/queries/useGrantsQuery.ts b/apps/deploy-web/src/queries/useGrantsQuery.ts
index 2870624197..4cc27686a9 100644
--- a/apps/deploy-web/src/queries/useGrantsQuery.ts
+++ b/apps/deploy-web/src/queries/useGrantsQuery.ts
@@ -4,6 +4,7 @@ import { useQuery } from "@tanstack/react-query";
import type { AxiosInstance } from "axios";
import { useServices } from "@src/context/ServicesProvider";
+import { useSettings } from "@src/context/SettingsProvider/SettingsProviderContext";
import type { AllowanceType, PaginatedAllowanceType, PaginatedGrantType } from "@src/types/grant";
import { ApiUrlService, loadWithPagination } from "@src/utils/apiUtils";
import { QueryKeys } from "./queryKeys";
@@ -14,10 +15,11 @@ export function useGranterGrants(
limit: number,
options: Omit, "queryKey" | "queryFn"> = {}
) {
+ const { settings } = useSettings();
const { authzHttpService } = useServices();
const offset = page * limit;
- options.enabled = options.enabled !== false && !!address && authzHttpService.isReady;
+ options.enabled = options.enabled !== false && !!address && authzHttpService.isReady && !settings.isBlockchainDown;
return useQuery({
queryKey: QueryKeys.getGranterGrants(address, page, offset),
@@ -27,9 +29,10 @@ export function useGranterGrants(
}
export function useGranteeGrants(address: string, options: Omit, "queryKey" | "queryFn"> = {}) {
+ const { settings } = useSettings();
const { authzHttpService } = useServices();
- options.enabled = options.enabled !== false && !!address && authzHttpService.isReady;
+ options.enabled = options.enabled !== false && !!address && authzHttpService.isReady && !settings.isBlockchainDown;
return useQuery({
queryKey: QueryKeys.getGranteeGrants(address || "UNDEFINED"),
@@ -44,10 +47,11 @@ export function useAllowancesIssued(
limit: number,
options: Omit, "queryKey" | "queryFn"> = {}
) {
+ const { settings } = useSettings();
const { authzHttpService } = useServices();
const offset = page * limit;
- options.enabled = options.enabled !== false && !!address && authzHttpService.isReady;
+ options.enabled = options.enabled !== false && !!address && authzHttpService.isReady && !settings.isBlockchainDown;
return useQuery({
queryKey: QueryKeys.getAllowancesIssued(address, page, offset),
@@ -61,9 +65,10 @@ async function getAllowancesGranted(chainApiHttpClient: AxiosInstance, address:
}
export function useAllowancesGranted(address: string, options: Omit, "queryKey" | "queryFn"> = {}) {
+ const { settings } = useSettings();
const { chainApiHttpClient } = useServices();
- options.enabled = options.enabled !== false && !!address && !!chainApiHttpClient.defaults.baseURL;
+ options.enabled = options.enabled !== false && !!address && !!chainApiHttpClient.defaults.baseURL && !settings.isBlockchainDown;
return useQuery({
queryKey: address ? QueryKeys.getAllowancesGranted(address) : [],
diff --git a/packages/ui/components/button.tsx b/packages/ui/components/button.tsx
index 819b3c6e4a..2f23d60967 100644
--- a/packages/ui/components/button.tsx
+++ b/packages/ui/components/button.tsx
@@ -6,7 +6,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "../utils/cn";
const buttonVariants = cva(
- "ring-offset-background focus-visible:ring-ring relative inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
+ "ring-offset-background focus-visible:ring-ring relative inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 aria-[disabled='true']:pointer-events-none aria-[disabled='true']:opacity-50",
{
variants: {
variant: {