diff --git a/messages/en.json b/messages/en.json index f20727a9..252c4485 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1,5 +1,6 @@ { "App": { + "copy": "Copy", "Dashboard": { "asset": "An asset in your wallet.", "buy": "Buy", @@ -14,8 +15,12 @@ "what-asset": "What is {asset}?" }, "miban": "MIBAN Code", + "save": "Save", "view-all": "View all", "Wallet": { + "amount": "Please specify an amount.", + "amount-custom": "Customize Amount", + "amount-ln": "Please specify an amount to generate a Lightning invoice.", "assets": "Assets", "receive": "Receive", "send": "Send" diff --git a/messages/es.json b/messages/es.json index d6a84b54..0a7a2e7f 100644 --- a/messages/es.json +++ b/messages/es.json @@ -1,5 +1,6 @@ { "App": { + "copy": "Copy", "Dashboard": { "asset": "Un activo en tu billetera.", "buy": "Comprar", @@ -14,8 +15,12 @@ "what-asset": "Qué es {asset}?" }, "miban": "Código MIBAN", + "save": "Save", "view-all": "Ver todos", "Wallet": { + "amount": "Please specify an amount.", + "amount-custom": "Customize Amount", + "amount-ln": "Please specify an amount to generate a Lightning invoice.", "assets": "Activos", "receive": "Recibir", "send": "Envíar" diff --git a/src/app/(app)/(layout)/wallet/[walletId]/account/[accountId]/receive/page.tsx b/src/app/(app)/(layout)/wallet/[walletId]/account/[accountId]/receive/page.tsx deleted file mode 100644 index a815915f..00000000 --- a/src/app/(app)/(layout)/wallet/[walletId]/account/[accountId]/receive/page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -'use client'; - -import { ReceiveAddress } from '@/views/wallet/ReceiveAddress'; - -export default function Page({ - params, -}: { - params: { walletId: string; accountId: string }; -}) { - return ( -
- -
- ); -} diff --git a/src/app/(app)/(layout)/wallet/receive/page.tsx b/src/app/(app)/(layout)/wallet/receive/page.tsx new file mode 100644 index 00000000..735c004c --- /dev/null +++ b/src/app/(app)/(layout)/wallet/receive/page.tsx @@ -0,0 +1,7 @@ +'use client'; + +import { Receive } from '@/views/wallet/Receive'; + +export default function Page() { + return ; +} diff --git a/src/graphql/mutations/__generated__/createInvoice.generated.tsx b/src/graphql/mutations/__generated__/createInvoice.generated.tsx new file mode 100644 index 00000000..2b3494be --- /dev/null +++ b/src/graphql/mutations/__generated__/createInvoice.generated.tsx @@ -0,0 +1,75 @@ +/* THIS FILE IS AUTOMATICALLY GENERATED. DO NOT MODIFY. */ +/* eslint-disable no-unused-vars, @typescript-eslint/no-unused-vars */ +import { gql } from '@apollo/client'; +import * as Apollo from '@apollo/client'; + +import * as Types from '../../types'; + +const defaultOptions = {} as const; +export type CreateLightningInvoiceMutationVariables = Types.Exact<{ + input: Types.CreateLightingInvoiceInput; +}>; + +export type CreateLightningInvoiceMutation = { + __typename?: 'Mutation'; + wallets: { + __typename?: 'WalletMutations'; + create_lightning_invoice: { + __typename?: 'CreateLightingInvoice'; + payment_request: string; + }; + }; +}; + +export const CreateLightningInvoiceDocument = gql` + mutation CreateLightningInvoice($input: CreateLightingInvoiceInput!) { + wallets { + create_lightning_invoice(input: $input) { + payment_request + } + } + } +`; +export type CreateLightningInvoiceMutationFn = Apollo.MutationFunction< + CreateLightningInvoiceMutation, + CreateLightningInvoiceMutationVariables +>; + +/** + * __useCreateLightningInvoiceMutation__ + * + * To run a mutation, you first call `useCreateLightningInvoiceMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useCreateLightningInvoiceMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [createLightningInvoiceMutation, { data, loading, error }] = useCreateLightningInvoiceMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useCreateLightningInvoiceMutation( + baseOptions?: Apollo.MutationHookOptions< + CreateLightningInvoiceMutation, + CreateLightningInvoiceMutationVariables + > +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useMutation< + CreateLightningInvoiceMutation, + CreateLightningInvoiceMutationVariables + >(CreateLightningInvoiceDocument, options); +} +export type CreateLightningInvoiceMutationHookResult = ReturnType< + typeof useCreateLightningInvoiceMutation +>; +export type CreateLightningInvoiceMutationResult = + Apollo.MutationResult; +export type CreateLightningInvoiceMutationOptions = Apollo.BaseMutationOptions< + CreateLightningInvoiceMutation, + CreateLightningInvoiceMutationVariables +>; diff --git a/src/graphql/mutations/createInvoice.ts b/src/graphql/mutations/createInvoice.ts new file mode 100644 index 00000000..cef07784 --- /dev/null +++ b/src/graphql/mutations/createInvoice.ts @@ -0,0 +1,11 @@ +import { gql } from '@apollo/client'; + +export const CreateLightningInvoice = gql` + mutation CreateLightningInvoice($input: CreateLightingInvoiceInput!) { + wallets { + create_lightning_invoice(input: $input) { + payment_request + } + } + } +`; diff --git a/src/graphql/types.ts b/src/graphql/types.ts index 6a4683cc..d0a941bb 100644 --- a/src/graphql/types.ts +++ b/src/graphql/types.ts @@ -91,6 +91,16 @@ export type CreateContactInput = { wallet_id: Scalars['String']['input']; }; +export type CreateLightingInvoice = { + __typename?: 'CreateLightingInvoice'; + payment_request: Scalars['String']['output']; +}; + +export type CreateLightingInvoiceInput = { + amount: Scalars['Float']['input']; + wallet_account_id: Scalars['String']['input']; +}; + export type CreateLiquidTransaction = { __typename?: 'CreateLiquidTransaction'; base_64: Scalars['String']['output']; @@ -810,6 +820,7 @@ export type WalletMutations = { broadcast_liquid_transaction: BroadcastLiquidTransaction; change_name: Scalars['Boolean']['output']; create: CreateWallet; + create_lightning_invoice: CreateLightingInvoice; create_onchain_address: CreateOnchainAddress; create_onchain_address_swap: ReceiveSwap; refresh_wallet: Scalars['Boolean']['output']; @@ -828,6 +839,10 @@ export type WalletMutationsCreateArgs = { input: CreateWalletInput; }; +export type WalletMutationsCreate_Lightning_InvoiceArgs = { + input: CreateLightingInvoiceInput; +}; + export type WalletMutationsCreate_Onchain_AddressArgs = { input: CreateOnchainAddressInput; }; diff --git a/src/utils/routes.ts b/src/utils/routes.ts index 798c1c99..82663493 100644 --- a/src/utils/routes.ts +++ b/src/utils/routes.ts @@ -16,8 +16,7 @@ export const ROUTES = { wallet: { home: '/wallet', settings: (id: string) => `/wallet/${id}/settings`, - receive: (walletId: string, accountId: string) => - `/wallet/${walletId}/account/${accountId}/receive`, + receive: '/wallet/receive', send: { home: (walletId: string, accountId: string, assetId?: string) => `/wallet/${walletId}/account/${accountId}/send${assetId ? `?assetId=${assetId}` : ''}`, diff --git a/src/views/dashboard/BancoCode.tsx b/src/views/dashboard/BancoCode.tsx index aadb2665..e4f36e44 100644 --- a/src/views/dashboard/BancoCode.tsx +++ b/src/views/dashboard/BancoCode.tsx @@ -88,7 +88,7 @@ export const BancoCode: FC<{ id: string }> = ({ id }) => {
{ + const t = useTranslations('App'); + const { toast } = useToast(); + const { Canvas } = useQRCode(); + + const [receive, setReceive] = useState('Any Currency'); + const [receiveString, setReceiveString] = useState(''); + const [optionsOpen, setOptionsOpen] = useState(false); + const [amountOpen, setAmountOpen] = useState(false); + const [amountUSDInput, setAmountUSDInput] = useState(''); + const [amountSatsInput, setAmountSatsInput] = useState(''); + const [amountUSDSaved, setAmountUSDSaved] = useState(''); + const [amountSatsSaved, setAmountSatsSaved] = useState(''); + const [satsFirst, setSatsFirst] = useState(false); + + const [value] = useLocalStorage(LOCALSTORAGE_KEYS.currentWalletId, ''); + + const { + data: detailsData, + loading: detailsLoading, + error: detailsError, + } = useGetWalletDetailsQuery({ + variables: { id: value }, + onError: err => { + const messages = handleApolloError(err); + + toast({ + variant: 'destructive', + title: 'Error getting wallet details.', + description: messages.join(', '), + }); + }, + }); + + const bancoCode = useMemo(() => { + if (!detailsData?.wallets.find_one.money_address.length) return ''; + + const first = detailsData.wallets.find_one.money_address[0]; + const code = first.user + '@' + first.domains[0]; + + setReceiveString(code); + + return code; + }, [detailsData]); + + const { + data: walletData, + loading: walletLoading, + error: walletError, + } = useGetWalletQuery({ + variables: { id: value }, + onError: err => { + const messages = handleApolloError(err); + + toast({ + variant: 'destructive', + title: 'Error getting wallet.', + description: messages.join(', '), + }); + }, + }); + + const liquidAccountId = useMemo( + () => walletData?.wallets.find_one.accounts.find(a => a.liquid)?.id || '', + [walletData?.wallets.find_one.accounts] + ); + + const [ + createLiquidAddress, + { data: liquidData, loading: liquidLoading, error: liquidError }, + ] = useCreateOnchainAddressMutation({ + variables: { input: { wallet_account_id: liquidAccountId } }, + onError: err => { + const messages = handleApolloError(err); + + toast({ + variant: 'destructive', + title: 'Error generating Liquid address.', + description: messages.join(', '), + }); + }, + }); + + useEffect(() => { + if (!liquidAccountId || liquidData) return; + + createLiquidAddress(); + }, [liquidAccountId, liquidData, createLiquidAddress]); + + const liquidAddress = useMemo(() => { + if (!liquidData?.wallets.create_onchain_address.address) { + return { address: '', formatted: '' }; + } + + const address = liquidData.wallets.create_onchain_address.address; + const formatted = (address.match(/.{1,6}/g) || []).join(' - '); + + return { address, formatted }; + }, [liquidData]); + + const [createLightningInvoice, { loading: invoiceLoading }] = + useCreateLightningInvoiceMutation({ + variables: { + input: { + amount: Number(amountSatsInput), + wallet_account_id: liquidAccountId, + }, + }, + onCompleted: data => { + setReceiveString(data.wallets.create_lightning_invoice.payment_request); + toast({ title: 'Invoice generated!' }); + }, + onError: err => { + setReceiveString(''); + setAmountUSDInput(''); + setAmountSatsInput(''); + setAmountUSDSaved(''); + setAmountSatsSaved(''); + setSatsFirst(false); + + const messages = handleApolloError(err); + + toast({ + variant: 'destructive', + title: 'Error generating Lightning invoice.', + description: messages.join(', '), + }); + }, + }); + + const receiveText = useMemo(() => { + switch (receive) { + case 'Any Currency': + return bancoCode; + case 'Lightning': + return receiveString + ? shorten(receiveString, 12) + : t('Wallet.amount-ln'); + case 'Liquid Bitcoin': + case 'Tether USD': + return liquidAddress.formatted; + } + }, [receive, bancoCode, receiveString, liquidAddress, t]); + + const from_date = useMemo( + () => sub(new Date(), { days: 1 }).toISOString(), + [] + ); + + const { + data: priceData, + loading: priceLoading, + error: priceError, + } = useGetPricesHistoricalQuery({ + variables: { + input: { from_date }, + }, + onError: err => { + const messages = handleApolloError(err); + + toast({ + variant: 'destructive', + title: 'Error getting asset prices.', + description: messages.join(', '), + }); + }, + }); + + const latestPrice = useMemo( + () => + priceData?.prices.historical.points + .filter(p => p.value !== null) + .toSorted((a, b) => Date.parse(b.date) - Date.parse(a.date))[0].value, + [priceData] + ); + + const loading = + detailsLoading || + walletLoading || + liquidLoading || + invoiceLoading || + priceLoading; + + const error = + Boolean(detailsError) || + Boolean(walletError) || + Boolean(liquidError) || + Boolean(priceError); + + if (error) return null; + + return ( +
+
+ + + + +

+ {t('Wallet.receive')} +

+
+ + + + + + + +
+ {options.map(o => ( + + + + ))} +
+
+
+ + {loading ? ( + + ) : ( +
+ +
+ )} + + {loading ? ( + + ) : ( +

+ {receiveText} +

+ )} + + {receive !== 'Any Currency' ? ( + { + setTimeout(() => { + setAmountUSDInput(amountUSDSaved); + setAmountSatsInput(amountSatsSaved); + }, 1000); + }} + > +
+ + {amountUSDSaved ? ( + + ) : ( + + )} + + + {amountSatsSaved ? ( +

+ {Number(amountSatsSaved).toLocaleString('en-US')} sats +

+ ) : null} +
+ + +
+ { + if (!latestPrice) return; + + const numberValue = Number(e.target.value); + const decimals = e.target.value.split('.')[1]; + const latestPricePerSat = latestPrice / 100_000_000; + + if (!e.target.value) { + setAmountUSDInput(''); + setAmountSatsInput(''); + return; + } + + if (numberValue < 0) return; + if (satsFirst && numberValue < 1) return; + if (satsFirst && e.target.value.includes('.')) return; + if (!satsFirst && decimals?.length > 2) return; + + if (satsFirst) { + setAmountSatsInput(numberValue.toFixed(0)); + setAmountUSDInput( + (latestPricePerSat * numberValue).toFixed(2) + ); + } else { + setAmountUSDInput(e.target.value); + setAmountSatsInput( + (numberValue / latestPricePerSat).toFixed(0) + ); + } + }} + className="w-full bg-transparent text-center text-5xl font-medium focus:outline-none" + /> + + +
+ +
+

+ {satsFirst + ? formatFiat(Number(amountUSDInput)) + ' USD' + : Number(amountSatsInput).toLocaleString('en-US') + ' sats'} +

+ + +
+ +

+ {t('Wallet.amount')} +

+ + +
+
+ ) : null} + + +
+ ); +}; diff --git a/src/views/wallet/ReceiveAddress.tsx b/src/views/wallet/ReceiveAddress.tsx deleted file mode 100644 index 2a28c20f..00000000 --- a/src/views/wallet/ReceiveAddress.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { Loader2 } from 'lucide-react'; -import { useQRCode } from 'next-qrcode'; -import { FC, useEffect, useMemo } from 'react'; - -import { Button } from '@/components/ui/button'; -import { - Card, - CardContent, - CardFooter, - CardHeader, - CardTitle, -} from '@/components/ui/card'; -import { useCreateOnchainAddressMutation } from '@/graphql/mutations/__generated__/createOnchainAddress.generated'; - -export const ReceiveAddress: FC<{ accountId: string }> = ({ accountId }) => { - const { Canvas } = useQRCode(); - - const [create, { data, loading, error }] = useCreateOnchainAddressMutation({ - variables: { input: { wallet_account_id: accountId } }, - }); - - useEffect(() => { - create(); - }, [create]); - - const addressInfo = useMemo(() => { - if (loading) return { address: '', formatted: '' }; - if (error) return { address: '', formatted: '' }; - if (!data?.wallets.create_onchain_address.address) { - return { address: '', formatted: '' }; - } - - const originalAddress = data.wallets.create_onchain_address.address; - - const formatted = originalAddress.match(/.{1,6}/g) || []; - - return { address: originalAddress, formatted: formatted.join(' - ') }; - }, [data, loading, error]); - - if (loading) { - return ( -
- -
- ); - } - - if (error || !addressInfo.address) { - return ( -
-

- Error getting onchain address. -

-
- ); - } - - return ( - - - New Address - - -
- -

- {addressInfo.formatted} -

-
-
- - - -
- ); -}; diff --git a/src/views/wallet/WalletInfo.tsx b/src/views/wallet/WalletInfo.tsx index fc730379..18a51dff 100644 --- a/src/views/wallet/WalletInfo.tsx +++ b/src/views/wallet/WalletInfo.tsx @@ -437,7 +437,7 @@ export const WalletInfo: FC<{