AppUsage.tsx raw

   1  import {
   2    CircleMinusIcon,
   3    CirclePlusIcon,
   4    CopyIcon,
   5    Trash2Icon,
   6  } from "lucide-react";
   7  import React from "react";
   8  import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
   9  import FormattedFiatAmount from "src/components/FormattedFiatAmount";
  10  import { IsolatedAppDrawDownDialog } from "src/components/IsolatedAppDrawDownDialog";
  11  import { IsolatedAppTopupDialog } from "src/components/IsolatedAppTopupDialog";
  12  import { Button } from "src/components/ui/button";
  13  import {
  14    Card,
  15    CardContent,
  16    CardDescription,
  17    CardHeader,
  18    CardTitle,
  19  } from "src/components/ui/card";
  20  import { InputWithAdornment } from "src/components/ui/custom/input-with-adornment";
  21  import { LoadingButton } from "src/components/ui/custom/loading-button";
  22  import { Progress } from "src/components/ui/progress";
  23  import { UpgradeDialog } from "src/components/UpgradeDialog";
  24  import { SUBWALLET_APPSTORE_APP_ID } from "src/constants";
  25  import { useAlbyMe } from "src/hooks/useAlbyMe";
  26  import { useCreateLightningAddress } from "src/hooks/useCreateLightningAddress";
  27  import { useDeleteLightningAddress } from "src/hooks/useDeleteLightningAddress";
  28  import { useTransactions } from "src/hooks/useTransactions";
  29  import { copyToClipboard } from "src/lib/clipboard";
  30  import { cn, getBudgetRenewalLabel } from "src/lib/utils";
  31  import { App, Transaction } from "src/types";
  32  
  33  export function AppUsage({ app }: { app: App }) {
  34    const [page, setPage] = React.useState(1);
  35    const { data: transactionsResponse } = useTransactions(
  36      app.id,
  37      false,
  38      100,
  39      page
  40    );
  41    const [allTransactions, setAllTransactions] = React.useState<Transaction[]>(
  42      []
  43    );
  44    React.useEffect(() => {
  45      if (transactionsResponse?.transactions.length) {
  46        setAllTransactions((current) =>
  47          [...current, ...transactionsResponse.transactions].filter(
  48            (v, i, a) => a.findIndex((t) => t.paymentHash === v.paymentHash) === i // remove duplicates
  49          )
  50        );
  51        setPage((current) => current + 1);
  52      }
  53    }, [transactionsResponse?.transactions]);
  54  
  55    const totalSpentSat = allTransactions
  56      .filter((tx) => tx.type === "outgoing" && tx.state === "settled")
  57      .map((tx) => Math.floor((tx.amountMsat + tx.feesPaidMsat) / 1000))
  58      .reduce((a, b) => a + b, 0);
  59  
  60    const totalReceivedSat = allTransactions
  61      .filter((tx) => tx.type === "incoming")
  62      .map((tx) => tx.amountSat)
  63      .reduce((a, b) => a + b, 0);
  64  
  65    const { data: albyMe } = useAlbyMe();
  66    const [intendedLightningAddress, setIntendedLightningAddress] =
  67      React.useState("");
  68    const { createLightningAddress, creatingLightningAddress } =
  69      useCreateLightningAddress(app.id);
  70    const {
  71      deleteLightningAddress: deleteSubwalletLightningAddress,
  72      deletingLightningAddress,
  73    } = useDeleteLightningAddress(app.id);
  74  
  75    return (
  76      <>
  77        {app.isolated && (
  78          <div
  79            className={cn(
  80              "grid grid-cols-1 gap-3",
  81              app.metadata?.app_store_app_id === SUBWALLET_APPSTORE_APP_ID &&
  82                "lg:grid-cols-2"
  83            )}
  84          >
  85            <Card className="justify-between">
  86              <CardHeader>
  87                <CardTitle>Isolated Balance</CardTitle>
  88              </CardHeader>
  89              <CardContent>
  90                <div className="flex justify-between items-end">
  91                  <div>
  92                    <p className="font-medium text-2xl">
  93                      <FormattedBitcoinAmount amountMsat={app.balanceMsat} />
  94                    </p>
  95                    <FormattedFiatAmount
  96                      amountSat={Math.floor(app.balanceMsat / 1000)}
  97                    />
  98                  </div>
  99                  <div className="flex gap-2 items-center">
 100                    {app.balanceMsat > 0 && (
 101                      <IsolatedAppDrawDownDialog appId={app.id}>
 102                        <Button size="sm" variant="outline">
 103                          <CircleMinusIcon />
 104                          Decrease
 105                        </Button>
 106                      </IsolatedAppDrawDownDialog>
 107                    )}
 108                    <IsolatedAppTopupDialog appId={app.id}>
 109                      <Button size="sm" variant="outline">
 110                        <CirclePlusIcon />
 111                        Increase
 112                      </Button>
 113                    </IsolatedAppTopupDialog>
 114                  </div>
 115                </div>
 116              </CardContent>
 117            </Card>
 118            {app.metadata?.app_store_app_id === SUBWALLET_APPSTORE_APP_ID && (
 119              <Card className="justify-between">
 120                <CardHeader>
 121                  <CardTitle>Lightning Address</CardTitle>
 122  
 123                  {!app.metadata.lud16 && (
 124                    <CardDescription>
 125                      <p className="text-muted-foreground">
 126                        Create a lightning address for this particular app
 127                        connection
 128                      </p>
 129                    </CardDescription>
 130                  )}
 131                </CardHeader>
 132                <CardContent>
 133                  {!app.metadata.lud16 && (
 134                    <div className="flex items-center gap-2">
 135                      <InputWithAdornment
 136                        type="text"
 137                        value={intendedLightningAddress}
 138                        onChange={(e) =>
 139                          setIntendedLightningAddress(e.target.value)
 140                        }
 141                        required
 142                        autoComplete="off"
 143                        endAdornment={
 144                          <span className="mr-1 text-muted-foreground text-xs">
 145                            @getalby.com
 146                          </span>
 147                        }
 148                        className="flex-1"
 149                      />
 150                      {!albyMe?.subscription.plan_code ? (
 151                        <UpgradeDialog>
 152                          <Button className="shrink-0" variant="secondary">
 153                            Create
 154                          </Button>
 155                        </UpgradeDialog>
 156                      ) : (
 157                        <LoadingButton
 158                          className="shrink-0"
 159                          variant="secondary"
 160                          loading={creatingLightningAddress}
 161                          onClick={() =>
 162                            createLightningAddress(intendedLightningAddress)
 163                          }
 164                        >
 165                          Create
 166                        </LoadingButton>
 167                      )}
 168                    </div>
 169                  )}
 170                  {app.metadata.lud16 && (
 171                    <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
 172                      <p className="font-semibold break-all min-w-0 flex-1">
 173                        {app.metadata.lud16}
 174                      </p>
 175                      <div className="flex items-center gap-2 shrink-0">
 176                        <Button
 177                          size="sm"
 178                          onClick={() =>
 179                            copyToClipboard(app.metadata?.lud16 || "")
 180                          }
 181                          variant="outline"
 182                        >
 183                          <CopyIcon />
 184                          Copy
 185                        </Button>
 186                        <LoadingButton
 187                          size="sm"
 188                          variant="outline"
 189                          loading={deletingLightningAddress}
 190                          onClick={deleteSubwalletLightningAddress}
 191                        >
 192                          <Trash2Icon />
 193                          Remove
 194                        </LoadingButton>
 195                      </div>
 196                    </div>
 197                  )}
 198                </CardContent>
 199              </Card>
 200            )}
 201          </div>
 202        )}
 203  
 204        <div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
 205          <Card>
 206            <CardHeader>
 207              <CardTitle>Total Spent</CardTitle>
 208            </CardHeader>
 209            <CardContent>
 210              <p className="font-medium text-2xl">
 211                <FormattedBitcoinAmount amountMsat={totalSpentSat * 1000} />
 212              </p>
 213              <FormattedFiatAmount amountSat={totalSpentSat} />
 214            </CardContent>
 215          </Card>
 216          <Card>
 217            <CardHeader>
 218              <CardTitle>Total Received</CardTitle>
 219            </CardHeader>
 220            <CardContent>
 221              <p className="font-medium text-2xl">
 222                <FormattedBitcoinAmount amountMsat={totalReceivedSat * 1000} />
 223              </p>
 224              <FormattedFiatAmount amountSat={totalReceivedSat} />
 225            </CardContent>
 226          </Card>
 227        </div>
 228  
 229        {app.maxAmountSat > 0 && (
 230          <Card>
 231            <CardHeader>
 232              <CardTitle>Budget</CardTitle>
 233            </CardHeader>
 234            <CardContent>
 235              <div className="flex flex-row justify-between mb-2">
 236                <div>
 237                  <p className="text-xs text-secondary-foreground font-medium">
 238                    Left in budget
 239                  </p>
 240                  <p className="text-xl font-medium">
 241                    <FormattedBitcoinAmount
 242                      amountMsat={app.maxAmountMsat - app.budgetUsageMsat}
 243                    />
 244                  </p>
 245                  <FormattedFiatAmount
 246                    amountSat={app.maxAmountSat - app.budgetUsageSat}
 247                  />
 248                </div>
 249                <div>
 250                  <p className="text-xs text-secondary-foreground font-medium">
 251                    Budget renewal
 252                  </p>
 253                  <p className="text-xl font-medium">
 254                    <FormattedBitcoinAmount amountMsat={app.maxAmountMsat} />
 255                    {app.budgetRenewal !== "never" && (
 256                      <> / {getBudgetRenewalLabel(app.budgetRenewal)}</>
 257                    )}
 258                  </p>
 259                  <FormattedFiatAmount amountSat={app.maxAmountSat} />
 260                </div>
 261              </div>
 262              <Progress
 263                className="h-4"
 264                value={100 - (app.budgetUsageSat * 100) / app.maxAmountSat}
 265              />
 266            </CardContent>
 267          </Card>
 268        )}
 269      </>
 270    );
 271  }
 272