PayFromSelect.tsx raw
1 "use client";
2
3 import { WalletIcon } from "lucide-react";
4 import React from "react";
5 import AppAvatar from "src/components/AppAvatar";
6 import {
7 Combobox,
8 ComboboxContent,
9 ComboboxEmpty,
10 ComboboxInput,
11 ComboboxItem,
12 ComboboxList,
13 useComboboxAnchor,
14 } from "src/components/ui/combobox";
15 import { InputGroupAddon } from "src/components/ui/input-group";
16 import { Label } from "src/components/ui/label";
17 import { PAY_FROM_SELECT_APPS_LIMIT } from "src/constants";
18 import { useApps } from "src/hooks/useApps";
19 import { getAppDisplayName } from "src/lib/utils";
20 import { App } from "src/types";
21
22 const LIGHTNING_BALANCE = "lightning-balance";
23 const LIGHTNING_BALANCE_LABEL = "Lightning Balance";
24
25 type PayFromOption = {
26 value: string;
27 label: string;
28 app?: App;
29 };
30
31 type Props = {
32 appId?: number;
33 onChange(appId: number | undefined): void;
34 };
35
36 function LightningOption() {
37 return (
38 <div className="flex items-center gap-3">
39 <div className="flex size-6 items-center justify-center rounded-lg bg-muted">
40 <WalletIcon className="size-3 text-muted-foreground" />
41 </div>
42 <div>{LIGHTNING_BALANCE_LABEL}</div>
43 </div>
44 );
45 }
46
47 function AppOption({ app }: { app: App }) {
48 return (
49 <div className="flex items-center gap-3">
50 <AppAvatar app={app} className="size-6 rounded-lg" />
51 <div className="min-w-0">
52 <div>{getAppDisplayName(app.name)}</div>
53 </div>
54 </div>
55 );
56 }
57
58 export default function PayFromSelect({ appId, onChange }: Props) {
59 const anchorRef = useComboboxAnchor();
60 const [search, setSearch] = React.useState("");
61 const { data: appsData } = useApps(PAY_FROM_SELECT_APPS_LIMIT, undefined, {
62 name: search,
63 });
64
65 const apps = React.useMemo(
66 () =>
67 [...(appsData?.apps || [])]
68 .filter((app) => app.scopes.includes("pay_invoice"))
69 .sort((a, b) =>
70 getAppDisplayName(a.name).localeCompare(getAppDisplayName(b.name))
71 ),
72 [appsData?.apps]
73 );
74
75 const options = React.useMemo<PayFromOption[]>(
76 () => [
77 { value: LIGHTNING_BALANCE, label: LIGHTNING_BALANCE_LABEL },
78 ...apps.map((app) => ({
79 value: app.id.toString(),
80 label: getAppDisplayName(app.name),
81 app,
82 })),
83 ],
84 [apps]
85 );
86
87 const selectedOption = options.find((opt) =>
88 appId ? opt.value === appId.toString() : undefined
89 );
90
91 return (
92 <div className="grid w-full gap-1.5">
93 <Label>Pay from</Label>
94 <Combobox
95 items={options}
96 value={selectedOption}
97 itemToStringValue={(option) => option.value}
98 onInputValueChange={setSearch}
99 onValueChange={(option) =>
100 onChange(
101 option?.value === LIGHTNING_BALANCE
102 ? undefined
103 : Number(option?.value)
104 )
105 }
106 >
107 <div ref={anchorRef} className="w-full">
108 <ComboboxInput placeholder={LIGHTNING_BALANCE_LABEL}>
109 <InputGroupAddon>
110 {selectedOption?.app ? (
111 <AppAvatar
112 app={selectedOption.app}
113 className="size-6 rounded-full"
114 />
115 ) : (
116 <div className="flex size-6 items-center justify-center rounded-sm bg-muted">
117 <WalletIcon className="size-3 text-muted-foreground" />
118 </div>
119 )}
120 </InputGroupAddon>
121 </ComboboxInput>
122 </div>
123 <ComboboxContent anchor={anchorRef}>
124 <ComboboxEmpty>No connections found.</ComboboxEmpty>
125 <ComboboxList>
126 {(option: PayFromOption) => (
127 <ComboboxItem key={option.value} value={option}>
128 {option.app ? (
129 <AppOption app={option.app} />
130 ) : (
131 <LightningOption />
132 )}
133 </ComboboxItem>
134 )}
135 </ComboboxList>
136 </ComboboxContent>
137 </Combobox>
138 </div>
139 );
140 }
141