Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions packages/admin-ui/src/components/modals/BackupNodeModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import React from "react";
import dappnodeServerShield from "img/dappnode_server_shield.png";
import BasePromotionModal from "./BasePromotionModal";
import { useNavigate } from "react-router-dom";
import { relativePath as premiumRelativePath } from "pages/premium/data";
import { premiumLanding } from "params";
import { Network } from "@dappnode/types";
import { usePremium } from "hooks/premium/usePremium";

interface BackupNodeModalProps {
show: boolean;
onClose: (shouldContinue: boolean) => void;
}

/**
* BackupNodeModal component
*
* This modal is used to prompt users to upgrade to Premium when changing execution clients.
*
* Primary Button ("Upgrade to Premium")
* - Navigates to the premium tab
* - Calls onClose(false) to abort the flow
*
* Secondary Button ("Learn More")
* - Opens the external documentation link in a new tab
* - Does NOT close the modal
* - User can read the landing page and come back to the modal
*
* Close Button (X) or Backdrop Click
* - Calls onClose(true) to continue with the flow
*
*/

export default function BackupNodeModal({ show, onClose }: BackupNodeModalProps) {
const navigate = useNavigate();

const navigateToPremiumTab = () => {
navigate(`/${premiumRelativePath}`);
onClose(false); // Abort the flow
};

return (
<BasePromotionModal
show={show}
onClose={() => onClose(true)} // Continue with the flow when closing via X or backdrop
title="Node runner, you are about to lose rewards!"
description="The Backup node keeps your validators attesting and proposing blocks when your local clients are syncing or not available. No more downtime."
imageSrc={dappnodeServerShield}
imageAlt="DAppNode Server Shield"
primaryButtonText="Upgrade to Premium"
primaryButtonAction={navigateToPremiumTab}
secondaryButton={{
type: "external-link",
text: "Learn More",
href: premiumLanding
}}
/>
);
}

/**
* Custom hook to handle the backup node modal in the stakers tab
*
* @param network - The current network
* @param isExecutionChanged - Whether the execution client has changed
* @param isSignerSelected - Whether the signer is selected
* @returns An object containing the modal component props and a function to show the modal
*/
export function useBackupNodeModal(network: Network, isExecutionChanged: boolean, isSignerSelected: boolean) {
const { isActivated: isPremium } = usePremium();
const [showBackupModal, setShowBackupModal] = React.useState(false);
const modalResolveRef = React.useRef<((value: boolean) => void) | null>(null);

const handleBackupModalClose = (shouldContinue: boolean) => {
setShowBackupModal(false);
if (modalResolveRef.current) {
modalResolveRef.current(shouldContinue);
modalResolveRef.current = null;
}
};

/**
* Shows the backup node modal if it meets its conditions and waits for user decision
* @returns Promise that resolves to true if user wants to continue, false if they abort
*/
async function showBackupNodeModal(): Promise<boolean> {
if (isExecutionChanged && isSignerSelected) {
if (!isPremium) {
// Only show backup modal for certain networks
if (network === Network.Mainnet || network === Network.Gnosis || network === Network.Hoodi) {
const shouldContinue = await new Promise<boolean>((resolve) => {
modalResolveRef.current = resolve;
setShowBackupModal(true);
});
return shouldContinue;
}
}
}

return true;
}

return {
show: showBackupModal,
onClose: handleBackupModalClose,
showBackupNodeModal
};
}
104 changes: 104 additions & 0 deletions packages/admin-ui/src/components/modals/BasePromotionModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React from "react";
import { externalUrlProps } from "params";
import { Link } from "react-router-dom";

import "./basePromotionModal.scss";

interface BasePromotionModalProps {
show: boolean;
onClose: () => void;
title: string;
description: string;
imageSrc: string;
imageAlt: string;
primaryButtonText: string;
primaryButtonAction: () => void;
secondaryButton?: {
text: string;
} & (
| { type: "action"; action: () => void }
| { type: "internal-link"; to: string }
| { type: "external-link"; href: string; onClick?: () => void }
);
}

export default function BasePromotionModal({
show,
onClose,
title,
description,
imageSrc,
imageAlt,
primaryButtonText,
primaryButtonAction,
secondaryButton
}: BasePromotionModalProps) {
if (!show) return null;

const renderSecondaryButton = () => {
if (!secondaryButton) return null;

const buttonElement = (
<button className="promotion-full-width-button promotion-button-secondary">{secondaryButton.text}</button>
);

switch (secondaryButton.type) {
case "action":
return (
<button className="promotion-full-width-button promotion-button-secondary" onClick={secondaryButton.action}>
{secondaryButton.text}
</button>
);
case "internal-link":
return (
<Link to={secondaryButton.to} className="promotion-link-button">
{buttonElement}
</Link>
);
case "external-link":
return (
<a
href={secondaryButton.href}
{...externalUrlProps}
className="promotion-link-button"
onClick={secondaryButton.onClick}
>
{buttonElement}
</a>
);
default:
return null;
}
};

return (
<div className="promotion-modal-overlay" onClick={onClose}>
<div className="promotion-modal-container" onClick={(e) => e.stopPropagation()}>
<button className="promotion-modal-close" onClick={onClose} aria-label="Close">
×
</button>

<div className="promotion-modal-header">
<h2 className="promotion-modal-title">{title}</h2>
</div>

<div className="promotion-modal-image-container">
<img src={imageSrc} alt={imageAlt} className="promotion-modal-image" />
</div>

<div className="promotion-modal-body">
<div className="promotion-modal-description">
<p>{description}</p>
</div>

<div className="promotion-modal-button-container">
<button className="promotion-full-width-button promotion-button-primary" onClick={primaryButtonAction}>
{primaryButtonText}
</button>
{renderSecondaryButton()}
</div>
</div>
</div>
</div>
);
}
Loading
Loading