ReceiveOffer.tsx raw
1 import { CircleAlertIcon, CopyIcon, RefreshCwIcon } from "lucide-react";
2 import React from "react";
3 import { toast } from "sonner";
4 import AppHeader from "src/components/AppHeader";
5 import QRCode from "src/components/QRCode";
6 import {
7 Alert,
8 AlertDescription,
9 AlertTitle,
10 } from "src/components/ui/alert.tsx";
11 import { Button } from "src/components/ui/button";
12 import {
13 Card,
14 CardContent,
15 CardFooter,
16 CardHeader,
17 CardTitle,
18 } from "src/components/ui/card";
19 import { LoadingButton } from "src/components/ui/custom/loading-button";
20 import { Input } from "src/components/ui/input";
21 import { Label } from "src/components/ui/label";
22
23 import { copyToClipboard } from "src/lib/clipboard";
24 import { CreateOfferRequest } from "src/types";
25 import { request } from "src/utils/request";
26
27 export default function ReceiveOffer() {
28 const [isLoading, setLoading] = React.useState(false);
29 const [description, setDescription] = React.useState<string>("");
30 const [offer, setOffer] = React.useState<string | null>(null);
31
32 const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
33 event.preventDefault();
34
35 try {
36 setLoading(true);
37 const offer = await request<string>("/api/offers", {
38 method: "POST",
39 headers: {
40 "Content-Type": "application/json",
41 },
42 body: JSON.stringify({
43 description,
44 } as CreateOfferRequest),
45 });
46
47 if (offer) {
48 setOffer(offer);
49
50 toast("Successfully created BOLT-12 offer");
51 }
52 } catch (e) {
53 toast.error("Failed to create offer", {
54 description: "" + e,
55 });
56 console.error(e);
57 } finally {
58 setLoading(false);
59 }
60 };
61
62 const copy = () => {
63 copyToClipboard(offer as string);
64 };
65
66 return (
67 <div className="grid gap-5">
68 <AppHeader
69 pageTitle={offer ? "Lightning Offer" : "Create Lightning Offer"}
70 title={offer ? "Lightning Offer" : "Create Lightning Offer"}
71 description={
72 !offer ? "Create a reusable, non-expiring lightning invoice" : ""
73 }
74 />
75 <div className="grid gap-6 md:max-w-lg">
76 {!offer && (
77 <Alert>
78 <CircleAlertIcon className="h-4 w-4" />
79 <AlertTitle>BOLT-12 Offers are in beta</AlertTitle>
80 <AlertDescription>
81 BOLT-12 is not supported by all wallets and nodes in the lightning
82 network. This feature will work only if you have a channel with a
83 node that supports onion message forwarding, and are paid by a
84 lightning wallet that supports paying BOLT-12 offers.
85 </AlertDescription>
86 </Alert>
87 )}
88 {offer ? (
89 <Card>
90 <CardHeader>
91 <CardTitle className="text-center">Lightning Offer</CardTitle>
92 </CardHeader>
93 <CardContent className="flex flex-col items-center gap-6">
94 <QRCode
95 value={offer}
96 className="w-full"
97 paymentType="lightning"
98 />
99 {description && (
100 <p className="text-muted-foreground my-2">{description}</p>
101 )}
102 </CardContent>
103 <CardFooter className="flex flex-col gap-3">
104 <Button className="w-full" onClick={copy} variant="secondary">
105 <CopyIcon className="size-4" />
106 Copy Offer
107 </Button>
108 <Button
109 className="w-full"
110 variant="outline"
111 onClick={() => {
112 setDescription("");
113 setOffer(null);
114 }}
115 >
116 <RefreshCwIcon className="size-4 shrink-0" />
117 New Offer
118 </Button>
119 </CardFooter>
120 </Card>
121 ) : (
122 <form onSubmit={handleSubmit} className="grid gap-6">
123 <div className="grid gap-2">
124 <Label htmlFor="description">Description</Label>
125 <Input
126 id="description"
127 type="text"
128 value={description}
129 placeholder="For e.g. what is this payment for?"
130 onChange={(e) => {
131 setDescription(e.target.value);
132 }}
133 />
134 </div>
135 <div>
136 <LoadingButton
137 className="w-full md:w-auto"
138 loading={isLoading}
139 type="submit"
140 >
141 Create Offer
142 </LoadingButton>
143 </div>
144 </form>
145 )}
146 </div>
147 </div>
148 );
149 }
150