Skip to content

Commit

Permalink
Merge pull request #343 from dkackman/fix-default-expiry
Browse files Browse the repository at this point in the history
Fix default expiry
  • Loading branch information
Rigidity authored Feb 28, 2025
2 parents 61d8f10 + ed50e30 commit 8b3a931
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 54 deletions.
33 changes: 32 additions & 1 deletion src/components/ui/masked-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,35 @@ const TokenAmountInput = React.forwardRef<HTMLInputElement, XchInputProps>(

TokenAmountInput.displayName = 'TokenAmountInput';

export { MaskedInput, TokenAmountInput };
// Integer input that only accepts positive integers
interface IntegerInputProps extends MaskedInputProps {
min?: number;
max?: number;
}

const IntegerInput = React.forwardRef<HTMLInputElement, IntegerInputProps>(
({ min = 0, max, ...props }, ref) => (
<MaskedInput
placeholder='0'
{...props}
type='text'
inputRef={ref}
decimalScale={0}
allowLeadingZeros={false}
allowNegative={false}
isAllowed={(values) => {
const { floatValue } = values;
if (floatValue === undefined) return true;

if (min !== undefined && floatValue < min) return false;
if (max !== undefined && floatValue > max) return false;

return true;
}}
/>
),
);

IntegerInput.displayName = 'IntegerInput';

export { MaskedInput, TokenAmountInput, IntegerInput };
2 changes: 1 addition & 1 deletion src/contexts/ErrorContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
import { ErrorKind } from '../bindings';

export interface CustomError {
kind: ErrorKind | 'walletconnect' | 'upload';
kind: ErrorKind | 'walletconnect' | 'upload' | 'invalid';
reason: string;
}

Expand Down
14 changes: 0 additions & 14 deletions src/hooks/useDefaultOfferExpiry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,22 +41,8 @@ export function useDefaultOfferExpiry() {
setExpiry(validateExpiry(newExpiry));
};

// Calculate total seconds
const getTotalSeconds = (): number | null => {
if (!validatedExpiry.enabled) return null;

// the || operates on the result of parseInt, so if parseInt returns NaN,
// it will return 1 etc. because NaN is falsy
const days = parseInt(validatedExpiry.days) || 1;
const hours = parseInt(validatedExpiry.hours) || 0;
const minutes = parseInt(validatedExpiry.minutes) || 0;

return days * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60;
};

return {
expiry: validatedExpiry,
setExpiry: setValidatedExpiry,
getTotalSeconds,
};
}
94 changes: 65 additions & 29 deletions src/pages/MakeOffer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { TokenAmountInput } from '@/components/ui/masked-input';
import { IntegerInput, TokenAmountInput } from '@/components/ui/masked-input';
import { Switch } from '@/components/ui/switch';
import { useErrors } from '@/hooks/useErrors';
import { uploadToDexie, uploadToMintGarden } from '@/lib/offerUpload';
Expand All @@ -33,7 +33,7 @@ import {
PlusIcon,
TrashIcon,
} from 'lucide-react';
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useDefaultOfferExpiry } from '@/hooks/useDefaultOfferExpiry';

Expand All @@ -53,7 +53,11 @@ export function MakeOffer() {
const [config, setConfig] = useState<NetworkConfig | null>(null);
const network = config?.network_id ?? 'mainnet';

const { expiry, getTotalSeconds } = useDefaultOfferExpiry();
const { expiry } = useDefaultOfferExpiry();

// Use refs to store initial values that won't trigger re-renders
const initialExpiryRef = useRef(expiry);
const initialStateRef = useRef(state);

useEffect(() => {
commands.networkConfig().then((config) => setConfig(config));
Expand All @@ -64,17 +68,28 @@ export function MakeOffer() {
setMintGardenLink('');
}, [offer]);

// Only run once when component mounts
useEffect(() => {
if (expiry.enabled && state.expiration === null) {
useOfferState.setState({
expiration: {
days: expiry.days.toString(),
hours: expiry.hours.toString(),
minutes: expiry.minutes.toString(),
},
});
const initialExpiry = initialExpiryRef.current;
const initialState = initialStateRef.current;

if (initialExpiry.enabled && initialState.expiration === null) {
const isAllZero =
(parseInt(initialExpiry.days) || 0) === 0 &&
(parseInt(initialExpiry.hours) || 0) === 0 &&
(parseInt(initialExpiry.minutes) || 0) === 0;

if (!isAllZero) {
useOfferState.setState({
expiration: {
days: initialExpiry.days.toString(),
hours: initialExpiry.hours.toString(),
minutes: initialExpiry.minutes.toString(),
},
});
}
}
}, [expiry, state.expiration]);
}, []);

const handleMake = async () => {
setPending(true);
Expand All @@ -84,6 +99,23 @@ export function MakeOffer() {
state.offered.cats.length === 0 &&
state.offered.nfts.length === 1;

let expiresAtSecond = null;
if (state.expiration !== null) {
const days = parseInt(state.expiration.days) || 0;
const hours = parseInt(state.expiration.hours) || 0;
const minutes = parseInt(state.expiration.minutes) || 0;
const totalSeconds = days * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60;
if (totalSeconds <= 0) {
addError({
kind: 'invalid',
reason: t`Expiration must be at least 1 second in the future`,
});
setPending(false);
return;
}
expiresAtSecond = Math.ceil(Date.now() / 1000) + totalSeconds;
}

const data = await commands.makeOffer({
offered_assets: {
xch: toMojos(
Expand Down Expand Up @@ -111,10 +143,7 @@ export function MakeOffer() {
(state.fee || '0').toString(),
walletState.sync.unit.decimals,
),
expires_at_second:
state.expiration === null
? null
: Math.ceil(Date.now() / 1000) + (getTotalSeconds() ?? 0),
expires_at_second: expiresAtSecond,
});

clearOffer();
Expand All @@ -127,9 +156,9 @@ export function MakeOffer() {

const invalid =
state.expiration !== null &&
(isNaN(Number(state.expiration.days)) ||
isNaN(Number(state.expiration.hours)) ||
isNaN(Number(state.expiration.minutes)));
(parseInt(state.expiration.days) || 0) === 0 &&
(parseInt(state.expiration.hours) || 0) === 0 &&
(parseInt(state.expiration.minutes) || 0) === 0;

return (
<>
Expand Down Expand Up @@ -219,7 +248,11 @@ export function MakeOffer() {
onCheckedChange={(value) => {
if (value) {
useOfferState.setState({
expiration: { days: '1', hours: '', minutes: '' },
expiration: {
days: initialExpiryRef.current.days.toString(),
hours: initialExpiryRef.current.hours.toString(),
minutes: initialExpiryRef.current.minutes.toString(),
},
});
} else {
useOfferState.setState({ expiration: null });
Expand All @@ -231,16 +264,17 @@ export function MakeOffer() {
{state.expiration !== null && (
<div className='flex gap-2'>
<div className='relative'>
<Input
<IntegerInput
className='pr-12'
value={state.expiration.days}
placeholder='0'
onChange={(e) => {
min={0}
onValueChange={(values) => {
if (state.expiration === null) return;
useOfferState.setState({
expiration: {
...state.expiration,
days: e.target.value,
days: values.value,
},
});
}}
Expand All @@ -253,16 +287,17 @@ export function MakeOffer() {
</div>

<div className='relative'>
<Input
<IntegerInput
className='pr-12'
value={state.expiration.hours}
placeholder='0'
onChange={(e) => {
min={0}
onValueChange={(values) => {
if (state.expiration === null) return;
useOfferState.setState({
expiration: {
...state.expiration,
hours: e.target.value,
hours: values.value,
},
});
}}
Expand All @@ -275,16 +310,17 @@ export function MakeOffer() {
</div>

<div className='relative'>
<Input
<IntegerInput
className='pr-12'
value={state.expiration.minutes}
placeholder='0'
onChange={(e) => {
min={0}
onValueChange={(values) => {
if (state.expiration === null) return;
useOfferState.setState({
expiration: {
...state.expiration,
minutes: e.target.value,
minutes: values.value,
},
});
}}
Expand Down
22 changes: 13 additions & 9 deletions src/pages/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { IntegerInput } from '@/components/ui/masked-input';
import {
Select,
SelectContent,
Expand Down Expand Up @@ -136,14 +137,15 @@ function GlobalSettings() {
{expiry.enabled && (
<div className='flex gap-2'>
<div className='relative'>
<Input
<IntegerInput
className='pr-12'
value={expiry.days}
placeholder='0'
onChange={(e) => {
min={0}
onValueChange={(values) => {
setExpiry({
...expiry,
days: e.target.value,
days: values.value,
});
}}
/>
Expand All @@ -155,14 +157,15 @@ function GlobalSettings() {
</div>

<div className='relative'>
<Input
<IntegerInput
className='pr-12'
value={expiry.hours}
placeholder='0'
onChange={(e) => {
min={0}
onValueChange={(values) => {
setExpiry({
...expiry,
hours: e.target.value,
hours: values.value,
});
}}
/>
Expand All @@ -174,14 +177,15 @@ function GlobalSettings() {
</div>

<div className='relative'>
<Input
<IntegerInput
className='pr-12'
value={expiry.minutes}
placeholder='0'
onChange={(e) => {
min={0}
onValueChange={(values) => {
setExpiry({
...expiry,
minutes: e.target.value,
minutes: values.value,
});
}}
/>
Expand Down

0 comments on commit 8b3a931

Please sign in to comment.