Support new contracts putCode (#4622)
* Support new contracts putCode * New works * Swap to default constructor as available * Split into steps
This commit is contained in:
parent
7ea7ab774a
commit
1ec6dfd302
@ -13,7 +13,7 @@ import styled from 'styled-components';
|
||||
import { DEV_PHRASE } from '@polkadot/keyring/defaults';
|
||||
import { getEnvironment } from '@polkadot/react-api/util';
|
||||
import { AddressRow, Button, Checkbox, CopyButton, Dropdown, Expander, Input, InputAddress, MarkError, MarkWarning, Modal, TextArea } from '@polkadot/react-components';
|
||||
import { useApi, useLedger } from '@polkadot/react-hooks';
|
||||
import { useApi, useLedger, useStepper } from '@polkadot/react-hooks';
|
||||
import { keyring } from '@polkadot/ui-keyring';
|
||||
import { settings } from '@polkadot/ui-settings';
|
||||
import { isHex, u8aToHex } from '@polkadot/util';
|
||||
@ -199,7 +199,7 @@ function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, typ
|
||||
const { isLedgerEnabled } = useLedger();
|
||||
const [{ address, derivePath, deriveValidation, isSeedValid, pairType, seed, seedType }, setAddress] = useState<AddressState>(() => generateSeed(propsSeed, '', propsSeed ? 'raw' : 'bip', isEthereum ? 'ethereum' : propsType));
|
||||
const [isMnemonicSaved, setIsMnemonicSaved] = useState<boolean>(false);
|
||||
const [step, setStep] = useState(1);
|
||||
const [step, nextStep, prevStep] = useStepper();
|
||||
const [isBusy, setIsBusy] = useState(false);
|
||||
const [{ isNameValid, name }, setName] = useState({ isNameValid: false, name: '' });
|
||||
const [{ isPasswordValid, password }, setPassword] = useState({ isPasswordValid: false, password: '' });
|
||||
@ -268,15 +268,6 @@ function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, typ
|
||||
setIsMnemonicSaved(!isMnemonicSaved);
|
||||
};
|
||||
|
||||
const _nextStep = useCallback(
|
||||
() => setStep((step) => step + 1),
|
||||
[]
|
||||
);
|
||||
|
||||
const _previousStep = useCallback(
|
||||
() => setStep((step) => step - 1),
|
||||
[]
|
||||
);
|
||||
const _onCommit = useCallback(
|
||||
(): void => {
|
||||
if (!isValid) {
|
||||
@ -480,7 +471,7 @@ function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, typ
|
||||
icon='step-forward'
|
||||
isDisabled={!isFirstStepValid}
|
||||
label={t<string>('Next')}
|
||||
onClick={_nextStep}
|
||||
onClick={nextStep}
|
||||
/>
|
||||
}
|
||||
{step === 2 && (
|
||||
@ -488,13 +479,13 @@ function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, typ
|
||||
<Button
|
||||
icon='step-backward'
|
||||
label={t<string>('Prev')}
|
||||
onClick={_previousStep}
|
||||
onClick={prevStep}
|
||||
/>
|
||||
<Button
|
||||
icon='step-forward'
|
||||
isDisabled={!isSecondStepValid}
|
||||
label={t<string>('Next')}
|
||||
onClick={_nextStep}
|
||||
onClick={nextStep}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@ -503,7 +494,7 @@ function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, typ
|
||||
<Button
|
||||
icon='step-backward'
|
||||
label={t<string>('Prev')}
|
||||
onClick={_previousStep}
|
||||
onClick={prevStep}
|
||||
/>
|
||||
<Button
|
||||
icon='plus'
|
||||
|
||||
@ -7,15 +7,18 @@ import type { CodeSubmittableResult } from '@polkadot/api-contract/promise/types
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { CodePromise } from '@polkadot/api-contract';
|
||||
import { InputAddress, InputFile, Modal, TxButton } from '@polkadot/react-components';
|
||||
import { useAccountId, useApi, useNonEmptyString } from '@polkadot/react-hooks';
|
||||
import { Button, Dropdown, InputAddress, InputBalance, InputFile, Modal, TxButton } from '@polkadot/react-components';
|
||||
import { useAccountId, useApi, useNonEmptyString, useNonZeroBn, useStepper } from '@polkadot/react-hooks';
|
||||
import { Available } from '@polkadot/react-query';
|
||||
import { keyring } from '@polkadot/ui-keyring';
|
||||
import { isNull, isWasm } from '@polkadot/util';
|
||||
|
||||
import { ABI, InputName } from '../shared';
|
||||
import { ENDOWMENT } from '../constants';
|
||||
import { ABI, InputMegaGas, InputName, MessageSignature, Params } from '../shared';
|
||||
import store from '../store';
|
||||
import { useTranslation } from '../translate';
|
||||
import useAbi from '../useAbi';
|
||||
import useWeight from '../useWeight';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
@ -25,10 +28,15 @@ function Upload ({ onClose }: Props): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [accountId, setAccountId] = useAccountId();
|
||||
const [step, nextStep, prevStep] = useStepper();
|
||||
const [uploadTx, setUploadTx] = useState<SubmittableExtrinsic<'promise'> | null>(null);
|
||||
const [constructorIndex, setConstructorIndex] = useState(0);
|
||||
const [endowment, isEndowmentValid, setEndowment] = useNonZeroBn(ENDOWMENT);
|
||||
const [params, setParams] = useState<any[]>([]);
|
||||
const [[wasm, isWasmValid], setWasm] = useState<[Uint8Array | null, boolean]>([null, false]);
|
||||
const [name, isNameValid, setName] = useNonEmptyString();
|
||||
const { abiName, contractAbi, errorText, isAbiError, isAbiSupplied, isAbiValid, onChangeAbi, onRemoveAbi } = useAbi();
|
||||
const weight = useWeight();
|
||||
|
||||
const code = useMemo(
|
||||
() => isAbiValid && isWasmValid && wasm && contractAbi
|
||||
@ -37,9 +45,32 @@ function Upload ({ onClose }: Props): React.ReactElement {
|
||||
[api, contractAbi, isAbiValid, isWasmValid, wasm]
|
||||
);
|
||||
|
||||
const constructOptions = useMemo(
|
||||
() => contractAbi
|
||||
? contractAbi.constructors.map((message, index) => ({
|
||||
info: message.identifier,
|
||||
key: `${index}`,
|
||||
text: (
|
||||
<MessageSignature
|
||||
asConstructor
|
||||
message={message}
|
||||
/>
|
||||
),
|
||||
value: index
|
||||
}))
|
||||
: [],
|
||||
[contractAbi]
|
||||
);
|
||||
|
||||
useEffect((): void => {
|
||||
setUploadTx(() => code ? code.createBlueprint() : null);
|
||||
}, [code]);
|
||||
setConstructorIndex(() =>
|
||||
constructOptions.find(({ info }) => info === 'default')?.value || 0
|
||||
);
|
||||
}, [constructOptions]);
|
||||
|
||||
useEffect((): void => {
|
||||
setParams([]);
|
||||
}, [constructorIndex]);
|
||||
|
||||
useEffect((): void => {
|
||||
setWasm(
|
||||
@ -53,6 +84,20 @@ function Upload ({ onClose }: Props): React.ReactElement {
|
||||
abiName && setName(abiName);
|
||||
}, [abiName, setName]);
|
||||
|
||||
useEffect((): void => {
|
||||
let contract: SubmittableExtrinsic<'promise'> | null = null;
|
||||
|
||||
try {
|
||||
contract = code && contractAbi && endowment
|
||||
? code.createContract(constructorIndex, { gasLimit: weight.weight, value: endowment }, params)
|
||||
: null;
|
||||
} catch (error) {
|
||||
// ignore, is null
|
||||
}
|
||||
|
||||
setUploadTx(() => contract);
|
||||
}, [code, contractAbi, constructorIndex, endowment, params, weight]);
|
||||
|
||||
const _onAddWasm = useCallback(
|
||||
(wasm: Uint8Array, name: string): void => {
|
||||
setWasm([wasm, isWasm(wasm)]);
|
||||
@ -66,75 +111,132 @@ function Upload ({ onClose }: Props): React.ReactElement {
|
||||
result.blueprint && store
|
||||
.saveCode(result.blueprint.codeHash, {
|
||||
abi: JSON.stringify(result.blueprint.abi.json),
|
||||
name: name || '<unknown>',
|
||||
name: name || '<>',
|
||||
tags: []
|
||||
})
|
||||
.catch(console.error);
|
||||
result.contract && keyring.saveContract(result.contract.address.toString(), {
|
||||
contract: {
|
||||
abi: JSON.stringify(result.contract.abi.json),
|
||||
genesisHash: api.genesisHash.toHex()
|
||||
},
|
||||
name: name || '<>',
|
||||
tags: []
|
||||
});
|
||||
},
|
||||
[name]
|
||||
[api, name]
|
||||
);
|
||||
|
||||
const isSubmittable = !!accountId && (!isNull(name) && isNameValid) && isWasmValid && isAbiSupplied && isAbiValid && !!uploadTx;
|
||||
const isSubmittable = !!accountId && (!isNull(name) && isNameValid) && isWasmValid && isAbiSupplied && isAbiValid && !!uploadTx && step === 2;
|
||||
const invalidAbi = isAbiError || !isAbiSupplied;
|
||||
|
||||
return (
|
||||
<Modal header={t('Upload WASM')}>
|
||||
<Modal header={t('Upload & deploy code {{info}}', { replace: { info: `${step}/2` } })}>
|
||||
<Modal.Content>
|
||||
<InputAddress
|
||||
help={t('Specify the user account to use for this deployment. Any fees will be deducted from this account.')}
|
||||
isInput={false}
|
||||
label={t('deployment account')}
|
||||
labelExtra={
|
||||
<Available
|
||||
label={t<string>('transferrable')}
|
||||
params={accountId}
|
||||
/>
|
||||
}
|
||||
onChange={setAccountId}
|
||||
type='account'
|
||||
value={accountId}
|
||||
/>
|
||||
<ABI
|
||||
contractAbi={contractAbi}
|
||||
errorText={errorText}
|
||||
isError={invalidAbi}
|
||||
isSupplied={isAbiSupplied}
|
||||
isValid={isAbiValid}
|
||||
label={t<string>('json for either ABI or .contract bundle')}
|
||||
onChange={onChangeAbi}
|
||||
onRemove={onRemoveAbi}
|
||||
withWasm
|
||||
/>
|
||||
{!invalidAbi && contractAbi && (
|
||||
{step === 1 && (
|
||||
<>
|
||||
{!contractAbi.project.source.wasm.length && (
|
||||
<InputFile
|
||||
help={t<string>('The compiled WASM for the contract that you wish to deploy. Each unique code blob will be attached with a code hash that can be used to create new instances.')}
|
||||
isError={!isWasmValid}
|
||||
label={t<string>('compiled contract WASM')}
|
||||
onChange={_onAddWasm}
|
||||
placeholder={
|
||||
wasm && !isWasmValid
|
||||
? t<string>('The code is not recognized as being in valid WASM format')
|
||||
: null
|
||||
}
|
||||
/>
|
||||
<InputAddress
|
||||
help={t('Specify the user account to use for this deployment. Any fees will be deducted from this account.')}
|
||||
isInput={false}
|
||||
label={t('deployment account')}
|
||||
labelExtra={
|
||||
<Available
|
||||
label={t<string>('transferrable')}
|
||||
params={accountId}
|
||||
/>
|
||||
}
|
||||
onChange={setAccountId}
|
||||
type='account'
|
||||
value={accountId}
|
||||
/>
|
||||
<ABI
|
||||
contractAbi={contractAbi}
|
||||
errorText={errorText}
|
||||
isError={invalidAbi}
|
||||
isSupplied={isAbiSupplied}
|
||||
isValid={isAbiValid}
|
||||
label={t<string>('json for either ABI or .contract bundle')}
|
||||
onChange={onChangeAbi}
|
||||
onRemove={onRemoveAbi}
|
||||
withWasm
|
||||
/>
|
||||
{!invalidAbi && contractAbi && (
|
||||
<>
|
||||
{!contractAbi.project.source.wasm.length && (
|
||||
<InputFile
|
||||
help={t<string>('The compiled WASM for the contract that you wish to deploy. Each unique code blob will be attached with a code hash that can be used to create new instances.')}
|
||||
isError={!isWasmValid}
|
||||
label={t<string>('compiled contract WASM')}
|
||||
onChange={_onAddWasm}
|
||||
placeholder={
|
||||
wasm && !isWasmValid
|
||||
? t<string>('The code is not recognized as being in valid WASM format')
|
||||
: null
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<InputName
|
||||
isError={!isNameValid}
|
||||
onChange={setName}
|
||||
value={name || undefined}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<InputName
|
||||
isError={!isNameValid}
|
||||
onChange={setName}
|
||||
value={name || undefined}
|
||||
</>
|
||||
)}
|
||||
{step === 2 && contractAbi && (
|
||||
<>
|
||||
<Dropdown
|
||||
help={t<string>('The deployment constructor information for this contract, as provided by the ABI.')}
|
||||
isDisabled={contractAbi.constructors.length <= 1}
|
||||
label={t('deployment constructor')}
|
||||
onChange={setConstructorIndex}
|
||||
options={constructOptions}
|
||||
value={constructorIndex}
|
||||
/>
|
||||
<Params
|
||||
onChange={setParams}
|
||||
params={contractAbi.constructors[constructorIndex].args}
|
||||
registry={contractAbi.registry}
|
||||
/>
|
||||
<InputBalance
|
||||
help={t<string>('The allotted endowment for the deployed contract, i.e. the amount transferred to the contract upon instantiation.')}
|
||||
isError={!isEndowmentValid}
|
||||
label={t<string>('endowment')}
|
||||
onChange={setEndowment}
|
||||
value={endowment}
|
||||
/>
|
||||
<InputMegaGas
|
||||
help={t<string>('The maximum amount of gas that can be used by this deployment, if the code requires more, the deployment will fail.')}
|
||||
weight={weight}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Modal.Content>
|
||||
<Modal.Actions onCancel={onClose}>
|
||||
{step === 1
|
||||
? (
|
||||
<Button
|
||||
icon='step-forward'
|
||||
isDisabled={!code || !contractAbi}
|
||||
label={t<string>('Next')}
|
||||
onClick={nextStep}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Button
|
||||
icon='step-backward'
|
||||
label={t<string>('Prev')}
|
||||
onClick={prevStep}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
extrinsic={uploadTx}
|
||||
icon='upload'
|
||||
isDisabled={!isSubmittable}
|
||||
label={t('Upload')}
|
||||
label={t('Deploy')}
|
||||
onClick={onClose}
|
||||
onSuccess={_onSuccess}
|
||||
/>
|
||||
|
||||
@ -1,50 +0,0 @@
|
||||
// Copyright 2017-2021 @polkadot/app-contracts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Button } from '@polkadot/react-components';
|
||||
import { useToggle } from '@polkadot/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function Ink3Banner ({ className = '' }: Props): React.ReactElement<Props> | null {
|
||||
const [isVisible, toggleVisible] = useToggle(true);
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<article className={`${className} warning centered`}>
|
||||
<div className='box'>
|
||||
<p>{t<string>('This contracts interface only supports Substrate 2.0+ with contracts using Ink! 3.0+ or Solang 0.1.5+ with the ability to generate the latest metadata formats.')}</p>
|
||||
<p>{t<string>('Older versions of the contracts ABI cannot be used and will display an error indicating an invalid ABI when uploaded. Ensure you follow the Ink! or solang upgrade instructions if you are using an older version.')}</p>
|
||||
<p>{t<string>("With the ABI updates, there may still be dragons... the new ABI formats are a complete departure and required a lot of under-the-hood changes. So when something doesn't quite work as expected, please let us know via GH issues.")}</p>
|
||||
<Button.Group>
|
||||
<Button
|
||||
icon='times'
|
||||
label={t<string>('Understood, close this')}
|
||||
onClick={toggleVisible}
|
||||
/>
|
||||
</Button.Group>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(styled(Ink3Banner)`
|
||||
.box {
|
||||
padding: 0 0.5rem;
|
||||
|
||||
.ui--Button-Group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
`);
|
||||
@ -4,7 +4,6 @@
|
||||
import type { SubmittableExtrinsic } from '@polkadot/api/types';
|
||||
import type { BlueprintSubmittableResult } from '@polkadot/api-contract/promise/types';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { BlueprintPromise } from '@polkadot/api-contract';
|
||||
@ -36,7 +35,7 @@ function Deploy ({ codeHash, constructorIndex = 0, onClose, setConstructorIndex
|
||||
const [initTx, setInitTx] = useState<SubmittableExtrinsic<'promise'> | null>(null);
|
||||
const [params, setParams] = useState<any[]>([]);
|
||||
const [accountId, isAccountIdValid, setAccountId] = useFormField<string | null>(null);
|
||||
const [endowment, isEndowmentValid, setEndowment] = useNonZeroBn(new BN(ENDOWMENT));
|
||||
const [endowment, isEndowmentValid, setEndowment] = useNonZeroBn(ENDOWMENT);
|
||||
const [salt, setSalt] = useState(() => randomAsHex());
|
||||
const [withSalt, setWithSalt] = useState(false);
|
||||
|
||||
@ -62,6 +61,7 @@ function Deploy ({ codeHash, constructorIndex = 0, onClose, setConstructorIndex
|
||||
const constructOptions = useMemo(
|
||||
() => contractAbi
|
||||
? contractAbi.constructors.map((message, index) => ({
|
||||
info: message.identifier,
|
||||
key: `${index}`,
|
||||
text: (
|
||||
<MessageSignature
|
||||
|
||||
@ -16,7 +16,6 @@ import ContractAdd from '../Contracts/Add';
|
||||
import ContractsTable from '../Contracts/ContractsTable';
|
||||
import store from '../store';
|
||||
import { useTranslation } from '../translate';
|
||||
import Banner from './Banner';
|
||||
import Deploy from './Deploy';
|
||||
import Summary from './Summary';
|
||||
|
||||
@ -71,7 +70,7 @@ function Contracts ({ className = '' }: Props): React.ReactElement<Props> {
|
||||
<Button.Group>
|
||||
<Button
|
||||
icon='plus'
|
||||
label={t('Upload WASM')}
|
||||
label={t('Upload & deploy code')}
|
||||
onClick={toggleUpload}
|
||||
/>
|
||||
<Button
|
||||
@ -85,7 +84,6 @@ function Contracts ({ className = '' }: Props): React.ReactElement<Props> {
|
||||
onClick={toggleAdd}
|
||||
/>
|
||||
</Button.Group>
|
||||
<Banner />
|
||||
<ContractsTable
|
||||
contracts={allContracts}
|
||||
updated={updated}
|
||||
|
||||
@ -35,6 +35,7 @@ export { useParaEndpoints } from './useParaEndpoints';
|
||||
export { usePassword } from './usePassword';
|
||||
export { useRegistrars } from './useRegistrars';
|
||||
export { useSavedFlags } from './useSavedFlags';
|
||||
export { useStepper } from './useStepper';
|
||||
export { useSudo } from './useSudo';
|
||||
export { useToggle } from './useToggle';
|
||||
export { useTreasury } from './useTreasury';
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Copyright 2017-2021 @polkadot/react-hooks authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { isUndefined } from '@polkadot/util';
|
||||
|
||||
@ -11,17 +11,20 @@ export type FormField<T> = [
|
||||
(_?: T | null) => void
|
||||
];
|
||||
|
||||
export function useFormField<T> (defaultValue: T | null, validate: (_: T) => boolean = (): boolean => true): FormField<T> {
|
||||
type ValidateFn<T> = (_: T) => boolean;
|
||||
|
||||
const defaultValidate = (): boolean => true;
|
||||
|
||||
export function useFormField<T> (defaultValue: T | null, validate: ValidateFn<T> = defaultValidate): FormField<T> {
|
||||
const [value, setValue] = useState<T | null>(defaultValue);
|
||||
const isValid = useMemo(
|
||||
(): boolean => !!value && validate(value),
|
||||
() => !!value && validate(value),
|
||||
[validate, value]
|
||||
);
|
||||
const setter = (value?: T | null) => !isUndefined(value) && setValue(value);
|
||||
const setter = useCallback(
|
||||
(value?: T | null) => !isUndefined(value) && setValue(value),
|
||||
[]
|
||||
);
|
||||
|
||||
return [
|
||||
value,
|
||||
isValid,
|
||||
setter
|
||||
];
|
||||
return [value, isValid, setter];
|
||||
}
|
||||
|
||||
@ -3,9 +3,10 @@
|
||||
|
||||
import { FormField, useFormField } from './useFormField';
|
||||
|
||||
export function useNonEmptyString (initialValue = ''): FormField<string> {
|
||||
return useFormField(
|
||||
initialValue,
|
||||
(value?: string | null): boolean => (value && value.length > 0) || false
|
||||
);
|
||||
function isValid (value?: string | null): boolean {
|
||||
return (value && value.length > 0) || false;
|
||||
}
|
||||
|
||||
export function useNonEmptyString (initialValue = ''): FormField<string> {
|
||||
return useFormField(initialValue, isValid);
|
||||
}
|
||||
|
||||
@ -1,15 +1,20 @@
|
||||
// Copyright 2017-2021 @polkadot/react-hooks authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import BN from 'bn.js';
|
||||
import type BN from 'bn.js';
|
||||
|
||||
import { BN_ZERO } from '@polkadot/util';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { BN_ZERO, bnToBn } from '@polkadot/util';
|
||||
|
||||
import { FormField, useFormField } from './useFormField';
|
||||
|
||||
export function useNonZeroBn (initialValue: BN = BN_ZERO): FormField<BN> {
|
||||
return useFormField(
|
||||
initialValue,
|
||||
(value: BN): boolean => !value.isZero()
|
||||
);
|
||||
function isValid (value: BN): boolean {
|
||||
return !value.isZero();
|
||||
}
|
||||
|
||||
export function useNonZeroBn (initialValue: BN | number = BN_ZERO): FormField<BN> {
|
||||
const value = useMemo(() => bnToBn(initialValue), [initialValue]);
|
||||
|
||||
return useFormField(value, isValid);
|
||||
}
|
||||
|
||||
22
packages/react-hooks/src/useStepper.ts
Normal file
22
packages/react-hooks/src/useStepper.ts
Normal file
@ -0,0 +1,22 @@
|
||||
// Copyright 2017-2021 @polkadot/react-hooks authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
type Result = [number, () => void, () => void, (step: number) => void];
|
||||
|
||||
export function useStepper (): Result {
|
||||
const [step, setStep] = useState(1);
|
||||
|
||||
const nextStep = useCallback(
|
||||
() => setStep((step) => step + 1),
|
||||
[]
|
||||
);
|
||||
|
||||
const prevStep = useCallback(
|
||||
() => setStep((step) => step - 1),
|
||||
[]
|
||||
);
|
||||
|
||||
return [step, nextStep, prevStep, setStep];
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user