From 2197b531ee566d34b6e108c9cb07c8cbda2bd4b2 Mon Sep 17 00:00:00 2001 From: nick theile Date: Fri, 14 Apr 2023 15:16:14 -0500 Subject: [PATCH] Fix conversion bug and initial data (#456) * fix: initial data * fix: sats conversion bug --- .../ParsePOSPayment/Receive-Invoice.tsx | 35 +-- components/ParsePOSPayment/index.tsx | 14 +- lib/graphql/generated.ts | 227 +++++++++--------- lib/use-realtime-price.tsx | 38 ++- 4 files changed, 175 insertions(+), 139 deletions(-) diff --git a/components/ParsePOSPayment/Receive-Invoice.tsx b/components/ParsePOSPayment/Receive-Invoice.tsx index 10c2e454..e87eb16d 100644 --- a/components/ParsePOSPayment/Receive-Invoice.tsx +++ b/components/ParsePOSPayment/Receive-Invoice.tsx @@ -9,7 +9,6 @@ import Tooltip from "react-bootstrap/Tooltip" import { QRCode } from "react-qrcode-logo" import { useTimer } from "react-timer-hook" import { useScreenshot } from "use-react-screenshot" -import { AmountUnit } from "." import { URL_HOST_DOMAIN, USD_INVOICE_EXPIRE_INTERVAL } from "../../config/config" import useCreateInvoice from "../../hooks/use-Create-Invoice" @@ -20,6 +19,7 @@ import { ACTION_TYPE } from "../../pages/_reducer" import PaymentOutcome from "../PaymentOutcome" import { Share } from "../Share" import styles from "./parse-payment.module.css" +import { safeAmount } from "../../utils/utils" interface Props { recipientWalletCurrency?: string @@ -86,30 +86,19 @@ function ReceiveInvoice({ recipientWalletCurrency, walletId, state, dispatch }: ) const paymentAmount = React.useMemo(() => { - const finalReturnValue = - recipientWalletCurrency === "USD" ? Number(amount) * 100 : usdToSats(Number(amount)) - if (amount === "0.00" || isNaN(Number(amount))) { - if ((!unit || !amount || !sats) && recipientWalletCurrency === "USD") { - return (Number(state.currentAmount) * 100).toString() - } - if (sats && unit === AmountUnit.Sat) { - if (recipientWalletCurrency === "USD") { - return satsToUsd(Number(state.currentAmount)).toString() - } - return sats - } else if (Number(state.currentAmount) > 0) { - return usdToSats(Number(state.currentAmount)).toFixed(2) - } + if (!router.query.sats || typeof router.query.sats !== "string") { + alert("No sats amount provided") + return } - - if (sats && unit === AmountUnit.Sat) { - if (recipientWalletCurrency === "USD") { - return (Number(amount) * 100).toString() - } - return sats + let amt = safeAmount(router.query.sats) + if (recipientWalletCurrency === "USD") { + const usdAmount = satsToUsd(Number(amt)) + if (isNaN(usdAmount)) return + const cents = parseFloat(usdAmount.toFixed(2)) * 100 + amt = Number(cents.toFixed()) } - - return finalReturnValue.toFixed(2) + if (amt === null) return + return safeAmount(amt).toString() }, [ amount, unit, diff --git a/components/ParsePOSPayment/index.tsx b/components/ParsePOSPayment/index.tsx index 83502c25..136a2f7c 100644 --- a/components/ParsePOSPayment/index.tsx +++ b/components/ParsePOSPayment/index.tsx @@ -52,7 +52,7 @@ const defaultCurrencyMetadata: Currency = { function ParsePayment({ defaultWalletCurrency, walletId, dispatch, state }: Props) { const router = useRouter() - const { username, amount, sats, unit, memo } = router.query + const { username, amount, sats, unit, memo, currency } = router.query const { display } = parseDisplayCurrency(router.query) const { currencyToSats, satsToCurrency, hasLoaded } = useRealtimePrice(display) const { currencyList } = useDisplayCurrency() @@ -66,6 +66,14 @@ function ParsePayment({ defaultWalletCurrency, walletId, dispatch, state }: Prop const prevUnit = React.useRef(AmountUnit.Cent) + if (!currency) { + const queryString = window.location.search + const searchParams = new URLSearchParams(queryString) + searchParams.set("currency", defaultWalletCurrency ?? "BTC") + const newQueryString = searchParams.toString() + window.history.pushState(null, "", "?" + newQueryString) + } + // onload // set all query params on first load, even if they are not passed useEffect(() => { @@ -198,7 +206,7 @@ function ParsePayment({ defaultWalletCurrency, walletId, dispatch, state }: Prop } } // eslint-disable-next-line react-hooks/exhaustive-deps - React.useEffect(handleAmountChange, [currentAmount, hasLoaded]) + React.useEffect(handleAmountChange, [currentAmount, hasLoaded.current]) React.useEffect(() => { setCurrentAmount(state.currentAmount) @@ -296,7 +304,7 @@ function ParsePayment({ defaultWalletCurrency, walletId, dispatch, state }: Prop }`} > {unit === "CENT" ? "≈" : ""} {formatOperand(valueInSats.toString())} sat - {!hasLoaded && ( + {!hasLoaded.current && ( @@ -87,6 +88,12 @@ export type AccountTransactionsArgs = { walletIds?: InputMaybe>> } +export const AccountLevel = { + One: "ONE", + Two: "TWO", +} as const + +export type AccountLevel = typeof AccountLevel[keyof typeof AccountLevel] export type AccountLimit = { /** The rolling time interval in seconds that the limits would apply for. */ readonly interval?: Maybe @@ -206,6 +213,7 @@ export type ConsumerAccount = Account & { readonly defaultWalletId: Scalars["WalletId"] readonly displayCurrency: Scalars["DisplayCurrency"] readonly id: Scalars["ID"] + readonly level: AccountLevel readonly limits: AccountLimits /** List the quiz questions of the consumer account */ readonly quiz: ReadonlyArray @@ -592,6 +600,7 @@ export type Mutation = { readonly onChainPaymentSend: PaymentSendPayload readonly onChainPaymentSendAll: PaymentSendPayload readonly onChainUsdPaymentSend: PaymentSendPayload + readonly onChainUsdPaymentSendAsBtcDenominated: PaymentSendPayload readonly quizCompleted: QuizCompletedPayload /** @deprecated will be moved to AccountContact */ readonly userContactUpdateAlias: UserContactUpdateAliasPayload @@ -701,6 +710,10 @@ export type MutationOnChainUsdPaymentSendArgs = { input: OnChainUsdPaymentSendInput } +export type MutationOnChainUsdPaymentSendAsBtcDenominatedArgs = { + input: OnChainUsdPaymentSendAsBtcDenominatedInput +} + export type MutationQuizCompletedArgs = { input: QuizCompletedInput } @@ -794,6 +807,14 @@ export type OnChainUpdate = { readonly walletId: Scalars["WalletId"] } +export type OnChainUsdPaymentSendAsBtcDenominatedInput = { + readonly address: Scalars["OnChainAddress"] + readonly amount: Scalars["SatAmount"] + readonly memo?: InputMaybe + readonly targetConfirmations?: InputMaybe + readonly walletId: Scalars["WalletId"] +} + export type OnChainUsdPaymentSendInput = { readonly address: Scalars["OnChainAddress"] readonly amount: Scalars["CentAmount"] @@ -947,6 +968,7 @@ export type Query = { readonly mobileVersions?: Maybe>> readonly onChainTxFee: OnChainTxFee readonly onChainUsdTxFee: OnChainUsdTxFee + readonly onChainUsdTxFeeAsBtcDenominated: OnChainUsdTxFee /** @deprecated TODO: remove. we don't need a non authenticated version of this query. the users can only do the query while authenticated */ readonly quizQuestions?: Maybe>> /** Returns 1 Sat and 1 Usd Cent price for the given currency */ @@ -987,6 +1009,13 @@ export type QueryOnChainUsdTxFeeArgs = { walletId: Scalars["WalletId"] } +export type QueryOnChainUsdTxFeeAsBtcDenominatedArgs = { + address: Scalars["OnChainAddress"] + amount: Scalars["SatAmount"] + targetConfirmations?: InputMaybe + walletId: Scalars["WalletId"] +} + export type QueryRealtimePriceArgs = { currency?: InputMaybe } @@ -1050,16 +1079,6 @@ export type SatAmountPayload = { readonly errors: ReadonlyArray } -export type SelectedDisplayCurrency = { - readonly __typename: "SelectedDisplayCurrency" - readonly currency?: Maybe - readonly flag?: Maybe - readonly fractionDigits?: Maybe - readonly id?: Maybe - readonly name?: Maybe - readonly symbol?: Maybe -} - export type SettlementVia = | SettlementViaIntraLedger | SettlementViaLn @@ -1418,36 +1437,6 @@ export type CurrencyListQuery = { }> } -export type RealtimePriceQueryVariables = Exact<{ [key: string]: never }> - -export type RealtimePriceQuery = { - readonly __typename: "Query" - readonly me?: { - readonly __typename: "User" - readonly id: string - readonly defaultAccount: { - readonly __typename: "ConsumerAccount" - readonly id: string - readonly realtimePrice: { - readonly __typename: "RealtimePrice" - readonly denominatorCurrency: string - readonly id: string - readonly timestamp: number - readonly btcSatPrice: { - readonly __typename: "PriceOfOneSatInMinorUnit" - readonly base: number - readonly offset: number - } - readonly usdCentPrice: { - readonly __typename: "PriceOfOneUsdCentInMinorUnit" - readonly base: number - readonly offset: number - } - } - } - } | null -} - export type RealtimePriceWsSubscriptionVariables = Exact<{ currency: Scalars["DisplayCurrency"] }> @@ -1478,6 +1467,29 @@ export type RealtimePriceWsSubscription = { } } +export type RealtimePriceInitialQueryVariables = Exact<{ + currency: Scalars["DisplayCurrency"] +}> + +export type RealtimePriceInitialQuery = { + readonly __typename: "Query" + readonly realtimePrice: { + readonly __typename: "RealtimePrice" + readonly timestamp: number + readonly denominatorCurrency: string + readonly btcSatPrice: { + readonly __typename: "PriceOfOneSatInMinorUnit" + readonly base: number + readonly offset: number + } + readonly usdCentPrice: { + readonly __typename: "PriceOfOneUsdCentInMinorUnit" + readonly base: number + readonly offset: number + } + } +} + export type PriceSubscriptionVariables = Exact<{ amount: Scalars["SatAmount"] amountCurrencyUnit: ExchangeCurrencyUnit @@ -1675,74 +1687,6 @@ export type CurrencyListQueryResult = Apollo.QueryResult< CurrencyListQuery, CurrencyListQueryVariables > -export const RealtimePriceDocument = gql` - query realtimePrice { - me { - id - defaultAccount { - id - realtimePrice { - btcSatPrice { - base - offset - } - denominatorCurrency - id - timestamp - usdCentPrice { - base - offset - } - } - } - } - } -` - -/** - * __useRealtimePriceQuery__ - * - * To run a query within a React component, call `useRealtimePriceQuery` and pass it any options that fit your needs. - * When your component renders, `useRealtimePriceQuery` returns an object from Apollo Client that contains loading, error, and data properties - * you can use to render your UI. - * - * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; - * - * @example - * const { data, loading, error } = useRealtimePriceQuery({ - * variables: { - * }, - * }); - */ -export function useRealtimePriceQuery( - baseOptions?: Apollo.QueryHookOptions, -) { - const options = { ...defaultOptions, ...baseOptions } - return Apollo.useQuery( - RealtimePriceDocument, - options, - ) -} -export function useRealtimePriceLazyQuery( - baseOptions?: Apollo.LazyQueryHookOptions< - RealtimePriceQuery, - RealtimePriceQueryVariables - >, -) { - const options = { ...defaultOptions, ...baseOptions } - return Apollo.useLazyQuery( - RealtimePriceDocument, - options, - ) -} -export type RealtimePriceQueryHookResult = ReturnType -export type RealtimePriceLazyQueryHookResult = ReturnType< - typeof useRealtimePriceLazyQuery -> -export type RealtimePriceQueryResult = Apollo.QueryResult< - RealtimePriceQuery, - RealtimePriceQueryVariables -> export const RealtimePriceWsDocument = gql` subscription realtimePriceWs($currency: DisplayCurrency!) { realtimePrice(input: { currency: $currency }) { @@ -1798,6 +1742,73 @@ export type RealtimePriceWsSubscriptionHookResult = ReturnType< > export type RealtimePriceWsSubscriptionResult = Apollo.SubscriptionResult +export const RealtimePriceInitialDocument = gql` + query realtimePriceInitial($currency: DisplayCurrency!) { + realtimePrice(currency: $currency) { + timestamp + btcSatPrice { + base + offset + } + usdCentPrice { + base + offset + } + denominatorCurrency + } + } +` + +/** + * __useRealtimePriceInitialQuery__ + * + * To run a query within a React component, call `useRealtimePriceInitialQuery` and pass it any options that fit your needs. + * When your component renders, `useRealtimePriceInitialQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useRealtimePriceInitialQuery({ + * variables: { + * currency: // value for 'currency' + * }, + * }); + */ +export function useRealtimePriceInitialQuery( + baseOptions: Apollo.QueryHookOptions< + RealtimePriceInitialQuery, + RealtimePriceInitialQueryVariables + >, +) { + const options = { ...defaultOptions, ...baseOptions } + return Apollo.useQuery( + RealtimePriceInitialDocument, + options, + ) +} +export function useRealtimePriceInitialLazyQuery( + baseOptions?: Apollo.LazyQueryHookOptions< + RealtimePriceInitialQuery, + RealtimePriceInitialQueryVariables + >, +) { + const options = { ...defaultOptions, ...baseOptions } + return Apollo.useLazyQuery< + RealtimePriceInitialQuery, + RealtimePriceInitialQueryVariables + >(RealtimePriceInitialDocument, options) +} +export type RealtimePriceInitialQueryHookResult = ReturnType< + typeof useRealtimePriceInitialQuery +> +export type RealtimePriceInitialLazyQueryHookResult = ReturnType< + typeof useRealtimePriceInitialLazyQuery +> +export type RealtimePriceInitialQueryResult = Apollo.QueryResult< + RealtimePriceInitialQuery, + RealtimePriceInitialQueryVariables +> export const PriceDocument = gql` subscription price( $amount: SatAmount! diff --git a/lib/use-realtime-price.tsx b/lib/use-realtime-price.tsx index b47b9bea..3073a1c8 100644 --- a/lib/use-realtime-price.tsx +++ b/lib/use-realtime-price.tsx @@ -2,6 +2,7 @@ import { gql, SubscriptionResult } from "@apollo/client" import * as React from "react" import { RealtimePriceWsSubscription, + useRealtimePriceInitialQuery, useRealtimePriceWsSubscription, } from "../lib/graphql/generated" import { useDisplayCurrency } from "../lib/use-display-currency" @@ -28,6 +29,23 @@ gql` } ` +gql` + query realtimePriceInitial($currency: DisplayCurrency!) { + realtimePrice(currency: $currency) { + timestamp + btcSatPrice { + base + offset + } + usdCentPrice { + base + offset + } + denominatorCurrency + } + } +` + const useRealtimePrice = ( currency: string, onSubscriptionDataCallback?: ( @@ -45,12 +63,22 @@ const useRealtimePrice = ( }, }) + const { data: initialData } = useRealtimePriceInitialQuery({ + variables: { currency }, + onCompleted(initData) { + if (initData?.realtimePrice?.btcSatPrice) { + const { base, offset } = initData.realtimePrice.btcSatPrice + priceRef.current = base / 10 ** offset + } + }, + }) + React.useEffect(() => { - if (data && !hasLoaded.current) { - // Subscription data has loaded for the first time + if ((data || initialData) && !hasLoaded.current) { + // Subscription data or graphql data has loaded for the first time hasLoaded.current = true } - }, [data]) + }, [data, initialData]) const conversions = React.useMemo( () => ({ @@ -82,7 +110,7 @@ const useRealtimePrice = ( formattedCurrency, } }, - hasLoaded, + hasLoaded: hasLoaded, }), [priceRef, formatCurrency], ) @@ -106,7 +134,7 @@ const useRealtimePrice = ( formattedCurrency: "0", } }, - hasLoaded: false, + hasLoaded: hasLoaded, } }