From 8efa2a8b8aa6fc0fac66f1eb233a88729d613b5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rard=20Dethier?= Date: Tue, 21 Nov 2023 17:28:49 +0100 Subject: [PATCH 1/2] feat: improve close. logion-network/logion-internal#1081 --- src/ExtrinsicSubmissionStateView.tsx | 2 +- src/loc/CloseLocButton.test.tsx | 10 +- src/loc/CloseLocButton.tsx | 223 +++++++++--------- src/logion-chain/__mocks__/LogionChainMock.ts | 5 + src/logion-chain/index.tsx | 2 + 5 files changed, 120 insertions(+), 122 deletions(-) diff --git a/src/ExtrinsicSubmissionStateView.tsx b/src/ExtrinsicSubmissionStateView.tsx index 2f37b9f2..b87747fe 100644 --- a/src/ExtrinsicSubmissionStateView.tsx +++ b/src/ExtrinsicSubmissionStateView.tsx @@ -1,4 +1,4 @@ -import { useLogionChain } from './logion-chain/LogionChainContext'; +import { useLogionChain } from './logion-chain'; import ExtrinsicSubmissionResult from './ExtrinsicSubmissionResult'; export interface Props { diff --git a/src/loc/CloseLocButton.test.tsx b/src/loc/CloseLocButton.test.tsx index f091addb..f02412c3 100644 --- a/src/loc/CloseLocButton.test.tsx +++ b/src/loc/CloseLocButton.test.tsx @@ -7,7 +7,6 @@ import { setLocItems, setLocState } from './__mocks__/LocContextMock'; import CloseLocButton from './CloseLocButton'; import { OpenLoc } from 'src/__mocks__/LogionClientMock'; -import { mockSubmittableResult } from 'src/logion-chain/__mocks__/SignatureMock'; import { LocItem, MetadataItem } from './LocItem'; import { FAILED_SUBMISSION, SUCCESSFUL_SUBMISSION, setExtrinsicSubmissionState } from 'src/logion-chain/__mocks__/LogionChainMock'; @@ -36,6 +35,7 @@ describe("CloseLocButton", () => { await clickByName(/Close LOC/); await clickByName("Proceed"); + await clickByName("Close"); await expectNoDialogVisible(); expect(closeCalled).toBe(true); @@ -59,8 +59,9 @@ describe("CloseLocButton", () => { await clickByName(/Close LOC/); await clickByName("Proceed"); - await clickByName("OK"); + await clickByName("Close"); await expectNoDialogVisible(); + expect(closeCalled).toBe(true); }) it("enables auto-ack toggle", async () => { @@ -116,14 +117,11 @@ let closeCalled = false; const successCloseLocMock = async (params: any) => { closeCalled = true; - params.callback(mockSubmittableResult(true)); return params.locState; }; -const failureCloseLocMock = async (params: any) => { +const failureCloseLocMock = async () => { closeCalled = true; - params.callback(mockSubmittableResult(false, "Failed", true)); - throw new Error(); }; let closeLocMock = successCloseLocMock; diff --git a/src/loc/CloseLocButton.tsx b/src/loc/CloseLocButton.tsx index b0a2ee6d..c13bcb7b 100644 --- a/src/loc/CloseLocButton.tsx +++ b/src/loc/CloseLocButton.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { useNavigate } from 'react-router-dom'; import { ProtectionRequest, OpenLoc } from "@logion/client"; import { Col, Row } from "react-bootstrap"; @@ -6,7 +6,6 @@ import { Col, Row } from "react-bootstrap"; import Button from "../common/Button"; import ProcessStep from "../common/ProcessStep"; import Alert from "../common/Alert"; -import ExtrinsicSubmitter, { SignAndSubmit } from "../ExtrinsicSubmitter"; import { useLocContext } from "./LocContext"; import Icon from "../common/Icon"; @@ -15,22 +14,18 @@ import { acceptProtectionRequest } from "./Model"; import { useLegalOfficerContext } from "../legal-officer/LegalOfficerContext"; import { PROTECTION_REQUESTS_PATH, RECOVERY_REQUESTS_PATH } from "../legal-officer/LegalOfficerPaths"; import StaticLabelValue from "../common/StaticLabelValue"; -import { useLogionChain } from "../logion-chain"; +import { useLogionChain, CallCallback, SignAndSubmit } from "../logion-chain"; import { signAndSend } from "../logion-chain/Signature"; -import ClientExtrinsicSubmitter, { Call, CallCallback } from "src/ClientExtrinsicSubmitter"; +import ExtrinsicSubmissionStateView from "src/ExtrinsicSubmissionStateView"; import './CloseLocButton.css'; import Checkbox from "src/components/toggle/Checkbox"; enum CloseStatus { NONE, - START, ACCEPT, CLOSE_PENDING, CLOSING, - ERROR, - VOUCHING, - ACCEPTING, DONE } @@ -44,100 +39,116 @@ export interface Props { export default function CloseLocButton(props: Props) { const navigate = useNavigate(); - const { accounts, axiosFactory, api, signer } = useLogionChain(); + const { accounts, axiosFactory, api, signer, submitCall, extrinsicSubmissionState, clearSubmissionState, submitSignAndSubmit } = useLogionChain(); const { refreshRequests } = useLegalOfficerContext(); const { mutateLocState,locItems, loc, locState } = useLocContext(); const [ closeState, setCloseState ] = useState({ status: CloseStatus.NONE }); - const [ call, setCall ] = useState(); - const [ signAndSubmitVouch, setSignAndSubmitVouch ] = useState(null); const [ autoAck, setAutoAck ] = useState(false); - useEffect(() => { - if (closeState.status === CloseStatus.CLOSE_PENDING) { - setCloseState({ status: CloseStatus.CLOSING }); - const call: Call = async (callback: CallCallback) => - mutateLocState(async current => { - if(signer && current instanceof OpenLoc) { - return current.legalOfficer.close({ - autoAck, - signer, - callback, - }); - } else { - return current; - } - }); - setCall(() => call); - } - }, [ mutateLocState, closeState, setCloseState, signer, autoAck ]); + const closeCall = useMemo(() => { + return async (callback: CallCallback) => + mutateLocState(async current => { + if(signer && current instanceof OpenLoc) { + return current.legalOfficer.close({ + autoAck, + signer, + callback, + }); + } else { + return current; + } + }); + }, [ mutateLocState, autoAck, signer ]); - const canClose = useMemo(() => { - if(locState instanceof OpenLoc) { - return locState.legalOfficer.canClose(autoAck); - } else { - return false; + const close = useCallback(() => { + setCloseState({ status: CloseStatus.CLOSING }); + submitCall(closeCall); + }, [ closeCall, submitCall ]); + + const clear = useCallback(() => { + clearSubmissionState(); + setCloseState({ status: CloseStatus.NONE }); + }, [ clearSubmissionState]); + + const accept = useCallback(async () => { + if (loc) { + clear(); + + const currentAddress = accounts!.current!.accountId.address; + await acceptProtectionRequest(axiosFactory!(currentAddress)!, { + requestId: props.protectionRequest!.id, + locId: loc.id, + }); + refreshRequests!(false); + + if(props.protectionRequest?.isRecovery) { + navigate({pathname: RECOVERY_REQUESTS_PATH, search: "?tab=history"}); + } else { + navigate({pathname: PROTECTION_REQUESTS_PATH, search: "?tab=history"}); + } } - }, [ locState, autoAck ]); + }, [ loc, clear, accounts, axiosFactory, navigate, props.protectionRequest, refreshRequests ]); const alreadyVouched = useCallback(async (lost: string, rescuer: string, currentAddress: string) => { const activeRecovery = await api!.queries.getActiveRecovery( lost, rescuer ); - return !!(activeRecovery && activeRecovery.legalOfficers.find(lo => lo === currentAddress)); - }, [ api ]); - const onCloseSuccess = useCallback(async () => { - if(props.protectionRequest && !props.protectionRequest.isRecovery) { - setCloseState({ status: CloseStatus.ACCEPTING }); - } else if(props.protectionRequest && props.protectionRequest.isRecovery) { - - const lost = props.protectionRequest!.addressToRecover!; - const rescuer = props.protectionRequest!.requesterAddress; - const currentAddress = accounts!.current!.accountId.address; - - if (await alreadyVouched(lost, rescuer, currentAddress)) { - setCloseState({ status: CloseStatus.ACCEPTING }); - } else { - setCloseState({ status: CloseStatus.VOUCHING }); + const vouchRecovery: SignAndSubmit = useMemo(() => { + const currentAddress = accounts!.current!.accountId.address; + if(props.protectionRequest && props.protectionRequest.isRecovery && currentAddress) { + const lost = props.protectionRequest.addressToRecover!; + const rescuer = props.protectionRequest.requesterAddress; - const signAndSubmit: SignAndSubmit = (callback, errorCallback) => signAndSend({ - signerId: currentAddress, - callback, - errorCallback, - submittable: api!.polkadot.tx.recovery.vouchRecovery( - lost, - rescuer, - ), - }); - setSignAndSubmitVouch(() => signAndSubmit); - } + return (callback, errorCallback) => signAndSend({ + signerId: currentAddress, + callback, + errorCallback, + submittable: api!.polkadot.tx.recovery.vouchRecovery( + lost, + rescuer, + ), + }); } else { - setCloseState({ status: CloseStatus.NONE }); + return null; } - }, [ props.protectionRequest, accounts, api, alreadyVouched ]); + }, [ props.protectionRequest, accounts, api ]); - useEffect(() => { - if (closeState.status === CloseStatus.ACCEPTING && loc) { - setCloseState({ status: CloseStatus.NONE }); - (async function() { + const clearAcceptOrVouch = useCallback(async () => { + if(extrinsicSubmissionState.error || !props.protectionRequest) { + clear(); + } else if(props.protectionRequest) { + if(props.protectionRequest.isRecovery) { + const lost = props.protectionRequest.addressToRecover!; + const rescuer = props.protectionRequest.requesterAddress; const currentAddress = accounts!.current!.accountId.address; - await acceptProtectionRequest(axiosFactory!(currentAddress)!, { - requestId: props.protectionRequest!.id, - locId: loc.id, - }); - refreshRequests!(false); - if(props.protectionRequest?.isRecovery) { - navigate({pathname: RECOVERY_REQUESTS_PATH, search: "?tab=history"}); + if (await alreadyVouched(lost, rescuer, currentAddress)) { + await accept(); } else { - navigate({pathname: PROTECTION_REQUESTS_PATH, search: "?tab=history"}); + clearSubmissionState(); + submitSignAndSubmit(vouchRecovery); } - })(); + } else { + await accept(); + } + } else { + throw new Error("Unexpected"); } - }, [ closeState, setCloseState, accounts, axiosFactory, loc, navigate, props.protectionRequest, refreshRequests ]); + }, [ + props.protectionRequest, + accounts, + alreadyVouched, + accept, + clearSubmissionState, + submitSignAndSubmit, + vouchRecovery, + clear, + extrinsicSubmissionState.error, + ]); const canAutoAck = useMemo(() => { if(locState instanceof OpenLoc) { @@ -147,6 +158,14 @@ export default function CloseLocButton(props: Props) { } }, [ locItems, locState ]); + const canClose = useMemo(() => { + if(locState instanceof OpenLoc) { + return locState.legalOfficer.canClose(autoAck); + } else { + return false; + } + }, [ locState, autoAck ]); + if(!loc) { return null; } @@ -167,7 +186,7 @@ export default function CloseLocButton(props: Props) { } } else { closeButtonText = "Close LOC"; - firstStatus = CloseStatus.START; + firstStatus = CloseStatus.CLOSE_PENDING; iconId = "lock"; } @@ -301,7 +320,7 @@ export default function CloseLocButton(props: Props) {

I executed my due diligence and accept to be the legal officer of this user:

setCloseState({ status: CloseStatus.CLOSE_PENDING }) + mayProceed: true, + callback: close, } ]} > @@ -341,44 +360,18 @@ export default function CloseLocButton(props: Props) { - setCloseState({ status: CloseStatus.ERROR }) } - /> - - setCloseState({ status: CloseStatus.NONE }) + callback: clearAcceptOrVouch, } - ]} - > - - Could not close LOC. - - - - setCloseState({ status: CloseStatus.ACCEPTING }) } - onError={ () => setCloseState({ status: CloseStatus.ERROR }) } - /> + ) diff --git a/src/logion-chain/__mocks__/LogionChainMock.ts b/src/logion-chain/__mocks__/LogionChainMock.ts index 25b10501..a15ce8a6 100644 --- a/src/logion-chain/__mocks__/LogionChainMock.ts +++ b/src/logion-chain/__mocks__/LogionChainMock.ts @@ -110,6 +110,8 @@ export const SUCCESSFUL_SUBMISSION: unknown = { isFinalized: true, } }, + callEnded: true, + submitted: true, }; export const FAILED_SUBMISSION: unknown = { @@ -122,6 +124,8 @@ export const FAILED_SUBMISSION: unknown = { isFinalized: false, } }, + callEnded: true, + submitted: true, }; export const PENDING_SUBMISSION: unknown = { @@ -169,6 +173,7 @@ export function useLogionChain() { resetSubmissionState: () => {}, submitSignAndSubmit: () => {}, submitCall: (call: Call) => call(() => {}), + clearSubmissionState: () => {}, }; } } diff --git a/src/logion-chain/index.tsx b/src/logion-chain/index.tsx index 9efd09b7..0271d0c6 100644 --- a/src/logion-chain/index.tsx +++ b/src/logion-chain/index.tsx @@ -3,6 +3,7 @@ import { LogionChainContextProvider, AxiosFactory, CallCallback, + SignAndSubmit, } from './LogionChainContext'; export { @@ -12,4 +13,5 @@ export { export type { AxiosFactory, CallCallback, + SignAndSubmit, }; From bb624ff517688fda6e16f267390d5ff95bb0de2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rard=20Dethier?= Date: Wed, 22 Nov 2023 14:12:10 +0100 Subject: [PATCH 2/2] feat: fix wallet transfers. logion-network/logion-internal#1082 --- src/common/CommonContext.tsx | 126 ++++++ src/common/WalletGauge.tsx | 311 ++++++------- .../__snapshots__/WalletGauge.test.tsx.snap | 418 +++++++++++++++++- 3 files changed, 698 insertions(+), 157 deletions(-) diff --git a/src/common/CommonContext.tsx b/src/common/CommonContext.tsx index 033f7cf1..8678040c 100644 --- a/src/common/CommonContext.tsx +++ b/src/common/CommonContext.tsx @@ -34,6 +34,9 @@ export interface CommonContext { viewer: Viewer; setViewer: ((viewer: Viewer) => void) | null; backendConfig: ((legalOfficerAddress: string | undefined) => BackendConfig); + expectNewTransactionState: ExpectNewTransactionState; + expectNewTransaction: () => void; + stopExpectNewTransaction: () => void; } interface FullCommonContext extends CommonContext { @@ -49,6 +52,18 @@ const DEFAULT_BACKEND_CONFIG: BackendConfig = { } }; +export enum ExpectNewTransactionStatus { + IDLE, + WAITING_NEW_TRANSACTION, + DONE +} + +export interface ExpectNewTransactionState { + status: ExpectNewTransactionStatus; + minExpectedTransactions?: number; + refreshCount: number; +} + function initialContextValue(): FullCommonContext { return { dataAddress: null, @@ -62,6 +77,12 @@ function initialContextValue(): FullCommonContext { viewer: "User", setViewer: null, backendConfig: () => DEFAULT_BACKEND_CONFIG, + expectNewTransactionState: { + status: ExpectNewTransactionStatus.IDLE, + refreshCount: 0, + }, + expectNewTransaction: () => {}, + stopExpectNewTransaction: () => {}, } } @@ -81,6 +102,10 @@ type ActionType = 'FETCH_IN_PROGRESS' | 'MUTATE_BALANCE_STATE' | 'SET_SET_VIEWER' | 'SET_VIEWER' + | 'EXPECT_NEW_TRANSACTION' + | 'SET_EXPECT_NEW_TRANSACTION' + | 'STOP_EXPECT_NEW_TRANSACTION' + | 'SET_STOP_EXPECT_NEW_TRANSACTION' ; interface Action { @@ -101,8 +126,13 @@ interface Action { viewer?: Viewer; setViewer?: (viewer: Viewer) => void; backendConfig?: ((legalOfficerAddress: string | undefined) => BackendConfig); + expectNewTransaction?: () => void; + stopExpectNewTransaction?: () => void; } +const MAX_REFRESH_COUNT = 12; +const REFRESH_PERIOD_MS = 3000; + const reducer: Reducer = (state: FullCommonContext, action: Action): FullCommonContext => { switch (action.type) { case 'FETCH_IN_PROGRESS': @@ -116,6 +146,16 @@ const reducer: Reducer = (state: FullCommonContext, a if(action.dataAddress === state.dataAddress) { const nodesUp = action.nodesUp !== undefined ? action.nodesUp : state.nodesUp; const nodesDown = action.nodesDown !== undefined ? action.nodesDown : state.nodesDown; + const expectNewTransactionState = buildNextExpectNewTransactionState(state.expectNewTransactionState, state.balanceState); + if(expectNewTransactionState.status === ExpectNewTransactionStatus.WAITING_NEW_TRANSACTION) { + console.log(`Scheduling retry #${expectNewTransactionState.refreshCount} (${state.balanceState!.transactions?.length} < ${state.expectNewTransactionState.minExpectedTransactions!})...`); + window.setTimeout(() => { + console.log(`Try #${ expectNewTransactionState.refreshCount }...`); + state.refresh(false); + }, REFRESH_PERIOD_MS); + } else { + console.log(`Stopped polling after ${state.expectNewTransactionState.refreshCount} retries (${state.balanceState?.transactions.length} >= ${state.expectNewTransactionState.minExpectedTransactions!})`); + } return { ...state, balanceState: action.balanceState, @@ -123,6 +163,7 @@ const reducer: Reducer = (state: FullCommonContext, a backendConfig: action.backendConfig!, nodesUp, nodesDown, + expectNewTransactionState, }; } else { return state; @@ -162,12 +203,71 @@ const reducer: Reducer = (state: FullCommonContext, a ...state, viewer: action.viewer!, }; + case 'SET_EXPECT_NEW_TRANSACTION': + return { + ...state, + expectNewTransaction: action.expectNewTransaction!, + } + case 'EXPECT_NEW_TRANSACTION': + if(state.expectNewTransactionState.status === ExpectNewTransactionStatus.IDLE || state.expectNewTransactionState.status === ExpectNewTransactionStatus.DONE) { + window.setTimeout(() => { + console.log(`Try #1...`); + state.refresh(false); + }, REFRESH_PERIOD_MS); + return { + ...state, + expectNewTransactionState: { + status: ExpectNewTransactionStatus.WAITING_NEW_TRANSACTION, + minExpectedTransactions: state.balanceState ? state.balanceState.transactions.length + 1 : 1, + refreshCount: 1, + }, + } + } else { + return state; + } + case 'SET_STOP_EXPECT_NEW_TRANSACTION': + return { + ...state, + stopExpectNewTransaction: action.stopExpectNewTransaction!, + }; + case 'STOP_EXPECT_NEW_TRANSACTION': + return { + ...state, + expectNewTransactionState: { + status: ExpectNewTransactionStatus.IDLE, + minExpectedTransactions: undefined, + refreshCount: 0, + }, + }; default: /* istanbul ignore next */ throw new Error(`Unknown type: ${action.type}`); } } +function buildNextExpectNewTransactionState(current: ExpectNewTransactionState, balanceState?: BalanceState): ExpectNewTransactionState { + if(current.status === ExpectNewTransactionStatus.WAITING_NEW_TRANSACTION) { + if(balanceState + && (balanceState.transactions.length >= current.minExpectedTransactions! + || current.refreshCount >= MAX_REFRESH_COUNT)) { + return { + status: ExpectNewTransactionStatus.DONE, + minExpectedTransactions: undefined, + refreshCount: 0, + }; + } else { + const refreshCount = current.refreshCount + 1; + return { + status: ExpectNewTransactionStatus.WAITING_NEW_TRANSACTION, + refreshCount, + minExpectedTransactions: current.minExpectedTransactions, + }; + } + } else { + return current; + } +} + export function CommonContextProvider(props: Props) { const { api, client, accounts } = useLogionChain(); const [ contextValue, dispatch ] = useReducer(reducer, initialContextValue()); @@ -296,6 +396,32 @@ export function CommonContextProvider(props: Props) { } }, [ contextValue ]); + const expectNewTransaction = useCallback(() => { + dispatch({ type: "EXPECT_NEW_TRANSACTION" }); + }, [ ]); + + useEffect(() => { + if(contextValue.expectNewTransaction !== expectNewTransaction) { + dispatch({ + type: "SET_EXPECT_NEW_TRANSACTION", + expectNewTransaction, + }) + } + }, [ contextValue.expectNewTransaction, expectNewTransaction ]); + + const stopExpectNewTransaction = useCallback(() => { + dispatch({ type: "STOP_EXPECT_NEW_TRANSACTION" }); + }, [ ]); + + useEffect(() => { + if(contextValue.stopExpectNewTransaction !== stopExpectNewTransaction) { + dispatch({ + type: "SET_STOP_EXPECT_NEW_TRANSACTION", + stopExpectNewTransaction, + }) + } + }, [ contextValue.stopExpectNewTransaction, stopExpectNewTransaction ]); + return ( {props.children} diff --git a/src/common/WalletGauge.tsx b/src/common/WalletGauge.tsx index eb8e0742..0028f9ec 100644 --- a/src/common/WalletGauge.tsx +++ b/src/common/WalletGauge.tsx @@ -1,22 +1,21 @@ -import { useState, useCallback } from 'react'; +import { useState, useCallback, useMemo, useEffect } from 'react'; import { Form, Spinner, InputGroup, DropdownButton, Dropdown } from 'react-bootstrap'; import { BalanceState } from '@logion/client'; import { Numbers, CoinBalance, Currency } from '@logion/node-api'; -import { useLogionChain } from '../logion-chain'; -import ClientExtrinsicSubmitter, { Call, CallCallback } from '../ClientExtrinsicSubmitter'; +import { CallCallback, useLogionChain } from '../logion-chain'; import Gauge from './Gauge'; import Button from './Button'; import Icon from './Icon'; import Dialog from './Dialog'; import FormGroup from './FormGroup'; -import { useCommonContext } from './CommonContext'; +import { ExpectNewTransactionStatus, useCommonContext } from './CommonContext'; import Alert from './Alert'; -import TransactionConfirmation, { Status } from "./TransactionConfirmation"; import './WalletGauge.css'; import BalanceDetails from './BalanceDetails'; +import ExtrinsicSubmissionStateView from 'src/ExtrinsicSubmissionStateView'; export interface Props { balance: CoinBalance, @@ -25,24 +24,29 @@ export interface Props { sendButton?: boolean } -interface TransferDialogParams { - title: string, - destination: boolean +interface TransferDialogState { + title: string; + destination: boolean; + show: boolean; } +const HIDDEN_DIALOG: TransferDialogState = { + title: "", + destination: true, + show: false, +}; + export default function WalletGauge(props: Props) { - const { accounts, signer, client } = useLogionChain(); - const { colorTheme, mutateBalanceState } = useCommonContext(); + const { accounts, signer, client, submitCall, extrinsicSubmissionState, clearSubmissionState } = useLogionChain(); + const { colorTheme, mutateBalanceState, expectNewTransaction, expectNewTransactionState, stopExpectNewTransaction } = useCommonContext(); const [ destination, setDestination ] = useState(""); const [ amount, setAmount ] = useState(""); const [ unit, setUnit ] = useState(Numbers.NONE); - const [ signAndSubmit, setSignAndSubmit ] = useState(); - const [ transferDialogParams, setTransferDialogParams ] = useState({title: "", destination: true}); + const [ transferDialogState, setTransferDialogState ] = useState(HIDDEN_DIALOG); const { vaultAddress, sendButton } = props; - const [ transferError, setTransferError ] = useState(false); - const transferCallback = useCallback(() => { - const signAndSubmit: Call = async (callback: CallCallback) => { + const transfer = useMemo(() => { + return async (callback: CallCallback) => { await mutateBalanceState(async (state: BalanceState) => { return await state.transfer({ amount: new Numbers.PrefixedNumber(amount, unit), @@ -51,153 +55,162 @@ export default function WalletGauge(props: Props) { signer: signer! }); }); + expectNewTransaction(); }; - setSignAndSubmit(() => signAndSubmit); - }, [ amount, destination, unit, mutateBalanceState, signer ]); + }, [ amount, destination, unit, mutateBalanceState, signer, expectNewTransaction ]); + + const transferCallback = useCallback(() => { + submitCall(transfer); + }, [ transfer, submitCall ]); const clearFormCallback = useCallback(() => { setDestination(""); setAmount(""); - setSignAndSubmit(undefined); - }, [ setDestination, setAmount, setSignAndSubmit ]) + setTransferDialogState(HIDDEN_DIALOG); + }, [ setDestination, setAmount ]); + + const cancelCallback = useCallback(() => { + clearFormCallback(); + stopExpectNewTransaction(); + clearSubmissionState(); + }, [ clearFormCallback, stopExpectNewTransaction, clearSubmissionState ]); + + useEffect(() => { + if(expectNewTransactionState.status === ExpectNewTransactionStatus.DONE) { + cancelCallback(); + } + }, [ expectNewTransactionState, cancelCallback ]); if(!client) { return null; } return ( - { - return
- - - { (sendButton === undefined || sendButton) && -
- - { vaultAddress !== undefined && - - } -
+
+ + + { (sendButton === undefined || sendButton) && +
+ + { vaultAddress !== undefined && + + } +
+ } + -

{ transferDialogParams.title }

- { - status === Status.TRANSFERRING && - <> - { transferDialogParams.destination && - setDestination(value.target.value) } - /> } - colors={ colorTheme.dialog } - /> - } - - setAmount(value.target.value) } - /> - { - [ - Numbers.NONE, - Numbers.MILLI, - Numbers.MICRO, - Numbers.NANO, - Numbers.PICO, - Numbers.FEMTO, - Numbers.ATTO - ].map(unit => setUnit(unit) }>{ `${ unit.symbol }${ Currency.SYMBOL }` }) - } - - Please enter a valid amount. - - - } - colors={ colorTheme.dialog } - /> - + ] } + size="lg" + > +

{ transferDialogState.title }

+ { + expectNewTransactionState.status === ExpectNewTransactionStatus.IDLE && + <> + { transferDialogState.destination && + setDestination(value.target.value) } + /> } + colors={ colorTheme.dialog } + /> } - + setAmount(value.target.value) } + /> + { + [ + Numbers.NONE, + Numbers.MILLI, + Numbers.MICRO, + Numbers.NANO, + Numbers.PICO, + Numbers.FEMTO, + Numbers.ATTO + ].map(unit => setUnit(unit) }>{ `${ unit.symbol }${ Currency.SYMBOL }` }) + } + + Please enter a valid amount. + + + } + colors={ colorTheme.dialog } + /> + setTransferError(true) } /> - { - (status === Status.EXPECTING_NEW_TRANSACTION || status === Status.WAITING_FOR_NEW_TRANSACTION) && - - -

Transfer successful, waiting for the transaction to be finalized.

-

Note that this may take up to 30 seconds. If you want to proceed, you can safely - click on cancel but your transaction may not show up yet.

-
- } -
-
- }}/> - ) + + } + { + expectNewTransactionState.status === ExpectNewTransactionStatus.WAITING_NEW_TRANSACTION && + + +

Transfer successful, waiting for the transaction to be finalized.

+

Note that this may take up to 30 seconds. If you want to proceed, you can safely + click on cancel but your transaction may not show up yet.

+
+ } + +
+ ); } diff --git a/src/common/__snapshots__/WalletGauge.test.tsx.snap b/src/common/__snapshots__/WalletGauge.test.tsx.snap index d8d63634..d8b486ae 100644 --- a/src/common/__snapshots__/WalletGauge.test.tsx.snap +++ b/src/common/__snapshots__/WalletGauge.test.tsx.snap @@ -1,17 +1,419 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders arc 1`] = ` - - [Function] - + + +
+ +
+ +

+ +

+ + + } + id="destination" + label="Destination" + /> + + + + + LGNT + + + mLGNT + + + µLGNT + + + nLGNT + + + pLGNT + + + fLGNT + + + aLGNT + + + + Please enter a valid amount. + + + } + id="amount" + label="Amount" + noFeedback={true} + /> + + +
+ `; exports[`renders linear 1`] = ` - - [Function] - + + +
+ +
+ +

+ +

+ + + } + id="destination" + label="Destination" + /> + + + + + LGNT + + + mLGNT + + + µLGNT + + + nLGNT + + + pLGNT + + + fLGNT + + + aLGNT + + + + Please enter a valid amount. + + + } + id="amount" + label="Amount" + noFeedback={true} + /> + + +
+ `;