diff --git a/src/components/earn/EarnForm.tsx b/src/components/earn/EarnForm.tsx new file mode 100644 index 00000000..5fe3e732 --- /dev/null +++ b/src/components/earn/EarnForm.tsx @@ -0,0 +1,229 @@ +import { useId } from 'react' +import { yupResolver } from '@hookform/resolvers/yup' +import { HandshakeIcon, Loader2Icon, PercentIcon } from 'lucide-react' +import { useForm, useWatch } from 'react-hook-form' +import type { Resolver, SubmitHandler } from 'react-hook-form' +import { useTranslation } from 'react-i18next' +import * as yup from 'yup' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' +import { Tabs, TabsContent } from '@/components/ui/tabs' +import { OFFER_FEE_ABS_MIN, OFFER_FEE_REL_MIN, OFFER_MINSIZE_MIN } from '@/constants/jam' +import type { OfferType } from '@/constants/jm' +import { cn, factorToPercentage } from '@/lib/utils' +import { SatSymbol } from '../CurrencySymbol' + +const FieldPrefixSatSymbol = ( + +) + +const OFFERTYPE_ABS: OfferType = 'sw0absoffer' +const OFFERTYPE_REL: OfferType = 'sw0reloffer' + +export interface EarnFormValues { + offerType: OfferType + offerAbsoluteFee?: number + offerRelativeFee?: number + offerMinAmount: number +} + +const FORM_INPUT_DEFAULT_VALUES: Required = { + offerType: OFFERTYPE_ABS, + offerRelativeFee: 0.03, + offerAbsoluteFee: 250, + offerMinAmount: 100_000, +} + +const schema = yup + .object() + .shape({ + offerType: yup.string().default(FORM_INPUT_DEFAULT_VALUES.offerType).required(), + offerAbsoluteFee: yup.number().integer().min(OFFER_FEE_ABS_MIN).optional(), + offerRelativeFee: yup.number().min(factorToPercentage(OFFER_FEE_REL_MIN)).optional(), + offerMinAmount: yup.number().integer().min(OFFER_MINSIZE_MIN).required(), + }) + .required() + +const OfferTypeInput = (props: React.ComponentProps) => { + const { t } = useTranslation() + const id = useId() + + return ( + +
+ +
+ + +
+
+
+ +
+ + +
+
+
+ ) +} +interface EarnFormProps { + className?: string + isWaitingMakerStart: boolean + onSubmit: SubmitHandler + disabled?: boolean +} + +export function EarnForm({ className, isWaitingMakerStart, onSubmit, disabled }: EarnFormProps) { + const { t } = useTranslation() + + const { + control, + register, + handleSubmit, + formState: { errors, isSubmitting, isValid }, + getValues, + setValue, + } = useForm({ + mode: 'all', + defaultValues: FORM_INPUT_DEFAULT_VALUES, + // force type (see https://github.com/react-hook-form/resolvers/issues/807) + resolver: yupResolver(schema) as Resolver, + }) + + const watchOfferType = useWatch({ control, name: 'offerType' }) + + return ( +
+ { + setValue('offerType', value, { + shouldValidate: true, // trigger validation + shouldTouch: true, // update touched fields form state + shouldDirty: true, // update dirty and dirty fields form state + }) + }} + /> + + +
+ +

{t('earn.description_abs_fee')}

+
+
{FieldPrefixSatSymbol}
+ + +
+ {errors.offerMinAmount && ( +
+ ERROR +
+ )} +
+
+ +
+ +

{t('earn.description_rel_fee')}

+
+
%
+ + +
+ {errors.offerMinAmount && ( +
+ ERROR +
+ )} +
+
+
+
+ +

{t('rescan_chain.description_blockheight')}

+
+
{FieldPrefixSatSymbol}
+ + +
+ {errors.offerMinAmount && ( +
+ ERROR +
+ )} +
+ + + ) +} diff --git a/src/components/earn/EarnPage.tsx b/src/components/earn/EarnPage.tsx index 9cba77e2..0f2f637b 100644 --- a/src/components/earn/EarnPage.tsx +++ b/src/components/earn/EarnPage.tsx @@ -1,33 +1,24 @@ -import { useId, useMemo, useState } from 'react' -import { yupResolver } from '@hookform/resolvers/yup' +import { useMemo, useState } from 'react' import { startmakerMutation, stopmakerOptions } from '@joinmarket-webui/joinmarket-api-ts/@tanstack/react-query' import type { ErrorMessage, StartMakerRequest } from '@joinmarket-webui/joinmarket-api-ts/jm' import { useMutation, useQuery } from '@tanstack/react-query' -import { AlertTriangle, HandshakeIcon, Loader2Icon, PercentIcon } from 'lucide-react' -import { useForm, useWatch } from 'react-hook-form' -import type { Resolver, SubmitHandler } from 'react-hook-form' +import { AlertTriangle, Loader2Icon } from 'lucide-react' +import type { SubmitHandler } from 'react-hook-form' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' -import * as yup from 'yup' import { useStore } from 'zustand' import { FeeLimitDialog } from '@/components/settings/FeeLimitDialog' import { FeeConfigErrorAlert } from '@/components/ui/FeeConfigErrorAlert' import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' -import { Tabs, TabsContent } from '@/components/ui/tabs' -import { OFFER_FEE_ABS_MIN, OFFER_FEE_REL_MIN, OFFER_MINSIZE_MIN } from '@/constants/jam' -import type { OfferType } from '@/constants/jm' import { useApiClient } from '@/hooks/useApiClient' import { useFeeConfigValidation } from '@/hooks/useFeeConfigValidation' import { useRefreshSession } from '@/hooks/useRefreshSession' import { withQueryDelay } from '@/lib/queryClient' -import { cn, factorToPercentage, isAbsoluteOffer, isRelativeOffer, percentageToFactor } from '@/lib/utils' +import { cn, isAbsoluteOffer, isRelativeOffer, percentageToFactor } from '@/lib/utils' import type { WalletFileName } from '@/lib/utils' import { jmSessionStore } from '@/store/jmSessionStore' import type { Milliseconds } from '@/types/global' -import { SatSymbol } from '../CurrencySymbol' +import { EarnForm, type EarnFormValues } from './EarnForm' import { OfferCard } from './OfferCard' // In order to prevent state mismatch, the 'maker stop' response is delayed shortly. @@ -36,220 +27,7 @@ import { OfferCard } from './OfferCard' // 2022-04-26: With value of 2_000ms, no state corruption could be provoked in a local dev setup. const MAKER_STOP_RESPONSE_DELAY: Milliseconds = 2_000 -const FieldPrefixSatSymbol = ( - -) - -const OFFERTYPE_ABS: OfferType = 'sw0absoffer' -const OFFERTYPE_REL: OfferType = 'sw0reloffer' - -interface Inputs { - offerType: OfferType - offerAbsoluteFee?: number - offerRelativeFee?: number - offerMinAmount: number -} - -const FORM_INPUT_DEFAULT_VALUES: Required = { - offerType: OFFERTYPE_ABS, - offerRelativeFee: 0.03, - offerAbsoluteFee: 250, - offerMinAmount: 100_000, -} - -const schema = yup - .object() - .shape({ - offerType: yup.string().default(FORM_INPUT_DEFAULT_VALUES.offerType).required(), - offerAbsoluteFee: yup.number().integer().min(OFFER_FEE_ABS_MIN).optional(), - offerRelativeFee: yup.number().min(factorToPercentage(OFFER_FEE_REL_MIN)).optional(), - offerMinAmount: yup.number().integer().min(OFFER_MINSIZE_MIN).required(), - }) - .required() - -const OfferTypeInput = (props: React.ComponentProps) => { - const { t } = useTranslation() - const id = useId() - - return ( - -
- -
- - -
-
-
- -
- - -
-
-
- ) -} -interface EarnFormProps { - className?: string - isWaitingMakerStart: boolean - onSubmit: SubmitHandler - disabled?: boolean -} - -function EarnForm({ className, isWaitingMakerStart, onSubmit, disabled }: EarnFormProps) { - const { t } = useTranslation() - - const { - control, - register, - handleSubmit, - formState: { errors, isSubmitting, isValid }, - getValues, - setValue, - } = useForm({ - mode: 'all', - defaultValues: FORM_INPUT_DEFAULT_VALUES, - // force type (see https://github.com/react-hook-form/resolvers/issues/807) - resolver: yupResolver(schema) as Resolver, - }) - - const watchOfferType = useWatch({ control, name: 'offerType' }) - - return ( -
- { - setValue('offerType', value, { - shouldValidate: true, // trigger validation - shouldTouch: true, // update touched fields form state - shouldDirty: true, // update dirty and dirty fields form state - }) - }} - /> - - -
- -

{t('earn.description_abs_fee')}

-
-
{FieldPrefixSatSymbol}
- - -
- {errors.offerMinAmount && ( -
- ERROR -
- )} -
-
- -
- -

{t('earn.description_rel_fee')}

-
-
%
- - -
- {errors.offerMinAmount && ( -
- ERROR -
- )} -
-
-
-
- -

{t('rescan_chain.description_blockheight')}

-
-
{FieldPrefixSatSymbol}
- - -
- {errors.offerMinAmount && ( -
- ERROR -
- )} -
- - - ) -} - -const toStartMakerRequest = (values: Inputs): StartMakerRequest => { +const toStartMakerRequest = (values: EarnFormValues): StartMakerRequest => { // both fee properties need to be provided. // prevent providing an invalid value by setting the ignored prop to zero const cjfee_a = isAbsoluteOffer(values.offerType) ? values.offerAbsoluteFee! : 0 @@ -343,7 +121,7 @@ export const EarnPage = ({ walletFileName }: EarnPageProps) => { } } - const onSubmit: SubmitHandler = async (data) => { + const onSubmit: SubmitHandler = async (data) => { return await startMaker.mutateAsync({ path: { walletname: encodeURIComponent(walletFileName),