{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tip-dialog",
  "type": "registry:block",
  "title": "Lens Tip Dialog",
  "description": "A dialog component for sending tips to Lens Posts and Accounts, with support for multiple tokens and payment sources.",
  "dependencies": [
    "@lens-protocol/react@0.0.0-canary-20250820102520",
    "@lens-protocol/client@0.0.0-canary-20250820102520"
  ],
  "registryDependencies": [
    "button",
    "dialog",
    "input",
    "select",
    "spinner",
    "skeleton",
    "tooltip",
    "https://lensblocks.com/r/use-tip-post-action.json",
    "https://lensblocks.com/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/common/lens-tip-dialog.tsx",
      "content": "\"use client\";\n\nimport {\n  Erc20Amount,\n  evmAddress,\n  NativeAmount,\n  PaymentSource,\n  SessionClient,\n  SigningError,\n  TransactionIndexingError,\n  TxHash,\n  UnauthenticatedError,\n  UnexpectedError,\n  useAuthenticatedUser,\n  ValidationError,\n} from \"@lens-protocol/react\";\nimport { forwardRef, useEffect, useImperativeHandle, useState } from \"react\";\nimport { Dialog, DialogContent, DialogHeader, DialogTitle } from \"@/registry/new-york/ui/dialog\";\nimport { fetchBalancesBulk } from \"@lens-protocol/client/actions\";\nimport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/registry/new-york/ui/select\";\nimport { Input } from \"@/registry/new-york/ui/input\";\nimport { Button } from \"@/registry/new-york/ui/button\";\nimport { Spinner } from \"@/registry/new-york/ui/spinner\";\nimport { LensChainNativeToken } from \"@/registry/new-york/lib/lens-utils\";\nimport { Skeleton } from \"@/registry/new-york/ui/skeleton\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"@/registry/new-york/ui/tooltip\";\nimport { ResultAsync } from \"neverthrow\";\n\nexport interface TipDialogRef {\n  open: () => void;\n  close: () => void;\n  isOpen: boolean;\n}\n\nexport type TipErrors =\n  | SigningError\n  | TransactionIndexingError\n  | UnauthenticatedError\n  | UnexpectedError\n  | ValidationError<string>;\n\nexport interface TipDialogProps {\n  /**\n   * The Lens Session Client used for making authenticated calls\n   */\n  sessionClient: SessionClient | null | undefined;\n\n  /**\n   *  Function to create a tip transaction. Any error thrown will be caught and passed to onError callback.\n   */\n  createTip: (source: PaymentSource, amount: string, tokenAddress: string) => ResultAsync<TxHash, TipErrors>;\n\n  /**\n   * Optional list of supported token addresses to tip with. If not provided, only the native token will be supported.\n   */\n  supportedTokens?: string[];\n\n  /**\n   * Callback fired when a tip is successfully created and the dialog is closed.\n   */\n  onTipCreated?: (txHash: TxHash) => void;\n\n  /**\n   * Callback fired when an error occurs during tip creation.\n   */\n  onTipError?: (error: Error) => void;\n}\n\nexport const LensTipDialog = forwardRef<TipDialogRef, TipDialogProps>(\n  ({ sessionClient, supportedTokens, createTip, onTipCreated, onTipError }, ref) => {\n    const [dialogOpen, setDialogOpen] = useState(false);\n    const [isSubmitting, setIsSubmitting] = useState(false);\n    const [balances, setBalances] = useState<(Erc20Amount | NativeAmount)[]>([]);\n    const [isLoading, setIsLoading] = useState(true);\n    const [error, setError] = useState<Error | null>(null);\n    const [inputError, setInputError] = useState<Error | null>(null);\n    const [selectedTokenAddress, setSelectedTokenAddress] = useState<string>(LensChainNativeToken);\n    const [inputValue, setInputValue] = useState<string>(\"\");\n    const [balance, setBalance] = useState<string>(\"0\");\n    const [paymentSource, setPaymentSource] = useState<PaymentSource>(PaymentSource.Signer);\n\n    const { data: user } = useAuthenticatedUser();\n\n    const account = user?.address;\n    const address = user?.signer;\n\n    useImperativeHandle(ref, () => ({\n      open: () => setDialogOpen(true),\n      close: () => setDialogOpen(false),\n      isOpen: dialogOpen,\n    }));\n\n    const getAddressFromBalance = (balance: Erc20Amount | NativeAmount) => {\n      return balance.__typename === \"NativeAmount\" ? LensChainNativeToken : balance.asset.contract.address;\n    };\n\n    const fetchBalances = async (session: SessionClient, paymentSource: PaymentSource, address: string) => {\n      try {\n        const res = await fetchBalancesBulk(session, {\n          address: paymentSource === PaymentSource.Account ? account : evmAddress(address),\n          tokens: supportedTokens,\n          includeNative: true,\n        });\n\n        if (res.isErr()) {\n          throw res.error;\n        }\n\n        const accountBalances = res.value.filter(\n          balance => balance.__typename === \"NativeAmount\" || balance.__typename === \"Erc20Amount\",\n        );\n        setBalances(accountBalances);\n      } finally {\n        setIsLoading(false);\n      }\n    };\n\n    useEffect(() => {\n      if (!dialogOpen || !sessionClient?.isSessionClient() || !address || !account) return;\n      setIsLoading(true);\n\n      fetchBalances(sessionClient, paymentSource, address).catch(err => {\n        setError(err);\n      });\n    }, [sessionClient, dialogOpen, address, account, paymentSource]);\n\n    useEffect(() => {\n      if (!balances.length) {\n        setSelectedTokenAddress(LensChainNativeToken);\n        return;\n      }\n\n      if (!selectedTokenAddress) {\n        if (balances[0].__typename === \"NativeAmount\") {\n          setSelectedTokenAddress(LensChainNativeToken);\n        } else {\n          setSelectedTokenAddress(balances[0]?.asset.contract.address);\n        }\n      }\n    }, [balances]);\n\n    useEffect(() => {\n      if (!balances.length || !selectedTokenAddress) {\n        setBalance(\"0\");\n        return;\n      }\n      const balance =\n        selectedTokenAddress === LensChainNativeToken\n          ? balances.find(b => b.__typename === \"NativeAmount\")\n          : balances.find(b => b.asset.contract.address === selectedTokenAddress);\n      setBalance(balance?.value || \"0\");\n    }, [balances, selectedTokenAddress]);\n\n    useEffect(() => {\n      if (!balance) return;\n      const numericInput = parseFloat(inputValue);\n      const numericBalance = parseFloat(balance);\n      // If input value exceeds balance, set and error, otherwise clear error\n      if (numericInput > numericBalance) {\n        setInputError(new Error(\"Input amount exceeds balance\"));\n      } else {\n        setInputError(null);\n      }\n    }, [inputValue, balance]);\n\n    const onSubmitClick = async () => {\n      if (!selectedTokenAddress || !inputValue) {\n        onTipError?.(new Error(\"Token and amount cannot be falsy\"));\n        return;\n      }\n\n      setIsSubmitting(true);\n\n      const res = await createTip(paymentSource, inputValue, selectedTokenAddress);\n      if (res.isErr()) {\n        onTipError?.(res.error);\n        setIsSubmitting(false);\n        return;\n      }\n\n      const txHash = res.value;\n      if (txHash) {\n        setDialogOpen(false);\n        setInputValue(\"\");\n        onTipCreated?.(txHash);\n      }\n      setIsSubmitting(false);\n    };\n\n    const onPaymentSourceChange = (value: PaymentSource) => {\n      setPaymentSource(value);\n      setSelectedTokenAddress(LensChainNativeToken);\n      setInputValue(\"\");\n    };\n\n    return (\n      <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>\n        <DialogContent className=\"sm:max-w-sm\">\n          <DialogHeader className=\"text-left border-b\">\n            <DialogTitle>Send a tip</DialogTitle>\n          </DialogHeader>\n          <div className=\"flex flex-col gap-4 min-w-0 px-4 pb-4\">\n            {(error || (!isLoading && balances.length === 0)) && (\n              <div className=\"py-4 flex gap-4 items-center\">\n                <span className=\"text-red-500\">No tokens available</span>\n              </div>\n            )}\n\n            {isLoading && balances.length === 0 && (\n              <div className=\"flex flex-col gap-4 min-w-0\">\n                <div className=\"flex gap-4 items-center\">\n                  <Skeleton className=\"h-8 flex-grow\" />\n                  <Skeleton className=\"h-8 w-1/3\" />\n                </div>\n                <div className=\"flex gap-4\">\n                  <Skeleton className=\"h-8 flex-grow\" />\n                  <Skeleton className=\"h-8 w-1/6\" />\n                </div>\n                <div className=\"flex gap-8 justify-between\">\n                  <Skeleton className=\"h-4 w-1/3 mt-1\" />\n                  <Skeleton className=\"h-8 w-1/3\" />\n                </div>\n              </div>\n            )}\n\n            {balances.length > 0 && (\n              <div className=\"flex flex-col gap-4 min-w-0\">\n                <div className=\"flex gap-4 items-center\">\n                  <Select value={selectedTokenAddress} onValueChange={setSelectedTokenAddress}>\n                    <SelectTrigger className=\"flex-grow\">\n                      <SelectValue placeholder=\"Select a token\" />\n                    </SelectTrigger>\n                    <SelectContent>\n                      <SelectGroup>\n                        {balances.map(balance => (\n                          <SelectItem\n                            key={getAddressFromBalance(balance)}\n                            value={getAddressFromBalance(balance)}\n                            disabled={balance.value === \"0\"}\n                          >\n                            {balance.asset.name} ({balance.asset.symbol})\n                          </SelectItem>\n                        ))}\n                      </SelectGroup>\n                    </SelectContent>\n                  </Select>\n                  <Select value={paymentSource} onValueChange={onPaymentSourceChange}>\n                    <SelectTrigger className=\"flex-none\">\n                      <SelectValue placeholder=\"Source\" />\n                    </SelectTrigger>\n                    <SelectContent>\n                      <SelectGroup>\n                        <SelectItem key={PaymentSource.Signer} value={PaymentSource.Signer}>\n                          from Wallet\n                        </SelectItem>\n                        <SelectItem key={PaymentSource.Account} value={PaymentSource.Account}>\n                          from Account\n                        </SelectItem>\n                      </SelectGroup>\n                    </SelectContent>\n                  </Select>\n                </div>\n                <div className=\"flex gap-4\">\n                  <Input\n                    id=\"amount\"\n                    type=\"number\"\n                    placeholder=\"Amount\"\n                    value={inputValue}\n                    onChange={e => setInputValue(e.target.value)}\n                    className={`flex-grow ${inputError ? \"border-red-500 !ring-destructive\" : \"\"}`}\n                    disabled={isLoading}\n                  />\n                  <Button\n                    variant=\"secondary\"\n                    className=\"flex-none\"\n                    onClick={() => setInputValue(balance)}\n                    disabled={isLoading || isSubmitting}\n                  >\n                    Max\n                  </Button>\n                </div>\n\n                <div className=\"flex gap-8 justify-between\">\n                  <span className=\"flex items-center gap-2 truncate text-muted-foreground text-sm h-fit\">\n                    <span className=\"text-muted-foreground\">Balance:</span>\n                    {isLoading ? (\n                      <Spinner />\n                    ) : (\n                      <Tooltip>\n                        <TooltipTrigger asChild>\n                          <span className=\"font-bold\">\n                            {parseFloat(balance).toLocaleString(undefined, {\n                              maximumFractionDigits: 6,\n                              useGrouping: false,\n                            })}\n                            {balance.split(\".\")[1]?.length > 6 ? \"…\" : \"\"}\n                          </span>\n                        </TooltipTrigger>\n                        <TooltipContent>\n                          <p className=\"font-bold\">{balance}</p>\n                        </TooltipContent>\n                      </Tooltip>\n                    )}\n                  </span>\n                  <Button\n                    className=\"flex-none flex items-center gap-2\"\n                    onClick={onSubmitClick}\n                    disabled={isLoading || isSubmitting || !!inputError || !inputValue}\n                  >\n                    {isSubmitting ? (\n                      <>\n                        <Spinner /> Sending...\n                      </>\n                    ) : (\n                      \"Send tip\"\n                    )}\n                  </Button>\n                </div>\n              </div>\n            )}\n          </div>\n        </DialogContent>\n      </Dialog>\n    );\n  },\n);\n",
      "type": "registry:block"
    }
  ]
}
