diff --git a/apps/deploy-web/src/components/authorizations/Authorizations.tsx b/apps/deploy-web/src/components/authorizations/Authorizations.tsx index 8c335459f3..db10b6d4a6 100644 --- a/apps/deploy-web/src/components/authorizations/Authorizations.tsx +++ b/apps/deploy-web/src/components/authorizations/Authorizations.tsx @@ -7,6 +7,7 @@ import { NextSeo } from "next-seo"; import { Fieldset } from "@src/components/shared/Fieldset"; import { browserEnvConfig } from "@src/config/browser-env.config"; +import { useSettings } from "@src/context/SettingsProvider"; import { useWallet } from "@src/context/WalletProvider"; import { useAllowance } from "@src/hooks/useAllowance"; import { useExactDeploymentGrantsQuery } from "@src/queries/useExactDeploymentGrantsQuery"; @@ -46,6 +47,7 @@ const selectNonMasterAllowances = (data: PaginatedAllowanceType) => ({ }); export const Authorizations: React.FunctionComponent = () => { + const { settings } = useSettings(); const { address, signAndBroadcastTx, isManaged } = useWallet(); const { fee: { all: allowancesGranted, isLoading: isLoadingAllowancesGranted, setDefault, default: defaultAllowance } @@ -207,246 +209,260 @@ export const Authorizations: React.FunctionComponent = () => { - - - - ) - } - > - {!address ? ( + {settings.isBlockchainDown ? ( + <> -
- -
+

The blockchain is down. Unable to create, list, or update authorizations.

- ) : ( - <> -

- These authorizations allow you authorize other addresses to spend on deployments or deployment deposits using your funds. You can revoke these - authorizations at any time. -

-
-
- { - setSearchGrantee(""); - setSearchError(null); - }} - > - - - } - /> - -
- {isLoadingGranterGrants || !filteredGranterGrants ? ( -
- + + ) : ( + <> + +
- ) : ( - <> - {filteredGranterGrants?.grants?.length > 0 ? ( - + {!address ? ( + <> +
+ +
+ + ) : ( + <> +

+ These authorizations allow you authorize other addresses to spend on deployments or deployment deposits using your funds. You can revoke these + authorizations at any time. +

+
+
+ { + setSearchGrantee(""); + setSearchError(null); + }} + > + + + } /> + +
+ {isLoadingGranterGrants || !filteredGranterGrants ? ( +
+ +
) : ( -

- {searchGrantee ? (searchError ? "Please enter a valid Akash address" : "No matching authorizations found.") : "No authorizations given."} -

+ <> + {filteredGranterGrants?.grants?.length > 0 ? ( + + ) : ( +

+ {searchGrantee + ? searchError + ? "Please enter a valid Akash address" + : "No matching authorizations found." + : "No authorizations given."} +

+ )} + )} - - )} -
+
-
- {isLoadingGranteeGrants || !granteeGrants ? ( -
- -
- ) : ( - <> - {granteeGrants.length > 0 ? ( - - - - Granter - Spending Limit - Expiration - - - - - {granteeGrants.map(grant => ( - - ))} - -
+
+ {isLoadingGranteeGrants || !granteeGrants ? ( +
+ +
) : ( -

No authorizations received.

+ <> + {granteeGrants.length > 0 ? ( + + + + Granter + Spending Limit + Expiration + + + + + {granteeGrants.map(grant => ( + + ))} + +
+ ) : ( +

No authorizations received.

+ )} + )} - +
+ + )} + +
+ Tx Fee Authorizations + {address && ( + )} -
- - )} - -
- Tx Fee Authorizations - {address && ( - - )} -
- - {!address ? ( - <> -
- -
- - ) : ( - <> -

- These authorizations allow you authorize other addresses to spend on transaction fees using your funds. You can revoke these authorizations at any - time. -

- -
- {isLoadingAllowancesIssued || !allowancesIssued ? ( -
- -
- ) : ( - <> - {allowancesIssued.allowances.length > 0 ? ( - + + + {!address ? ( + <> +
+ +
+ + ) : ( + <> +

+ These authorizations allow you authorize other addresses to spend on transaction fees using your funds. You can revoke these authorizations at + any time. +

+ +
+ {isLoadingAllowancesIssued || !allowancesIssued ? ( +
+ +
) : ( -

No allowances issued.

+ <> + {allowancesIssued.allowances.length > 0 ? ( + + ) : ( +

No allowances issued.

+ )} + )} - - )} -
+
-
- {isLoadingAllowancesGranted || !allowancesGranted ? ( -
- -
- ) : ( - <> - {allowancesGranted.length > 0 ? ( - - - - Default - Type - Grantee - Spending Limit - Expiration - - - - - {!!allowancesGranted && ( - setDefault(undefined)} - selected={!defaultAllowance} - /> - )} - {allowancesGranted.map(allowance => ( - setDefault(allowance.granter)} - selected={defaultAllowance === allowance.granter} - /> - ))} - -
+
+ {isLoadingAllowancesGranted || !allowancesGranted ? ( +
+ +
) : ( -

No allowances received.

+ <> + {allowancesGranted.length > 0 ? ( + + + + Default + Type + Grantee + Spending Limit + Expiration + + + + + {!!allowancesGranted && ( + setDefault(undefined)} + selected={!defaultAllowance} + /> + )} + {allowancesGranted.map(allowance => ( + setDefault(allowance.granter)} + selected={defaultAllowance === allowance.granter} + /> + ))} + +
+ ) : ( +

No allowances received.

+ )} + )} - - )} -
- - )} - - {!!deletingGrants && ( - setDeletingGrants(null)} - onCancel={() => setDeletingGrants(null)} - onValidate={onDeleteGrantsConfirmed} - enableCloseOnBackdropClick - > - Deleting grants will revoke their ability to spend your funds on deployments. - - )} - {!!deletingAllowances && ( - setDeletingAllowances(null)} - onCancel={() => setDeletingAllowances(null)} - onValidate={onDeleteAllowanceConfirmed} - enableCloseOnBackdropClick - > - Deleting allowance to will revoke their ability to fees on your behalf. - - )} - {showGrantModal && } - {showAllowanceModal && } - +
+ + )} + + {!!deletingGrants && ( + setDeletingGrants(null)} + onCancel={() => setDeletingGrants(null)} + onValidate={onDeleteGrantsConfirmed} + enableCloseOnBackdropClick + > + Deleting grants will revoke their ability to spend your funds on deployments. + + )} + {!!deletingAllowances && ( + setDeletingAllowances(null)} + onCancel={() => setDeletingAllowances(null)} + onValidate={onDeleteAllowanceConfirmed} + enableCloseOnBackdropClick + > + Deleting allowance to will revoke their ability to fees on your behalf. + + )} + {showGrantModal && } + {showAllowanceModal && } +
+ + )}
); }; diff --git a/apps/deploy-web/src/components/deployments/CreateCertificateButton/CreateCertificateButton.spec.tsx b/apps/deploy-web/src/components/deployments/CreateCertificateButton/CreateCertificateButton.spec.tsx index a876afbab5..ea32aa5aed 100644 --- a/apps/deploy-web/src/components/deployments/CreateCertificateButton/CreateCertificateButton.spec.tsx +++ b/apps/deploy-web/src/components/deployments/CreateCertificateButton/CreateCertificateButton.spec.tsx @@ -39,9 +39,10 @@ describe(CreateCertificateButton.name, () => { isCreatingCert?: boolean; isLocalCertExpired?: boolean; localCert?: LocalCert; + isBlockchainDown?: boolean; } ) { - const { createCertificate, isCreatingCert, isLocalCertExpired, localCert, ...props } = input ?? {}; + const { createCertificate, isCreatingCert, isLocalCertExpired, localCert, isBlockchainDown, ...props } = input ?? {}; return render( { isCreatingCert: isCreatingCert ?? false, isLocalCertExpired: isLocalCertExpired ?? false, localCert: localCert ?? null - })) as (typeof CREATE_CERTIFICATE_BUTTON_DEPENDENCIES)["useCertificate"] + })) as (typeof CREATE_CERTIFICATE_BUTTON_DEPENDENCIES)["useCertificate"], + useSettings: () => ({ + settings: { + apiEndpoint: "https://api.example.com", + rpcEndpoint: "https://rpc.example.com", + isCustomNode: false, + nodes: [], + selectedNode: null, + customNode: null, + isBlockchainDown: isBlockchainDown ?? false + }, + setSettings: jest.fn(), + isLoadingSettings: false, + isSettingsInit: true, + refreshNodeStatuses: jest.fn(), + isRefreshingNodeStatus: false + }) }} /> ); diff --git a/apps/deploy-web/src/components/deployments/CreateCertificateButton/CreateCertificateButton.tsx b/apps/deploy-web/src/components/deployments/CreateCertificateButton/CreateCertificateButton.tsx index 502df66885..3d0d448495 100644 --- a/apps/deploy-web/src/components/deployments/CreateCertificateButton/CreateCertificateButton.tsx +++ b/apps/deploy-web/src/components/deployments/CreateCertificateButton/CreateCertificateButton.tsx @@ -5,12 +5,14 @@ import { Alert, Button, Spinner } from "@akashnetwork/ui/components"; import { cn } from "@akashnetwork/ui/utils"; import { useCertificate } from "@src/context/CertificateProvider"; +import { useSettings } from "@src/context/SettingsProvider"; export const DEPENDENCIES = { Alert, Button, Spinner, - useCertificate + useCertificate, + useSettings }; export interface Props extends Omit { @@ -20,6 +22,7 @@ export interface Props extends Omit { } export const CreateCertificateButton: FC = ({ afterCreate, containerClassName, dependencies: d = DEPENDENCIES, ...buttonProps }) => { + const { settings } = d.useSettings(); const { isCreatingCert, createCertificate, isLocalCertExpired, localCert } = d.useCertificate(); const _createCertificate = useCallback(async () => { @@ -38,7 +41,12 @@ export const CreateCertificateButton: FC = ({ afterCreate, containerClass {warningText} - + {isCreatingCert ? : buttonText} diff --git a/apps/deploy-web/src/components/deployments/DeploymentList.tsx b/apps/deploy-web/src/components/deployments/DeploymentList.tsx index 185ae97d2c..fbb8a590c0 100644 --- a/apps/deploy-web/src/components/deployments/DeploymentList.tsx +++ b/apps/deploy-web/src/components/deployments/DeploymentList.tsx @@ -224,7 +224,12 @@ export const DeploymentList: React.FunctionComponent = () => { {isSignedInWithTrial && !user &&

If you are expecting to see some, you may need to sign-in or connect a wallet

} {isWalletConnected ? ( - + Deploy diff --git a/apps/deploy-web/src/components/home/YourAccount.tsx b/apps/deploy-web/src/components/home/YourAccount.tsx index 682ece3daf..45efe63dee 100644 --- a/apps/deploy-web/src/components/home/YourAccount.tsx +++ b/apps/deploy-web/src/components/home/YourAccount.tsx @@ -13,6 +13,7 @@ import { AddFundsLink } from "@src/components/user/AddFundsLink"; import { browserEnvConfig } from "@src/config/browser-env.config"; import { UAKT_DENOM } from "@src/config/denom.config"; import { usePricing } from "@src/context/PricingProvider"; +import { useSettings } from "@src/context/SettingsProvider"; import { useWallet } from "@src/context/WalletProvider"; import { useUsdcDenom } from "@src/hooks/useDenom"; import { useFlag } from "@src/hooks/useFlag"; @@ -41,6 +42,7 @@ type Props = { }; export const YourAccount: React.FunctionComponent = ({ isLoadingBalances, walletBalance, activeDeployments, leases, providers }) => { + const { settings } = useSettings(); const { resolvedTheme } = useTheme(); const tw = useTailwind(); const { address, isManaged: isManagedWallet, isTrialing } = useWallet(); @@ -229,7 +231,12 @@ export const YourAccount: React.FunctionComponent = ({ isLoadingBalances, )}
- + Deploy diff --git a/apps/deploy-web/src/components/layout/Layout.tsx b/apps/deploy-web/src/components/layout/Layout.tsx index 2a4d2e2567..39c4790aaf 100644 --- a/apps/deploy-web/src/components/layout/Layout.tsx +++ b/apps/deploy-web/src/components/layout/Layout.tsx @@ -10,13 +10,12 @@ import { millisecondsInMinute } from "date-fns/constants"; import { ACCOUNT_BAR_HEIGHT } from "@src/config/ui.config"; import { useSettings } from "@src/context/SettingsProvider"; +import { TopBannerProvider, useTopBanner } from "@src/context/TopBannerProvider/TopBannerProvider"; import { useWallet } from "@src/context/WalletProvider"; -import { useHasCreditCardBanner } from "@src/hooks/useHasCreditCardBanner"; -import { useVariant } from "@src/hooks/useVariant"; import { LinearLoadingSkeleton } from "../shared/LinearLoadingSkeleton"; import { Nav } from "./Nav"; import { Sidebar } from "./Sidebar"; -import { CreditCardBanner, MaintenanceBanner } from "./TopBanner"; +import { TopBanner } from "./TopBanner"; import { TrackingScripts } from "./TrackingScripts"; import { WelcomeToTrialModal } from "./WelcomeToTrialModal"; @@ -40,21 +39,22 @@ const Layout: React.FunctionComponent = ({ children, isLoading, isUsingSe return ( - - {children} - + + + {children} + + ); }; const LayoutApp: React.FunctionComponent = ({ children, isLoading, isUsingSettings, isUsingWallet, disableContainer, containerClassName = "" }) => { - const maintenanceBannerFlag = useVariant("maintenance_banner"); const muiTheme = useMuiTheme(); const smallScreen = useMediaQuery(muiTheme.breakpoints.down("md")); const [isNavOpen, setIsNavOpen] = useState(() => { @@ -67,11 +67,9 @@ const LayoutApp: React.FunctionComponent = ({ children, isLoading, isUsin return true; }); const [isMobileOpen, setIsMobileOpen] = useState(false); - const [isMaintenanceBannerOpen, setIsMaintenanceBannerOpen] = useState(!!maintenanceBannerFlag.enabled); const { refreshNodeStatuses, isSettingsInit } = useSettings(); const { isWalletLoaded } = useWallet(); - const hasCreditCardBanner = useHasCreditCardBanner(isMaintenanceBannerOpen); - const hasBanner = hasCreditCardBanner || isMaintenanceBannerOpen; + const { hasBanner } = useTopBanner(); useEffect(() => { const refreshNodeIntervalId = setInterval(async () => { @@ -99,8 +97,7 @@ const LayoutApp: React.FunctionComponent = ({ children, isLoading, isUsin return (
- {hasCreditCardBanner && } - {isMaintenanceBannerOpen && setIsMaintenanceBannerOpen(false)} />} +
diff --git a/apps/deploy-web/src/components/layout/Sidebar.tsx b/apps/deploy-web/src/components/layout/Sidebar.tsx index 7ae32b6498..b9a5445135 100644 --- a/apps/deploy-web/src/components/layout/Sidebar.tsx +++ b/apps/deploy-web/src/components/layout/Sidebar.tsx @@ -36,6 +36,7 @@ import { useAtom } from "jotai"; import Image from "next/image"; import Link from "next/link"; +import { useSettings } from "@src/context/SettingsProvider"; import { useWallet } from "@src/context/WalletProvider"; import { useFlag } from "@src/hooks/useFlag"; import { useUser } from "@src/hooks/useUser"; @@ -60,6 +61,7 @@ const DRAWER_WIDTH = 240; const CLOSED_DRAWER_WIDTH = 57; export const Sidebar: React.FunctionComponent = ({ isMobileOpen, handleDrawerToggle, isNavOpen, onOpenMenuClick, mdDrawerClassName }) => { + const { settings } = useSettings(); const [, setDeploySdl] = useAtom(sdlStore.deploySdl); const muiTheme = useMuiTheme(); const smallScreen = useMediaQuery(muiTheme.breakpoints.down("md")); @@ -310,6 +312,7 @@ export const Sidebar: React.FunctionComponent = ({ isMobileOpen, handleDr href={UrlService.newDeployment()} onClick={onDeployClick} data-testid="sidebar-deploy-button" + aria-disabled={settings.isBlockchainDown} > {isNavOpen && "Deploy "} diff --git a/apps/deploy-web/src/components/layout/TopBanner.tsx b/apps/deploy-web/src/components/layout/TopBanner.tsx index d4d258df40..026403fbac 100644 --- a/apps/deploy-web/src/components/layout/TopBanner.tsx +++ b/apps/deploy-web/src/components/layout/TopBanner.tsx @@ -2,11 +2,12 @@ import { FormattedDate } from "react-intl"; import { Button } from "@akashnetwork/ui/components"; import { Xmark } from "iconoir-react"; +import { useTopBanner } from "@src/context/TopBannerProvider"; import { useWallet } from "@src/context/WalletProvider/WalletProvider"; import { useVariant } from "@src/hooks/useVariant"; import { ConnectManagedWalletButton } from "../wallet/ConnectManagedWalletButton"; -export function CreditCardBanner() { +function CreditCardBanner() { const { hasManagedWallet } = useWallet(); return ( @@ -18,7 +19,15 @@ export function CreditCardBanner() { ); } -export function MaintenanceBanner({ onClose }: { onClose: () => void }) { +function NetworkDownBanner() { + return ( +
+ The network is down. Unable to change deployments at the moment. +
+ ); +} + +function MaintenanceBanner({ onClose }: { onClose: () => void }) { const maintenanceBannerFlag = useVariant("maintenance_banner"); const { message, date } = maintenanceBannerFlag.enabled @@ -36,3 +45,21 @@ export function MaintenanceBanner({ onClose }: { onClose: () => void }) {
); } + +export function TopBanner() { + const { isMaintenanceBannerOpen, setIsMaintenanceBannerOpen, isBlockchainDown, hasCreditCardBanner } = useTopBanner(); + + if (isMaintenanceBannerOpen) { + return setIsMaintenanceBannerOpen(false)} />; + } + + if (isBlockchainDown) { + return ; + } + + if (hasCreditCardBanner) { + return ; + } + + return null; +} diff --git a/apps/deploy-web/src/components/new-deployment/CreateLease/CreateLease.spec.tsx b/apps/deploy-web/src/components/new-deployment/CreateLease/CreateLease.spec.tsx index d4e8469b5a..3b37c81394 100644 --- a/apps/deploy-web/src/components/new-deployment/CreateLease/CreateLease.spec.tsx +++ b/apps/deploy-web/src/components/new-deployment/CreateLease/CreateLease.spec.tsx @@ -148,6 +148,44 @@ describe(CreateLease.name, () => { jest.useRealTimers(); }); + it("disables Accept Bid button when blockchain is down", async () => { + const BidGroup = jest.fn(ComponentMock); + const bids = [ + buildRpcBid({ + bid: { + bid_id: { + gseq: 1 + }, + state: "open" + } + }), + buildRpcBid({ + bid: { + bid_id: { + gseq: 1 + }, + state: "open" + } + }) + ]; + setup({ + BidGroup, + bids, + isBlockchainDown: true + }); + + await waitFor(() => { + expect((BidGroup as jest.Mock).mock.calls.length).toBeGreaterThan(0); + }); + const bidGroupProps = (BidGroup as jest.Mock).mock.calls[0][0]; + act(() => { + bidGroupProps.handleBidSelected(mapToBidDto(bids[0])); + }); + await waitFor(() => { + expect(screen.queryByRole("button", { name: /Accept Bid/i })).toBeDisabled(); + }); + }); + describe("lease creation", () => { it("creates lease on chain and submits manifest to provider", async () => { const signAndBroadcastTx = jest.fn().mockResolvedValue({ code: 0 }); @@ -362,6 +400,7 @@ describe(CreateLease.name, () => { updateSelectedCertificate?: (cert: CertificatePem) => Promise; isTrialWallet?: boolean; getBlock?: () => Promise; + isBlockchainDown?: boolean; }) { const favoriteProviders: string[] = []; const useLocalNotes = (() => ({ @@ -456,6 +495,22 @@ describe(CreateLease.name, () => { useRouter: () => mock(), useManagedDeploymentConfirm: () => ({ closeDeploymentConfirm: () => Promise.resolve(true) + }), + 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/new-deployment/CreateLease/CreateLease.tsx b/apps/deploy-web/src/components/new-deployment/CreateLease/CreateLease.tsx index 6095fdc3ad..cd05a4611a 100644 --- a/apps/deploy-web/src/components/new-deployment/CreateLease/CreateLease.tsx +++ b/apps/deploy-web/src/components/new-deployment/CreateLease/CreateLease.tsx @@ -31,6 +31,7 @@ import { SignUpButton } from "@src/components/auth/SignUpButton/SignUpButton"; import { browserEnvConfig } from "@src/config/browser-env.config"; import type { LocalCert } from "@src/context/CertificateProvider/CertificateProviderContext"; import { useServices } from "@src/context/ServicesProvider"; +import { useSettings } from "@src/context/SettingsProvider"; import { useWallet } from "@src/context/WalletProvider"; import { useManagedDeploymentConfirm } from "@src/hooks/useManagedDeploymentConfirm"; import { useWhen } from "@src/hooks/useWhen"; @@ -98,7 +99,8 @@ export const DEPENDENCIES = { useSnackbar, useManagedDeploymentConfirm, useRouter, - useBlock + useBlock, + useSettings }; // Refresh bids every 7 seconds; @@ -110,6 +112,7 @@ const WARNING_NUM_OF_BID_REQUESTS = Math.round((60 * 1000) / REFRESH_BIDS_INTERV const TRIAL_SIGNUP_WARNING_TIMEOUT = 33_000; export const CreateLease: React.FunctionComponent = ({ dseq, dependencies: d = DEPENDENCIES }) => { + const { settings } = d.useSettings(); const { providerProxy, analyticsService, errorHandler, networkStore } = d.useServices(); const [isSendingManifest, setIsSendingManifest] = useState(false); @@ -409,7 +412,7 @@ export const CreateLease: React.FunctionComponent = ({ dseq, dependencies
- handleCloseDeployment()} icon={}> + handleCloseDeployment()} icon={} disabled={settings.isBlockchainDown}> Close Deployment @@ -422,7 +425,7 @@ export const CreateLease: React.FunctionComponent = ({ dseq, dependencies color="secondary" onClick={createLease} className="w-full whitespace-nowrap md:w-auto" - disabled={hasActiveBid ? false : dseqList.some(gseq => !selectedBids[gseq]) || isSendingManifest || isCreatingLeases} + disabled={settings.isBlockchainDown || isSendingManifest || isCreatingLeases || (!hasActiveBid && dseqList.some(gseq => !selectedBids[gseq]))} data-testid="create-lease-button" > {isCreatingLeases || isSendingManifest ? ( @@ -442,7 +445,7 @@ export const CreateLease: React.FunctionComponent = ({ dseq, dependencies )} {!isLoadingBids && allClosed && ( - + Close Deployment )} @@ -594,7 +597,14 @@ export const CreateLease: React.FunctionComponent = ({ 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 = ({

-
- -
+ {!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: {