Skip to content

Commit

Permalink
Merge pull request #187 from getAlby/task-max-min
Browse files Browse the repository at this point in the history
chore: handle min max values in lnurl withdraw
  • Loading branch information
im-adithya authored Dec 23, 2024
2 parents 7cc220b + 68f0a8c commit 5a40ba2
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 16 deletions.
11 changes: 10 additions & 1 deletion components/DualCurrencyInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
SATS_REGEX,
} from "~/lib/constants";
import { useAppStore } from "~/lib/state/appStore";
import { cn } from "~/lib/utils";
import { RefreshCw } from "./Icons";
import { Input } from "./ui/input";
import { Text } from "./ui/text";
Expand All @@ -17,13 +18,17 @@ type DualCurrencyInputProps = {
setAmount(amount: string): void;
autoFocus?: boolean;
readOnly?: boolean;
max?: number;
min?: number;
};

export function DualCurrencyInput({
amount,
setAmount,
autoFocus = false,
readOnly = false,
max,
min,
}: DualCurrencyInputProps) {
const getFiatAmount = useGetFiatAmount();
const getSatsAmount = useGetSatsAmount();
Expand Down Expand Up @@ -61,7 +66,11 @@ export function DualCurrencyInput({
return (
<View className="w-full flex flex-col items-center justify-center gap-5">
<Input
className="w-full border-transparent bg-transparent text-center mt-3"
className={cn(
"w-full border-transparent bg-transparent text-center mt-3",
((max && Number(amount) > max) || (min && Number(amount) < min)) &&
"text-destructive",
)}
placeholder="0"
keyboardType={inputMode === "sats" ? "number-pad" : "decimal-pad"}
value={inputMode === "sats" ? amount : fiatAmount}
Expand Down
32 changes: 30 additions & 2 deletions pages/send/LNURLPay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Input } from "~/components/ui/input";
import { Text } from "~/components/ui/text";
import { errorToast } from "~/lib/errorToast";
import { LNURLPayServiceResponse, lnurl } from "~/lib/lnurl";
import { cn } from "~/lib/utils";

export function LNURLPay() {
const {
Expand All @@ -24,10 +25,17 @@ export function LNURLPay() {
};
const lnurlDetails: LNURLPayServiceResponse = JSON.parse(lnurlDetailsJSON);
const [isLoading, setLoading] = React.useState(false);
const [amount, setAmount] = React.useState(amountParam ?? 0);
const [amount, setAmount] = React.useState(amountParam ?? "");
const [comment, setComment] = React.useState("");
const [isAmountReadOnly, setAmountReadOnly] = React.useState(false);

const isAmountInvalid = React.useMemo(() => {
const min = Math.floor(lnurlDetails.minSendable / 1000);
const max = Math.floor(lnurlDetails.maxSendable / 1000);

return Number(amount) < min || Number(amount) > max;
}, [amount, lnurlDetails.minSendable, lnurlDetails.maxSendable]);

useEffect(() => {
// Handle fixed amount LNURLs
if (lnurlDetails.minSendable === lnurlDetails.maxSendable) {
Expand Down Expand Up @@ -76,7 +84,27 @@ export function LNURLPay() {
setAmount={setAmount}
readOnly={isAmountReadOnly}
autoFocus={!isAmountReadOnly && !amount}
min={Math.floor(lnurlDetails.minSendable / 1000)}
max={Math.floor(lnurlDetails.maxSendable / 1000)}
/>
<View className="w-full">
<Text
className={cn(
"text-muted-foreground text-center font-semibold2",
amount && isAmountInvalid ? "text-destructive" : "",
)}
>
Between{" "}
{new Intl.NumberFormat().format(
Math.floor(lnurlDetails.minSendable / 1000),
)}
{" and "}
{new Intl.NumberFormat().format(
Math.floor(lnurlDetails.maxSendable / 1000),
)}{" "}
sats
</Text>
</View>
<View className="w-full">
<Text className="text-muted-foreground text-center font-semibold2">
Comment
Expand All @@ -96,7 +124,7 @@ export function LNURLPay() {
size="lg"
className="flex flex-row gap-2"
onPress={requestInvoice}
disabled={isLoading}
disabled={isLoading || isAmountInvalid}
>
{isLoading && <Loading className="text-primary-foreground" />}
<Text>Next</Text>
Expand Down
75 changes: 62 additions & 13 deletions pages/withdraw/Withdraw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,22 @@ import React, { useEffect } from "react";
import { View } from "react-native";
import DismissableKeyboardView from "~/components/DismissableKeyboardView";
import { DualCurrencyInput } from "~/components/DualCurrencyInput";
import { ClipboardPaste } from "~/components/Icons";
import { AlertCircle, ClipboardPaste } from "~/components/Icons";
import Loading from "~/components/Loading";
import QRCodeScanner from "~/components/QRCodeScanner";
import Screen from "~/components/Screen";
import { Button } from "~/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardTitle,
} from "~/components/ui/card";
import { Text } from "~/components/ui/text";
import { useGetFiatAmount } from "~/hooks/useGetFiatAmount";
import { errorToast } from "~/lib/errorToast";
import { useAppStore } from "~/lib/state/appStore";
import { cn } from "~/lib/utils";

export function Withdraw() {
const { url } = useLocalSearchParams<{ url: string }>();
Expand All @@ -26,6 +33,16 @@ export function Withdraw() {
const [lnurlDetails, setLnurlDetails] =
React.useState<LNURLWithdrawServiceResponse>();

const isAmountInvalid = React.useMemo(() => {
if (!lnurlDetails) {
return true;
}
const min = Math.floor(lnurlDetails.minWithdrawable / 1000);
const max = Math.floor(lnurlDetails.maxWithdrawable / 1000);

return Number(valueSat) < min || Number(valueSat) > max;
}, [valueSat, lnurlDetails]);

// Delay starting the QR scanner if url has valid lnurl withdraw info
useEffect(() => {
if (url) {
Expand Down Expand Up @@ -109,17 +126,6 @@ export function Withdraw() {
return;
}

if (Number(valueSat) < lnurlDetails.minWithdrawable / 1000) {
throw new Error(
`Amount below minimum limit of ${lnurlDetails.minWithdrawable} sats`,
);
}
if (Number(valueSat) > lnurlDetails.maxWithdrawable / 1000) {
throw new Error(
`Amount exceeds maximum limit of ${lnurlDetails.maxWithdrawable} sats.`,
);
}

setLoadingConfirm(true);

const nwcClient = useAppStore.getState().nwcClient;
Expand Down Expand Up @@ -222,8 +228,28 @@ export function Withdraw() {
<DualCurrencyInput
amount={valueSat}
setAmount={setValueSat}
min={Math.floor(lnurlDetails.minWithdrawable / 1000)}
max={Math.floor(lnurlDetails.maxWithdrawable / 1000)}
autoFocus
/>
<View className="w-full">
<Text
className={cn(
"text-muted-foreground text-center font-semibold2",
valueSat && isAmountInvalid ? "text-destructive" : "",
)}
>
Between{" "}
{new Intl.NumberFormat().format(
Math.floor(lnurlDetails.minWithdrawable / 1000),
)}
{" and "}
{new Intl.NumberFormat().format(
Math.floor(lnurlDetails.maxWithdrawable / 1000),
)}{" "}
sats
</Text>
</View>
<View className="flex flex-col gap-2 items-center">
<Text className="text-muted-foreground text-center font-semibold2">
Description
Expand All @@ -235,11 +261,34 @@ export function Withdraw() {
</View>
)}
<View className="p-6">
{lnurlDetails.minWithdrawable !==
lnurlDetails.maxWithdrawable && (
<Card className="mb-4">
<CardContent className="flex flex-row items-center gap-4">
<AlertCircle className="text-muted-foreground" />
<View className="flex flex-1 flex-col">
<CardTitle>Withdraw Limit</CardTitle>
<CardDescription>
Enter an amount between{" "}
<Text className="font-bold2 text-sm">
{Math.floor(lnurlDetails.minWithdrawable / 1000)}{" "}
sats
</Text>{" "}
and{" "}
<Text className="font-bold2 text-sm">
{Math.floor(lnurlDetails.maxWithdrawable / 1000)}{" "}
sats
</Text>
</CardDescription>
</View>
</CardContent>
</Card>
)}
<Button
size="lg"
className="flex flex-row gap-2"
onPress={confirm}
disabled={loadingConfirm}
disabled={loadingConfirm || isAmountInvalid}
>
{loadingConfirm && (
<Loading className="text-primary-foreground" />
Expand Down

0 comments on commit 5a40ba2

Please sign in to comment.