From 901898cc4c39027faa03ad6662684976623e360d Mon Sep 17 00:00:00 2001 From: gmolki Date: Thu, 24 Oct 2024 14:01:51 +0200 Subject: [PATCH] feat/refactor: Simplify instance instance creation modals v2 --- .../pages/computing/NewInstancePage/cmp.tsx | 511 +++++++----------- src/hooks/common/useConnection.ts | 26 +- src/hooks/form/useAddSSHKeys.ts | 31 +- src/hooks/form/useSelectPaymentMethod.ts | 25 +- .../pages/computing/useNewInstancePage.ts | 361 ++++++++++--- src/store/connection.ts | 29 + 6 files changed, 578 insertions(+), 405 deletions(-) diff --git a/src/components/pages/computing/NewInstancePage/cmp.tsx b/src/components/pages/computing/NewInstancePage/cmp.tsx index 81f2ba4a..a481ea34 100644 --- a/src/components/pages/computing/NewInstancePage/cmp.tsx +++ b/src/components/pages/computing/NewInstancePage/cmp.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef } from 'react' import Image from 'next/image' import { Button, @@ -8,12 +8,8 @@ import { NodeScore, TableColumn, NoisyContainer, - useModal, - Tooltip, - useResponsiveMax, } from '@aleph-front/core' import { CRN } from '@/domain/node' -import { BlockchainId } from '@/domain/connect/base' import SelectInstanceImage from '@/components/form/SelectInstanceImage' import SelectInstanceSpecs from '@/components/form/SelectInstanceSpecs' import AddVolumes from '@/components/form/AddVolumes' @@ -29,7 +25,10 @@ import { apiServer, } from '@/helpers/constants' import Container from '@/components/common/CenteredContainer' -import { useNewInstancePage } from '@/hooks/pages/computing/useNewInstancePage' +import { + useNewInstancePage, + UseNewInstancePageReturn, +} from '@/hooks/pages/computing/useNewInstancePage' import Form from '@/components/form/Form' import ToggleContainer from '@/components/common/ToggleContainer' import NewEntityTab from '../NewEntityTab' @@ -38,12 +37,47 @@ import SpinnerOverlay from '@/components/common/SpinnerOverlay' import { SectionTitle } from '@/components/common/CompositeTitle' import { PageProps } from '@/types/types' import Strong from '@/components/common/Strong' -import { useConnection } from '@/hooks/common/useConnection' import CRNList from '../../../common/CRNList' import BackButtonSection from '@/components/common/BackButtonSection' -import { isBlockchainSupported as isBlockchainPAYGCompatible } from '@aleph-sdk/superfluid' -import { isBlockchainHoldingCompatible } from '@/domain/blockchain' -import tw from 'twin.macro' +import { ConnectionConfirmationModal } from '@/store/connection' +import ResponsiveTooltip from '@/components/common/ResponsiveTooltip' + +const CheckoutButton = ({ + disabled, + handleSubmit, + tooltipContent, +}: { + disabled: boolean + handleSubmit: UseNewInstancePageReturn['handleSubmit'] + tooltipContent?: React.ReactNode +}) => { + const checkoutButtonRef = useRef(null) + + return ( + <> + + {tooltipContent && ( + + )} + + ) +} export default function NewInstancePage({ mainRef }: PageProps) { const { @@ -58,138 +92,44 @@ export default function NewInstancePage({ mainRef }: PageProps) { node, nodeSpecs, lastVersion, + disabledPAYG, + hasModifiedFormValues, + waitingModalConfirmation, + selectedModal, + setSelectedModal, + selectedNode, + setSelectedNode, + modalOpen, + modalClose, handleSubmit, - handleSelectNode, + handleCloseModal, + handleResetForm, + handleSwitchPaymentMethod, + handleManuallySelectCRN, + handleSwitchToAutoHold, + handleSwitchToNodeStream, + handleEnableConfirmationModal, + handleDisableConfirmationModal, handleBack, } = useNewInstancePage() const sectionNumber = useCallback((n: number) => (node ? 1 : 0) + n, [node]) - const { blockchain, provider, handleConnect } = useConnection({ - triggerOnMount: false, - }) // ------------------ - const modal = useModal() - const modalOpen = modal?.open - const modalClose = modal?.close - - const [selectedNode, setSelectedNode] = useState() - const [selectedModal, setSelectedModal] = useState< - | 'node-list' - | 'switch-to-hold' - | 'switch-to-stream' - | 'switch-to-node-stream' - >() - - const handleCloseModal = useCallback(() => setSelectedModal(undefined), []) - - const handleManuallySelectCRN = useCallback(() => { - isBlockchainPAYGCompatible(blockchain) - ? setSelectedModal('node-list') - : setSelectedModal('switch-to-node-stream') - }, [blockchain]) - - const handleSwitchToNodeStream = useCallback(async () => { - if (!isBlockchainPAYGCompatible(blockchain)) - handleConnect({ blockchain: BlockchainId.BASE, provider }) - - if (selectedNode !== node?.hash) handleSelectNode(selectedNode) - - setSelectedModal(undefined) - }, [ - blockchain, - provider, - handleConnect, - handleSelectNode, - node, - selectedNode, - ]) - - const handleSwitchToAutoHold = useCallback(() => { - if (node?.hash) { - setSelectedNode(undefined) - handleSelectNode(undefined) - } - - if (!isBlockchainHoldingCompatible(blockchain)) - handleConnect({ blockchain: BlockchainId.ETH, provider }) - - setSelectedModal(undefined) - }, [blockchain, provider, handleConnect, handleSelectNode, node?.hash]) - - useEffect(() => { - if (!modalOpen) return - if (!modalClose) return - - if (!selectedModal) { - return modalClose() - } - - if (selectedModal === 'node-list') { - return modalOpen({ - header: '', - width: '80rem', - onClose: handleCloseModal, - content: ( - - ), - footer: ( - <> -
- {node && ( - - )} - -
- - ), - }) - } - - if (selectedModal === 'switch-to-hold') { - return modalOpen({ + const confirmationModal: ConnectionConfirmationModal['cardProps'] = + useCallback( + (action, payload) => ({ width: '40rem', - title: 'Confirm Payment Method Change', + title: 'Confirm Chain Switch', onClose: handleCloseModal, content: (
- Switching to the Holder tier will set your - payment method to holding $ALEPH tokens on{' '} - Ethereum and{' '} - automatically select the most suitable CRN. -
-
- Switching modes will prompt your wallet to automatically adjust to - the Ethereum network if needed, and the system will reset to align - with your new selection preferences. Ensure your wallet is ready - for the Ethereum network. -
-
- Token Requirements -
- You will need to hold sufficient $ALEPH tokens in your wallet to - continue using the instance. $ALEPH can be acquired from Uniswap - or Coinbase. -
+ Switching chains will reset all data you've + currently entered in the form. This is necessary to align with the + specific configurations and requirements of the new chain. Please + save any important information before proceeding.
), @@ -201,158 +141,96 @@ export default function NewInstancePage({ mainRef }: PageProps) { size="md" onClick={handleCloseModal} > - Stay on Tier + Cancel ), - }) - } + }), + [handleCloseModal, handleResetForm], + ) - if (selectedModal === 'switch-to-stream') { - return modalOpen({ - width: '40rem', - title: 'Confirm Payment Method Change', - onClose: handleCloseModal, - content: ( -
-
- You are about to switch your payment method to{' '} - Pay-as-you-go, which will also allow you to{' '} - manually select your preferred CRN on{' '} - Base. -
-
- Making this change will prompt your wallet to automatically switch - networks. This will reset your current configuration to - accommodate your new selection. Please ensure your wallet is - compatible and ready for this transition. -
-
- Token Requirements -
- You will need $ETH on Base to start the PAYG stream and $ALEPH - on Base to stream. Purchase $ETH on Base at Uniswap and get - $ALEPH on Base by swapping your Ethereum $ALEPH on - swap.aleph.im. -
-
-
- ), - footer: ( -
- - -
- ), - }) - } + // Handle confirmation modal + useEffect(() => { + if (node) return handleEnableConfirmationModal(confirmationModal) - if (selectedModal === 'switch-to-node-stream') { - return modalOpen({ - width: '40rem', - title: 'Confirm CRN selection and Payment Method Change', - onClose: handleCloseModal, - content: ( -
-
- You are about to switch from the automated Holder-tier setup on{' '} - {blockchainName} to manually selecting a CRN with - the Pay-as-you-go method on Base - . -
-
- Making this change will prompt your wallet to automatically switch - networks. This will reset your current configuration to - accommodate your new selection. Please ensure your wallet is - compatible and ready for this transition. -
-
- Token Requirements -
- You will need $ETH on Base to start the PAYG stream and $ALEPH - on Base to stream. Purchase $ETH on Base at Uniswap and get - $ALEPH on Base by swapping your Ethereum $ALEPH on - swap.aleph.im. + if (!address) return handleDisableConfirmationModal() + if (!hasModifiedFormValues) return handleDisableConfirmationModal() + if (!modalOpen) return handleDisableConfirmationModal() + + handleEnableConfirmationModal(confirmationModal) + + return () => handleDisableConfirmationModal() + }, [ + address, + hasModifiedFormValues, + confirmationModal, + modalOpen, + handleDisableConfirmationModal, + handleEnableConfirmationModal, + node, + ]) + + // Handle modals + useEffect(() => { + if (!modalOpen) return + if (!modalClose) return + if (waitingModalConfirmation) return + if (!selectedModal) return modalClose() + + switch (selectedModal) { + case 'node-list': + return modalOpen({ + header: '', + width: '80rem', + onClose: handleCloseModal, + content: ( + + ), + footer: ( + <> +
+
-
-
- ), - footer: ( -
- - -
- ), - }) + + ), + }) } }, [ + blockchainName, + node, + selectedNode, + setSelectedNode, + selectedModal, + setSelectedModal, + waitingModalConfirmation, + modalOpen, + modalClose, handleCloseModal, - handleConnect, - handleSelectNode, handleSwitchToAutoHold, handleSwitchToNodeStream, - modalClose, - modalOpen, - node, - selectedModal, - selectedNode, - blockchainName, ]) // ------------------------ - const handleSwitchPaymentMethod = useCallback( - (method: PaymentMethod) => { - if (method === PaymentMethod.Stream) { - isBlockchainPAYGCompatible(blockchain) - ? node - ? handleSwitchToNodeStream() - : setSelectedModal('node-list') - : setSelectedModal('switch-to-stream') - } else if (method === PaymentMethod.Hold) { - isBlockchainHoldingCompatible(blockchain) - ? handleSwitchToAutoHold() - : setSelectedModal('switch-to-hold') - } - }, - [blockchain, node, handleSwitchToAutoHold, handleSwitchToNodeStream], - ) - const columns = useMemo(() => { return [ { @@ -398,15 +276,10 @@ export default function NewInstancePage({ mainRef }: PageProps) { ), }, ] as TableColumn[] - }, [lastVersion]) - - const data = useMemo(() => (node ? [node] : []), [node]) + }, [lastVersion, setSelectedModal]) - const createButtonRef = useRef(null) - const isMobile = useResponsiveMax('md') - const mobileTw = isMobile - ? tw`!fixed !left-0 !top-0 !transform-none m-6 !z-20 !w-[calc(100% - 3rem)] !h-[calc(100% - 3rem)] !max-w-full` - : tw`` + const nodeData = useMemo(() => (node ? [node] : []), [node]) + const manuallySelectButtonRef = useRef(null) return ( <> @@ -417,7 +290,7 @@ export default function NewInstancePage({ mainRef }: PageProps) { - {node && ( + {values.paymentMethod === PaymentMethod.Stream && (
@@ -425,22 +298,23 @@ export default function NewInstancePage({ mainRef }: PageProps) {
- ({ className: '_active' })} />
- + {!node && ( + + )}
@@ -474,14 +348,37 @@ export default function NewInstancePage({ mainRef }: PageProps) { {!node && (
+ {disabledPAYG && ( + +

+ Manual CRN Selection Unavailable +

+

+ Manual selection of CRN is not supported on this + chain. To access manual CRN selection, please + switch to the Base{' '} + or Avalanche chain using the + dropdown at the top of the page. +

+
+ } + /> + )}
)} @@ -606,7 +503,20 @@ export default function NewInstancePage({ mainRef }: PageProps) { unlockedAmount={accountBalance} paymentMethod={values.paymentMethod} streamDuration={values.streamDuration} - disablePaymentMethod={false} + disablePaymentMethod={disabledPAYG} + disabledStreamTooltip={ +
+

+ Payment Method Toggle Disabled +

+

+ This chain supports only the Holder tier payment method. To use + the Pay-As-You-Go tier, please switch to the{' '} + Base or Avalanche chain using + the dropdown at the top of the page. +

+
+ } mainRef={mainRef} onSwitchPaymentMethod={handleSwitchPaymentMethod} description={ @@ -618,31 +528,20 @@ export default function NewInstancePage({ mainRef }: PageProps) { feature, enabling real-time payment for resources as you use them. } + // Duplicate buttons to have different references for the tooltip on each one button={ - <> - - {createButtonTooltipContent && ( - - )} - + + } + footerButton={ + } /> diff --git a/src/hooks/common/useConnection.ts b/src/hooks/common/useConnection.ts index 09dfc401..25d53737 100644 --- a/src/hooks/common/useConnection.ts +++ b/src/hooks/common/useConnection.ts @@ -1,6 +1,7 @@ import { useCallback, useEffect, useRef } from 'react' -import { useNotification } from '@aleph-front/core' +import { useModal, useNotification } from '@aleph-front/core' import { + ConnectionConfirmUpdateAction, ConnectionConnectAction, ConnectionDisconnectAction, ConnectionState, @@ -13,6 +14,7 @@ import { BaseConnectionProviderManager } from '@/domain/connect/base' export type UseConnectionProps = { triggerOnMount?: boolean + confirmSwitchModal?: React.ReactNode } export type UseConnectionReturn = ConnectionState & { @@ -24,7 +26,7 @@ export const useConnection = ({ triggerOnMount, }: UseConnectionProps): UseConnectionReturn => { const [state, dispatch] = useAppState() - const { blockchain, provider } = state.connection + const { blockchain, provider, confirmationModal } = state.connection const prevConnectionProviderRef = useRef< BaseConnectionProviderManager | undefined @@ -32,11 +34,25 @@ export const useConnection = ({ const noti = useNotification() const addNotification = noti?.add + const modal = useModal() + const openModal = modal?.open const handleConnect = useCallback( - (payload: ConnectionConnectAction['payload']) => - dispatch(new ConnectionConnectAction(payload)), - [dispatch], + (payload: ConnectionConnectAction['payload']) => { + if (confirmationModal) { + if (!openModal) return + + dispatch( + new ConnectionConfirmUpdateAction({ waitingConfirmation: true }), + ) + + openModal(confirmationModal.cardProps(ConnectionConnectAction, payload)) + } else { + dispatch(new ConnectionConnectAction(payload)) + } + }, + + [confirmationModal, openModal, dispatch], ) const handleDisconnect = useCallback( diff --git a/src/hooks/form/useAddSSHKeys.ts b/src/hooks/form/useAddSSHKeys.ts index 21da9592..f2409c14 100644 --- a/src/hooks/form/useAddSSHKeys.ts +++ b/src/hooks/form/useAddSSHKeys.ts @@ -20,6 +20,21 @@ export const defaultValues: SSHKeyField = { isNew: true, } +export function useAccountSSHKeyItems(): SSHKeyField[] { + const { entities: accountSSHKeys } = useRequestSSHKeys() + + return useMemo( + () => + (accountSSHKeys || []).map(({ key, label = '' }, i) => ({ + key, + label, + isSelected: i === 0, + isNew: false, + })), + [accountSSHKeys], + ) +} + export type UseSSHKeyItemProps = { name?: string index: number @@ -116,22 +131,10 @@ export function useAddSSHKeys({ const { remove: handleRemove, append, replace, prepend } = sshKeysCtrl const fields = sshKeysCtrl.fields as (SSHKeyField & { id: string })[] - - const { entities: accountSSHKeys } = useRequestSSHKeys() - - const accountSSHKeyItems: SSHKeyField[] = useMemo( - () => - (accountSSHKeys || []).map(({ key, label = '' }, i) => ({ - key, - label, - isSelected: i === 0, - isNew: false, - })), - [accountSSHKeys], - ) + const accountSSHKeyItems = useAccountSSHKeyItems() // Empty when the account changes - useEffect(() => handleRemove(), [handleRemove, accountSSHKeys]) + useEffect(() => handleRemove(), [handleRemove, accountSSHKeyItems]) useEffect(() => { let newValues = accountSSHKeyItems diff --git a/src/hooks/form/useSelectPaymentMethod.ts b/src/hooks/form/useSelectPaymentMethod.ts index d1515437..6b2f47a9 100644 --- a/src/hooks/form/useSelectPaymentMethod.ts +++ b/src/hooks/form/useSelectPaymentMethod.ts @@ -1,5 +1,5 @@ import { PaymentMethod } from '@/helpers/constants' -import { ChangeEvent, useCallback } from 'react' +import { ChangeEvent, useCallback, useRef } from 'react' import { Control, UseControllerReturn, useController } from 'react-hook-form' export type UseSelectPaymentMethodProps = { @@ -8,6 +8,7 @@ export type UseSelectPaymentMethodProps = { defaultValue?: PaymentMethod disabledHold?: boolean disabledStream?: boolean + disabledStreamTooltip: React.ReactNode onSwitch?: (e: PaymentMethod) => void } @@ -15,6 +16,9 @@ export type UseSelectPaymentMethodReturn = { disabledHold?: boolean disabledStream?: boolean paymentMethodCtrl: UseControllerReturn + disabledStreamTooltip: React.ReactNode + switchRef: React.Ref + selectedPaymentMethod: PaymentMethod handleClickStream: () => void handleClickHold: () => void } @@ -24,6 +28,7 @@ export function useSelectPaymentMethod({ control, defaultValue, disabledHold, + disabledStream, onSwitch, ...rest }: UseSelectPaymentMethodProps): UseSelectPaymentMethodReturn { @@ -47,20 +52,32 @@ export function useSelectPaymentMethod({ ;(paymentMethodCtrl.field as any).checked = paymentMethodCtrl.field.value === PaymentMethod.Stream + const selectedPaymentMethod = paymentMethodCtrl.field.value + const handleClickStream = useCallback(() => { + if (disabledStream) return + if (selectedPaymentMethod === PaymentMethod.Stream) return + onChange(PaymentMethod.Stream) - }, [onChange]) + }, [disabledStream, onChange, selectedPaymentMethod]) const handleClickHold = useCallback(() => { if (disabledHold) return + if (selectedPaymentMethod === PaymentMethod.Hold) return + onChange(PaymentMethod.Hold) - }, [disabledHold, onChange]) + }, [disabledHold, onChange, selectedPaymentMethod]) + + const switchRef = useRef(null) return { paymentMethodCtrl, + selectedPaymentMethod, + disabledHold, + disabledStream, + switchRef, handleClickStream, handleClickHold, - disabledHold, ...rest, } } diff --git a/src/hooks/pages/computing/useNewInstancePage.ts b/src/hooks/pages/computing/useNewInstancePage.ts index 4f797ffd..8a5c2a55 100644 --- a/src/hooks/pages/computing/useNewInstancePage.ts +++ b/src/hooks/pages/computing/useNewInstancePage.ts @@ -12,7 +12,7 @@ import { defaultNameAndTags, NameAndTagsField, } from '@/hooks/form/useAddNameAndTags' -import { SSHKeyField } from '@/hooks/form/useAddSSHKeys' +import { SSHKeyField, useAccountSSHKeyItems } from '@/hooks/form/useAddSSHKeys' import { VolumeField } from '@/hooks/form/useAddVolume' import { defaultInstanceImage, @@ -47,6 +47,16 @@ import { BlockchainId, blockchains } from '@/domain/connect/base' import { PaymentConfiguration } from '@/domain/executable' import { EVMAccount } from '@aleph-sdk/evm' import { isBlockchainHoldingCompatible } from '@/domain/blockchain' +import { + ConnectionConfirmationModal, + ConnectionConfirmUpdateAction, +} from '@/store/connection' +import { + ModalCardProps, + NotificationCardProps, + useModal, + useNotification, +} from '@aleph-front/core' export type NewInstanceFormState = NameAndTagsField & { image: InstanceImageField @@ -62,20 +72,23 @@ export type NewInstanceFormState = NameAndTagsField & { streamCost: number } -const specs = { ...getDefaultSpecsOptions(true, PaymentMethod.Stream)[0] } +const defaultSpecs = { + ...getDefaultSpecsOptions(true, PaymentMethod.Stream)[0], +} export const defaultValues: Partial = { ...defaultNameAndTags, image: defaultInstanceImage, - specs, - systemVolumeSize: specs.storage, + specs: defaultSpecs, + systemVolumeSize: defaultSpecs.storage, paymentMethod: PaymentMethod.Hold, streamDuration: defaultStreamDuration, streamCost: Number.POSITIVE_INFINITY, - // sshKeys: [{ ...sshKeyDefaultValues }], } -export type UseNewInstancePage = { +export type Modal = 'node-list' + +export type UseNewInstancePageReturn = { address: string accountBalance: number blockchainName: string @@ -87,56 +100,64 @@ export type UseNewInstancePage = { node?: CRN lastVersion?: NodeLastVersions nodeSpecs?: CRNSpecs + disabledPAYG: boolean + hasModifiedFormValues?: boolean + waitingModalConfirmation?: boolean + selectedModal?: Modal + setSelectedModal: (modal?: Modal) => void + selectedNode?: string + setSelectedNode: (hash?: string) => void + modalOpen?: (info: ModalCardProps) => void + modalClose?: () => void handleSubmit: (e: FormEvent) => Promise - handleSelectNode: (hash?: string) => Promise + handleCloseModal: () => void + handleResetForm: (action: any, payload: any) => void + handleSwitchPaymentMethod: (method: PaymentMethod) => void + handleManuallySelectCRN: () => void + handleSwitchToAutoHold: () => void + handleSwitchToNodeStream: () => Promise + handleEnableConfirmationModal: ( + confirmationModal: ConnectionConfirmationModal['cardProps'], + ) => void + handleDisableConfirmationModal: () => void handleBack: () => void } -export function useNewInstancePage(): UseNewInstancePage { +export function useNewInstancePage(): UseNewInstancePageReturn { const router = useRouter() const [, dispatch] = useAppState() - const [isCreateButtonDisabled, setIsCreateButtonDisabled] = useState(false) - const [createButtonTooltipContent, setCreateButtonTooltipContent] = useState< - string | undefined - >(undefined) - const { blockchain, account, + provider, balance: accountBalance = 0, + waitingConfirmation: waitingModalConfirmation, handleConnect, } = useConnection({ triggerOnMount: false, }) - // ------------------------- + const modal = useModal() + const modalOpen = modal?.open + const modalClose = modal?.close - const { crn } = router.query + const notification = useNotification() + const addNotification = notification?.add - const handleSelectNode = useCallback( - async (hash?: string) => { - const { crn, ...rest } = router.query - const query = hash ? { ...rest, crn: hash } : rest + const [node, setNode] = useState() + const [selectedNode, setSelectedNode] = useState() + const [selectedModal, setSelectedModal] = useState() + const [isCreateButtonDisabled, setIsCreateButtonDisabled] = useState(false) + const [createButtonTooltipContent, setCreateButtonTooltipContent] = useState< + string | undefined + >(undefined) - if (crn === query.crn) return false + // ------------------------- + // Request CRNs specs - return router.replace({ query }) - }, - [router], - ) + const userNodes = useMemo(() => (node ? [node] : undefined), [node]) const { nodes, lastVersion } = useRequestCRNs({}) - - const node = useMemo(() => { - if (!nodes) return - return nodes.find((node) => node.hash === crn) - }, [nodes, crn]) - - const userNodes = useMemo(() => { - if (!node) return - return [node] - }, [node]) - const { specs } = useRequestCRNSpecs({ nodes: userNodes }) const nodeSpecs = useMemo(() => { @@ -147,10 +168,10 @@ export function useNewInstancePage(): UseNewInstancePage { }, [specs, node]) // ------------------------- + // Checkout flow const manager = useInstanceManager() const { next, stop } = useCheckoutNotification({}) - const onSubmit = useCallback( async (state: NewInstanceFormState) => { if (!manager) throw Err.ConnectYourWallet @@ -240,23 +261,216 @@ export function useNewInstancePage(): UseNewInstancePage { ], ) + // ------------------------- + // Setup form + + const { crn } = router.query + const accountSSHKeyItems = useAccountSSHKeyItems() + const defaultFormValues: Partial = useMemo( + () => ({ + ...defaultValues, + // paymentMethod: crn ? PaymentMethod.Stream : PaymentMethod.Hold, + sshKeys: accountSSHKeyItems, + }), + [accountSSHKeyItems], + ) + const { control, handleSubmit, - formState: { errors }, + formState: { errors, isDirty: hasModifiedFormValues }, setValue, + reset: resetForm, } = useForm({ - defaultValues, + defaultValues: defaultFormValues, onSubmit, resolver: zodResolver( !node ? InstanceManager.addSchema : InstanceManager.addStreamSchema, ), - readyDeps: [], + readyDeps: [defaultFormValues], + }) + + const formValues = useWatch({ control }) as NewInstanceFormState + + // ------------------------- + + const { storage } = formValues.specs + const { systemVolumeSize } = formValues + const { cost } = useEntityCost({ + entityType: EntityType.Instance, + props: { + specs: formValues.specs, + volumes: formValues.volumes, + paymentMethod: formValues.paymentMethod, + streamDuration: formValues.streamDuration, + }, }) - const values = useWatch({ control }) as NewInstanceFormState - const { storage } = values.specs - const { systemVolumeSize } = values + const blockchainName = useMemo(() => { + return blockchain ? blockchains[blockchain]?.name : 'current network' + }, [blockchain]) + + const disabledPAYG = useMemo(() => { + return !isAccountPAYGCompatible(account) + }, [account]) + + const address = useMemo(() => account?.address || '', [account]) + + // ------------------------- + // Handlers + + const handleShowNotification = useCallback( + (props: NotificationCardProps) => { + addNotification && addNotification(props) + }, + [addNotification], + ) + + const handleSelectNode = useCallback( + async (hash?: string) => { + const { crn, ...rest } = router.query + const query = hash ? { ...rest, crn: hash } : rest + + if (crn === query.crn) return false + + return router.replace({ query }) + }, + [router], + ) + + const handleDisableConfirmationModal = useCallback( + () => + dispatch( + new ConnectionConfirmUpdateAction({ + confirmationModal: undefined, + waitingConfirmation: false, + }), + ), + [dispatch], + ) + + const handleEnableConfirmationModal = useCallback( + (confirmationModal: ConnectionConfirmationModal['cardProps']) => { + if (!modalOpen) return + + dispatch( + new ConnectionConfirmUpdateAction({ + confirmationModal: { modalOpen, cardProps: confirmationModal }, + }), + ) + }, + [dispatch, modalOpen], + ) + + const handleCloseModal = useCallback(() => { + dispatch( + new ConnectionConfirmUpdateAction({ + waitingConfirmation: false, + }), + ) + setSelectedModal(undefined) + }, [dispatch]) + + const handleManuallySelectCRN = useCallback(() => { + isBlockchainPAYGCompatible(blockchain) + ? setSelectedModal('node-list') + : handleShowNotification({ + variant: 'warning', + title: `Manual CRN Selection Unavailable`, + text: `Manual selection of CRN is not supported on ${blockchainName}. + To access manual CRN selection, please switch to the Base or + Avalanche chain using the dropdown at the top of the page.`, + }) + }, [blockchain, blockchainName, handleShowNotification]) + + const handleSwitchToNodeStream = useCallback(async () => { + if (!isBlockchainPAYGCompatible(blockchain)) + handleConnect({ blockchain: BlockchainId.BASE, provider }) + + if (selectedNode !== node?.hash) handleSelectNode(selectedNode) + + setSelectedModal(undefined) + setValue('paymentMethod', PaymentMethod.Stream) + }, [ + blockchain, + handleConnect, + provider, + selectedNode, + node?.hash, + handleSelectNode, + setValue, + ]) + + const handleSwitchToAutoHold = useCallback(() => { + if (node?.hash) { + setSelectedNode(undefined) + handleSelectNode(undefined) + } + + setSelectedModal(undefined) + setValue('paymentMethod', PaymentMethod.Hold) + }, [handleSelectNode, node?.hash, setValue]) + + const handleSwitchPaymentMethod = useCallback( + (method: PaymentMethod) => { + if (method === PaymentMethod.Stream) { + !isBlockchainPAYGCompatible(blockchain) + ? handleSwitchToNodeStream() + : handleShowNotification({ + variant: 'warning', + title: 'Unsupported chain', + text: `${blockchainName} does not support the Pay-As-You-Go tier payment method`, + }) + } else if (method === PaymentMethod.Hold) { + !isBlockchainHoldingCompatible(blockchain) + ? handleSwitchToAutoHold() + : handleShowNotification({ + variant: 'warning', + title: 'Unsupported chain', + text: `${blockchainName} does not support the Holder tier payment method`, + }) + } + }, + [ + blockchain, + blockchainName, + handleShowNotification, + handleSwitchToAutoHold, + handleSwitchToNodeStream, + ], + ) + + const handleResetForm = useCallback( + (action: any, payload: any) => { + resetForm(defaultFormValues) + handleSwitchToAutoHold() + handleCloseModal() + dispatch(new action({ ...payload })) + }, + [ + defaultFormValues, + dispatch, + handleCloseModal, + handleSwitchToAutoHold, + resetForm, + ], + ) + + const handleBack = () => { + router.push('.') + } + + // ------------------------- + // Effects + + // @note: Set node depending on CRN + useEffect(() => { + if (!nodes) return setNode(undefined) + if (formValues.paymentMethod === PaymentMethod.Hold) + return setNode(undefined) + + setNode(nodes.find((node) => node.hash === crn)) + }, [crn, nodes, formValues.paymentMethod]) // @note: Change default System fake volume size when the specs changes useEffect(() => { @@ -271,35 +485,20 @@ export function useNewInstancePage(): UseNewInstancePage { setValue('nodeSpecs', nodeSpecs) }, [nodeSpecs, setValue]) - // @note: Set payment method depending on wallet blockchain network - useEffect(() => { - setValue('paymentMethod', !node ? PaymentMethod.Hold : PaymentMethod.Stream) - }, [node, setValue]) - - // ------------------------- - - const { cost } = useEntityCost({ - entityType: EntityType.Instance, - props: { - specs: values.specs, - volumes: values.volumes, - paymentMethod: values.paymentMethod, - streamDuration: values.streamDuration, - }, - }) + // // @note: Set payment method depending on wallet blockchain network + // useEffect(() => { + // setValue('paymentMethod', !node ? PaymentMethod.Hold : PaymentMethod.Stream) + // }, [node, setValue]) // @note: Set streamCost useEffect(() => { if (!cost) return - if (values.streamCost === cost.totalStreamCost) return + if (formValues.streamCost === cost.totalStreamCost) return setValue('streamCost', cost.totalStreamCost) - }, [cost, setValue, values]) - - const blockchainName = useMemo(() => { - return blockchain ? blockchains[blockchain]?.name : 'current network' - }, [blockchain]) + }, [cost, setValue, formValues]) + // @note: Check if configuration is valid and set tooltip message useEffect(() => { if (process.env.NEXT_PUBLIC_OVERRIDE_ALEPH_BALANCE === 'true') { setCreateButtonTooltipContent(undefined) @@ -310,12 +509,12 @@ export function useNewInstancePage(): UseNewInstancePage { accountBalance >= (cost?.totalCost || Number.MAX_SAFE_INTEGER) const hasValidPAYGConfig = - values.paymentMethod === PaymentMethod.Stream && + formValues.paymentMethod === PaymentMethod.Stream && node && isBlockchainPAYGCompatible(blockchain) const hasValidHoldingConfig = - values.paymentMethod === PaymentMethod.Hold && + formValues.paymentMethod === PaymentMethod.Hold && isBlockchainHoldingCompatible(blockchain) if (!canAfford || !(hasValidPAYGConfig || hasValidHoldingConfig)) { @@ -342,29 +541,39 @@ export function useNewInstancePage(): UseNewInstancePage { blockchainName, cost, node, - values.paymentMethod, + formValues.paymentMethod, ]) - // ------------------------- - - const handleBack = () => { - router.push('.') - } - return { - address: account?.address || '', + address, accountBalance, blockchainName, isCreateButtonDisabled, createButtonTooltipContent, - values, + values: formValues, control, errors, node, lastVersion, nodeSpecs, + disabledPAYG, + hasModifiedFormValues, + waitingModalConfirmation, + selectedModal, + setSelectedModal, + selectedNode, + setSelectedNode, + modalOpen, + modalClose, handleSubmit, - handleSelectNode, + handleCloseModal, + handleResetForm, + handleSwitchPaymentMethod, + handleManuallySelectCRN, + handleSwitchToAutoHold, + handleSwitchToNodeStream, + handleEnableConfirmationModal, + handleDisableConfirmationModal, handleBack, } } diff --git a/src/store/connection.ts b/src/store/connection.ts index 0c8c87ba..6623306a 100644 --- a/src/store/connection.ts +++ b/src/store/connection.ts @@ -1,4 +1,5 @@ import { Account } from '@aleph-sdk/account' +import { ModalCardProps } from '@aleph-front/core' import { StoreReducer } from './store' import { BlockchainId, @@ -6,11 +7,21 @@ import { ProviderId, } from '@/domain/connect/base' +export type ConnectionConfirmationModal = { + modalOpen: (info: ModalCardProps) => void + cardProps: ( + actionClass: { new (payload: Action['payload']): Action }, + payload: Action['payload'], + ) => ModalCardProps +} + export type ConnectionState = { account?: Account balance?: number blockchain?: BlockchainId provider?: ProviderId + waitingConfirmation?: boolean + confirmationModal?: ConnectionConfirmationModal } export const initialState: ConnectionState = { @@ -25,6 +36,7 @@ export enum ConnectionActionType { CONNECTION_DISCONNECT = 'CONNECTION_DISCONNECT', CONNECTION_UPDATE = 'CONNECTION_UPDATE', CONNECTION_SET_BALANCE = 'CONNECTION_SET_BALANCE', + CONNECTION_CONFIRM_UPDATE = 'CONNECTION_CONFIRM_UPDATE', } export class ConnectionConnectAction { @@ -68,11 +80,22 @@ export class ConnectionSetBalanceAction { ) {} } +export class ConnectionConfirmUpdateAction { + readonly type = ConnectionActionType.CONNECTION_CONFIRM_UPDATE + constructor( + public payload: { + waitingConfirmation?: boolean + confirmationModal?: ConnectionConfirmationModal + }, + ) {} +} + export type ConnectionAction = | ConnectionConnectAction | ConnectionDisconnectAction | ConnectionUpdateAction | ConnectionSetBalanceAction + | ConnectionConfirmUpdateAction export type ConnectionReducer = StoreReducer @@ -128,6 +151,12 @@ export function getConnectionReducer(): ConnectionReducer { ...action.payload, } } + case ConnectionActionType.CONNECTION_CONFIRM_UPDATE: { + return { + ...state, + ...action.payload, + } + } default: { return state