diff --git a/.storybook/storybook.requires.js b/.storybook/storybook.requires.js index 5ed706010c..ad87fa055d 100644 --- a/.storybook/storybook.requires.js +++ b/.storybook/storybook.requires.js @@ -49,6 +49,7 @@ const getStories = () => { "./app/components/currency-keyboard/currency-keyboard.stories.tsx": require("../app/components/currency-keyboard/currency-keyboard.stories.tsx"), "./app/components/custom-modal/custom-modal.stories.tsx": require("../app/components/custom-modal/custom-modal.stories.tsx"), "./app/components/menu-select/menu-select.stories.tsx": require("../app/components/menu-select/menu-select.stories.tsx"), + "./app/components/button-group/button-group.stories.tsx": require("../app/components/button-group/button-group.stories.tsx"), "./app/components/modal-nfc/modal-nfc.stories.tsx": require("../app/components/modal-nfc/modal-nfc.stories.tsx"), "./app/components/set-default-account-modal/set-default-account-modal.stories.tsx": require("../app/components/set-default-account-modal/set-default-account-modal.stories.tsx"), "./app/components/set-lightning-address-modal/set-lightning-address-modal.stories.tsx": require("../app/components/set-lightning-address-modal/set-lightning-address-modal.stories.tsx"), @@ -79,7 +80,7 @@ const getStories = () => { "./app/screens/home-screen/home-screen.stories.tsx": require("../app/screens/home-screen/home-screen.stories.tsx"), "./app/screens/phone-auth-screen/phone-flow.stories.tsx": require("../app/screens/phone-auth-screen/phone-flow.stories.tsx"), "./app/screens/phone-auth-screen/phone-validation.stories.tsx": require("../app/screens/phone-auth-screen/phone-validation.stories.tsx"), - "./app/screens/receive-bitcoin-screen/receive-wrapper.stories.tsx": require("../app/screens/receive-bitcoin-screen/receive-wrapper.stories.tsx"), + "./app/screens/receive-bitcoin-screen/receive-screen.stories.tsx": require("../app/screens/receive-bitcoin-screen/receive-screen.stories.tsx"), "./app/screens/redeem-lnurl-withdrawal-screen/redeem-bitcoin-detail-screen.stories.tsx": require("../app/screens/redeem-lnurl-withdrawal-screen/redeem-bitcoin-detail-screen.stories.tsx"), "./app/screens/redeem-lnurl-withdrawal-screen/redeem-bitcoin-result-screen.stories.tsx": require("../app/screens/redeem-lnurl-withdrawal-screen/redeem-bitcoin-result-screen.stories.tsx"), "./app/screens/send-bitcoin-screen/confirm-destination-modal.stories.tsx": require("../app/screens/send-bitcoin-screen/confirm-destination-modal.stories.tsx"), diff --git a/__tests__/screens/receive.spec.tsx b/__tests__/screens/receive.spec.tsx index 4c9d72b130..73c50e0991 100644 --- a/__tests__/screens/receive.spec.tsx +++ b/__tests__/screens/receive.spec.tsx @@ -2,12 +2,12 @@ import React from "react" import { act, render } from "@testing-library/react-native" import { ContextForScreen } from "./helper" -import ReceiveWrapperScreen from "@app/screens/receive-bitcoin-screen/receive-wrapper" +import ReceiveScreen from "@app/screens/receive-bitcoin-screen/receive-wrapper" it("Receive", async () => { render( - + , ) await act(async () => {}) diff --git a/app/components/amount-input/amount-input-button.tsx b/app/components/amount-input/amount-input-button.tsx index d5b253ef12..c92979c907 100644 --- a/app/components/amount-input/amount-input-button.tsx +++ b/app/components/amount-input/amount-input-button.tsx @@ -12,6 +12,7 @@ export type AmountInputButtonProps = { disabled?: boolean secondaryValue?: string primaryTextTestProps?: string + showValuesIfDisabled?: boolean } & PressableProps export const AmountInputButton: React.FC = ({ @@ -22,6 +23,7 @@ export const AmountInputButton: React.FC = ({ disabled, secondaryValue, primaryTextTestProps, + showValuesIfDisabled = true, ...props }) => { const { @@ -46,6 +48,7 @@ export const AmountInputButton: React.FC = ({ case disabled: colorStyles = { backgroundColor: colors.grey5, + opacity: 0.5, } break default: @@ -65,13 +68,18 @@ export const AmountInputButton: React.FC = ({ return [colorStyles, sizeStyles] } + if (!showValuesIfDisabled) { + value = "" + secondaryValue = "" + } + const primaryText = value || placeholder || "" return ( canSetAmount?: boolean isSendingMax?: boolean + showValuesIfDisabled?: boolean } export const AmountInput: React.FC = ({ @@ -33,6 +34,7 @@ export const AmountInput: React.FC = ({ convertMoneyAmount, canSetAmount = true, isSendingMax = false, + showValuesIfDisabled = true, }) => { const [isSettingAmount, setIsSettingAmount] = React.useState(false) const { formatMoneyAmount, getSecondaryAmountIfCurrencyIsDifferent } = @@ -117,10 +119,13 @@ export const AmountInput: React.FC = ({ return ( ) diff --git a/app/components/button-group/button-group.stories.tsx b/app/components/button-group/button-group.stories.tsx new file mode 100644 index 0000000000..462b18b301 --- /dev/null +++ b/app/components/button-group/button-group.stories.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import { Story, UseCase } from "../../../.storybook/views" +import { ButtonGroup } from "." + +export default { + title: "ButtonGroup", + component: ButtonGroup, +} + +export const Default = () => { + return ( + + + {}} + buttons={[ + { id: "invoice", text: "Invoice", icon: "md-flash" }, + { id: "paycode", text: "Paycode", icon: "md-at" }, + { id: "onchain", text: "On-chain", icon: "logo-bitcoin" }, + ]} + /> + + + ) +} diff --git a/app/components/button-group/button-group.tsx b/app/components/button-group/button-group.tsx new file mode 100644 index 0000000000..41b92fdf6a --- /dev/null +++ b/app/components/button-group/button-group.tsx @@ -0,0 +1,90 @@ +import React from "react" +import { StyleProp, TouchableWithoutFeedback, View, ViewStyle } from "react-native" +import { Text, makeStyles } from "@rneui/themed" +import Icon from "react-native-vector-icons/Ionicons" + +type ButtonForButtonGroupProps = { + id: string + text: string + icon: string | React.ReactElement +} + +const ButtonForButtonGroup: React.FC< + ButtonForButtonGroupProps & { + selected: boolean + onPress: () => void + } +> = ({ text, icon, selected, onPress }) => { + const styles = useStyles(Boolean(selected)) + return ( + + + {text} + {typeof icon === "string" ? : icon} + + + ) +} + +export type ButtonGroupProps = { + selectedId: string + buttons: ButtonForButtonGroupProps[] + style?: StyleProp + disabled?: boolean + onPress: (id: string) => void +} + +export const ButtonGroup: React.FC = ({ + buttons, + selectedId, + onPress, + style, + disabled, +}) => { + const styles = useStyles() + const selectedButton = buttons.find(({ id }) => id === selectedId) + + return ( + + {!disabled && + buttons.map((props) => ( + { + if (selectedId !== props.id) { + onPress(props.id) + } + }} + selected={selectedId === props.id} + /> + ))} + {disabled && selectedButton && ( + {}} /> + )} + + ) +} + +const useStyles = makeStyles(({ colors }, selected: boolean) => ({ + button: { + flex: 1, + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + padding: 10, + paddingVertical: 10, + marginHorizontal: 3, + borderRadius: 5, + backgroundColor: selected ? colors.grey5 : colors.grey4, + }, + text: { + fontSize: 16, + color: selected ? colors.primary : colors.grey1, + }, + buttonGroup: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + }, +})) diff --git a/app/components/button-group/index.ts b/app/components/button-group/index.ts new file mode 100644 index 0000000000..eaf4512dcf --- /dev/null +++ b/app/components/button-group/index.ts @@ -0,0 +1 @@ +export * from "./button-group" diff --git a/app/components/note-input/index.ts b/app/components/note-input/index.ts new file mode 100644 index 0000000000..811fa740f1 --- /dev/null +++ b/app/components/note-input/index.ts @@ -0,0 +1 @@ +export * from "./note-input" diff --git a/app/components/note-input/note-input.tsx b/app/components/note-input/note-input.tsx new file mode 100644 index 0000000000..6e0ed17e59 --- /dev/null +++ b/app/components/note-input/note-input.tsx @@ -0,0 +1,85 @@ +import React from "react" + +import { makeStyles } from "@rneui/themed" +import { View, TextInput, StyleProp, ViewStyle } from "react-native" + +import { useI18nContext } from "@app/i18n/i18n-react" +import NoteIcon from "@app/assets/icons/note.svg" +import { useTheme } from "@rneui/themed" + +export type NoteInputProps = { + onBlur?: (() => void) | undefined + onChangeText?: ((text: string) => void) | undefined + value?: string | undefined + editable?: boolean | undefined + style?: StyleProp +} + +export const NoteInput: React.FC = ({ + onChangeText, + value, + editable, + onBlur, + style, +}) => { + const styles = useStyles(Boolean(editable)) + const { + theme: { colors }, + } = useTheme() + const { LL } = useI18nContext() + + return ( + + + + + + + + + ) +} + +const useStyles = makeStyles(({ colors }, editable: boolean) => ({ + fieldBackground: { + flexDirection: "row", + borderStyle: "solid", + overflow: "hidden", + backgroundColor: colors.grey5, + paddingHorizontal: 10, + borderRadius: 10, + alignItems: "center", + height: 60, + opacity: editable ? 1 : 0.5, + }, + fieldContainer: { + marginBottom: 12, + }, + noteContainer: { + flex: 1, + flexDirection: "row", + }, + noteIconContainer: { + justifyContent: "center", + alignItems: "flex-start", + }, + noteIcon: { + justifyContent: "center", + alignItems: "center", + }, + noteInput: { + flex: 1, + color: colors.black, + fontSize: 16, + }, +})) diff --git a/app/graphql/generated.gql b/app/graphql/generated.gql index 944073f538..1d620d16ca 100644 --- a/app/graphql/generated.gql +++ b/app/graphql/generated.gql @@ -869,42 +869,7 @@ query onChainUsdTxFeeAsBtcDenominated($walletId: WalletId!, $address: OnChainAdd } } -query quizSats { - quizQuestions { - id - earnAmount - __typename - } -} - -query realtimePrice { - me { - id - defaultAccount { - id - realtimePrice { - btcSatPrice { - base - offset - __typename - } - denominatorCurrency - id - timestamp - usdCentPrice { - base - offset - __typename - } - __typename - } - __typename - } - __typename - } -} - -query receiveBtc { +query paymentRequest { globals { network feesInformation { @@ -919,6 +884,7 @@ query receiveBtc { } me { id + username defaultAccount { id wallets { @@ -927,45 +893,42 @@ query receiveBtc { walletCurrency __typename } + defaultWalletId __typename } __typename } } -query receiveUsd { - globals { - network - __typename - } - me { +query quizSats { + quizQuestions { id - defaultAccount { - id - wallets { - id - balance - walletCurrency - __typename - } - __typename - } + earnAmount __typename } } -query receiveWrapperScreen { +query realtimePrice { me { id defaultAccount { id - wallets { + realtimePrice { + btcSatPrice { + base + offset + __typename + } + denominatorCurrency id - balance - walletCurrency + timestamp + usdCentPrice { + base + offset + __typename + } __typename } - defaultWalletId __typename } __typename diff --git a/app/graphql/generated.ts b/app/graphql/generated.ts index 7a69da242f..ac609e7a21 100644 --- a/app/graphql/generated.ts +++ b/app/graphql/generated.ts @@ -1906,20 +1906,10 @@ export type MyLnUpdatesSubscriptionVariables = Exact<{ [key: string]: never; }>; export type MyLnUpdatesSubscription = { readonly __typename: 'Subscription', readonly myUpdates: { readonly __typename: 'MyUpdatesPayload', readonly errors: ReadonlyArray<{ readonly __typename: 'GraphQLApplicationError', readonly message: string }>, readonly update?: { readonly __typename: 'IntraLedgerUpdate' } | { readonly __typename: 'LnUpdate', readonly paymentHash: string, readonly status: InvoicePaymentStatus } | { readonly __typename: 'OnChainUpdate' } | { readonly __typename: 'Price' } | { readonly __typename: 'RealtimePrice' } | null } }; -export type ReceiveBtcQueryVariables = Exact<{ [key: string]: never; }>; +export type PaymentRequestQueryVariables = Exact<{ [key: string]: never; }>; -export type ReceiveBtcQuery = { readonly __typename: 'Query', readonly globals?: { readonly __typename: 'Globals', readonly network: Network, readonly feesInformation: { readonly __typename: 'FeesInformation', readonly deposit: { readonly __typename: 'DepositFeesInformation', readonly minBankFee: string, readonly minBankFeeThreshold: string } } } | null, readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> } } | null }; - -export type ReceiveUsdQueryVariables = Exact<{ [key: string]: never; }>; - - -export type ReceiveUsdQuery = { readonly __typename: 'Query', readonly globals?: { readonly __typename: 'Globals', readonly network: Network } | null, readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> } } | null }; - -export type ReceiveWrapperScreenQueryVariables = Exact<{ [key: string]: never; }>; - - -export type ReceiveWrapperScreenQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly defaultWalletId: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> } } | null }; +export type PaymentRequestQuery = { readonly __typename: 'Query', readonly globals?: { readonly __typename: 'Globals', readonly network: Network, readonly feesInformation: { readonly __typename: 'FeesInformation', readonly deposit: { readonly __typename: 'DepositFeesInformation', readonly minBankFee: string, readonly minBankFeeThreshold: string } } } | null, readonly me?: { readonly __typename: 'User', readonly id: string, readonly username?: string | null, readonly defaultAccount: { readonly __typename: 'ConsumerAccount', readonly id: string, readonly defaultWalletId: string, readonly wallets: ReadonlyArray<{ readonly __typename: 'BTCWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency } | { readonly __typename: 'UsdWallet', readonly id: string, readonly balance: number, readonly walletCurrency: WalletCurrency }> } } | null }; export type LnNoAmountInvoiceCreateMutationVariables = Exact<{ input: LnNoAmountInvoiceCreateInput; @@ -3702,8 +3692,8 @@ export function useMyLnUpdatesSubscription(baseOptions?: Apollo.SubscriptionHook } export type MyLnUpdatesSubscriptionHookResult = ReturnType; export type MyLnUpdatesSubscriptionResult = Apollo.SubscriptionResult; -export const ReceiveBtcDocument = gql` - query receiveBtc { +export const PaymentRequestDocument = gql` + query paymentRequest { globals { network feesInformation { @@ -3715,93 +3705,7 @@ export const ReceiveBtcDocument = gql` } me { id - defaultAccount { - id - wallets { - id - balance - walletCurrency - } - } - } -} - `; - -/** - * __useReceiveBtcQuery__ - * - * To run a query within a React component, call `useReceiveBtcQuery` and pass it any options that fit your needs. - * When your component renders, `useReceiveBtcQuery` 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 } = useReceiveBtcQuery({ - * variables: { - * }, - * }); - */ -export function useReceiveBtcQuery(baseOptions?: Apollo.QueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(ReceiveBtcDocument, options); - } -export function useReceiveBtcLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(ReceiveBtcDocument, options); - } -export type ReceiveBtcQueryHookResult = ReturnType; -export type ReceiveBtcLazyQueryHookResult = ReturnType; -export type ReceiveBtcQueryResult = Apollo.QueryResult; -export const ReceiveUsdDocument = gql` - query receiveUsd { - globals { - network - } - me { - id - defaultAccount { - id - wallets { - id - balance - walletCurrency - } - } - } -} - `; - -/** - * __useReceiveUsdQuery__ - * - * To run a query within a React component, call `useReceiveUsdQuery` and pass it any options that fit your needs. - * When your component renders, `useReceiveUsdQuery` 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 } = useReceiveUsdQuery({ - * variables: { - * }, - * }); - */ -export function useReceiveUsdQuery(baseOptions?: Apollo.QueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(ReceiveUsdDocument, options); - } -export function useReceiveUsdLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(ReceiveUsdDocument, options); - } -export type ReceiveUsdQueryHookResult = ReturnType; -export type ReceiveUsdLazyQueryHookResult = ReturnType; -export type ReceiveUsdQueryResult = Apollo.QueryResult; -export const ReceiveWrapperScreenDocument = gql` - query receiveWrapperScreen { - me { - id + username defaultAccount { id wallets { @@ -3816,31 +3720,31 @@ export const ReceiveWrapperScreenDocument = gql` `; /** - * __useReceiveWrapperScreenQuery__ + * __usePaymentRequestQuery__ * - * To run a query within a React component, call `useReceiveWrapperScreenQuery` and pass it any options that fit your needs. - * When your component renders, `useReceiveWrapperScreenQuery` returns an object from Apollo Client that contains loading, error, and data properties + * To run a query within a React component, call `usePaymentRequestQuery` and pass it any options that fit your needs. + * When your component renders, `usePaymentRequestQuery` 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 } = useReceiveWrapperScreenQuery({ + * const { data, loading, error } = usePaymentRequestQuery({ * variables: { * }, * }); */ -export function useReceiveWrapperScreenQuery(baseOptions?: Apollo.QueryHookOptions) { +export function usePaymentRequestQuery(baseOptions?: Apollo.QueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(ReceiveWrapperScreenDocument, options); + return Apollo.useQuery(PaymentRequestDocument, options); } -export function useReceiveWrapperScreenLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { +export function usePaymentRequestLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(ReceiveWrapperScreenDocument, options); + return Apollo.useLazyQuery(PaymentRequestDocument, options); } -export type ReceiveWrapperScreenQueryHookResult = ReturnType; -export type ReceiveWrapperScreenLazyQueryHookResult = ReturnType; -export type ReceiveWrapperScreenQueryResult = Apollo.QueryResult; +export type PaymentRequestQueryHookResult = ReturnType; +export type PaymentRequestLazyQueryHookResult = ReturnType; +export type PaymentRequestQueryResult = Apollo.QueryResult; export const LnNoAmountInvoiceCreateDocument = gql` mutation lnNoAmountInvoiceCreate($input: LnNoAmountInvoiceCreateInput!) { lnNoAmountInvoiceCreate(input: $input) { diff --git a/app/graphql/mocks.ts b/app/graphql/mocks.ts index f596276cc5..9c4da59a54 100644 --- a/app/graphql/mocks.ts +++ b/app/graphql/mocks.ts @@ -11,7 +11,7 @@ import { RealtimePriceDocument, ReceiveBtcDocument, ReceiveUsdDocument, - ReceiveWrapperScreenDocument, + ReceiveScreenDocument, SendBitcoinConfirmationScreenDocument, SendBitcoinDestinationDocument, SendBitcoinDetailsScreenDocument, @@ -335,7 +335,7 @@ const mocks = [ }, { request: { - query: ReceiveWrapperScreenDocument, + query: ReceiveScreenDocument, }, result: { data: { diff --git a/app/i18n/en/index.ts b/app/i18n/en/index.ts index 65f7c24e47..348ec396b5 100644 --- a/app/i18n/en/index.ts +++ b/app/i18n/en/index.ts @@ -7,7 +7,8 @@ const en: BaseTranslation = { title: "Receive payment by using:", buttonTitle: "Set your address", yourAddress: "Your {bankName: string} address", - notAbleToChange: "You won't be able to change your {bankName: string} address after it's set.", + notAbleToChange: + "You won't be able to change your {bankName: string} address after it's set.", addressNotAvailable: "This {bankName: string} address is already taken.", somethingWentWrong: "Something went wrong. Please try again later.", merchantTitle: "For merchants", @@ -20,15 +21,19 @@ const en: BaseTranslation = { howToUseYourAddress: "How to use a Lightning address", howToUseYourPaycode: "How to use your Paycode", howToUseYourCashRegister: "How to use your Cash Register", - howToUseYourAddressExplainer: "Share with someone that has a compatible wallet, such as:", - howToUseYourPaycodeExplainer: "You can print your Paycode (technically, this is an lnurl-pay address) and display it in your business to receive payments. Individuals can pay you by scanning it with a Lightning-enabled wallet.\n\nHowever, be aware that some wallets can’t scan a Paycode such as:", - howToUseYourCashRegisterExplainer: "Allow people to collect payments via the Cash Register link, without accessing your wallet.\n\nThey can create invoices and payments will be sent directly to your {bankName: string} Wallet.", + howToUseYourAddressExplainer: + "Share with someone that has a compatible wallet, such as:", + howToUseYourPaycodeExplainer: + "You can print your Paycode (technically, this is an lnurl-pay address) and display it in your business to receive payments. Individuals can pay you by scanning it with a Lightning-enabled wallet.\n\nHowever, be aware that some wallets can’t scan a Paycode such as:", + howToUseYourCashRegisterExplainer: + "Allow people to collect payments via the Cash Register link, without accessing your wallet.\n\nThey can create invoices and payments will be sent directly to your {bankName: string} Wallet.", }, SetAccountModal: { title: "Set default account", - description: "This account will be initially selected for sending and receiving payments. It can be changed at any time.", + description: + "This account will be initially selected for sending and receiving payments. It can be changed at any time.", stablesatsTag: "Choose this to maintain a stable USD value.", - bitcoinTag: "Choose this to be on a Bitcoin standard." + bitcoinTag: "Choose this to be on a Bitcoin standard.", }, AuthenticationScreen: { authenticationDescription: "Authenticate to continue", @@ -75,7 +80,8 @@ const en: BaseTranslation = { "Nope. At least not one that we know of!", ], question: "So what exactly is Bitcoin?", - text: "Bitcoin is digital money. \n\nIt can be transferred instantly and securely between any two people in the world — without the need for a bank or any other financial company in the middle.", + text: + "Bitcoin is digital money. \n\nIt can be transferred instantly and securely between any two people in the world — without the need for a bank or any other financial company in the middle.", title: "So what exactly is Bitcoin?", }, sat: { @@ -90,7 +96,8 @@ const en: BaseTranslation = { "Ummm.... not quite!", ], question: 'I just earned a “Sat". What is that?', - text: "One “Sat” is the smallest unit of a bitcoin. \n\nWe all know that one US Dollar can be divided into 100 cents. Similarly, one Bitcoin can be divided into 100,000,000 sats. \n\nIn fact, you do not need to own one whole bitcoin in order to use it. You can use bitcoin whether you have 20 sats, 3000 sats — or 100,000,000 sats (which you now know is equal to one bitcoin).", + text: + "One “Sat” is the smallest unit of a bitcoin. \n\nWe all know that one US Dollar can be divided into 100 cents. Similarly, one Bitcoin can be divided into 100,000,000 sats. \n\nIn fact, you do not need to own one whole bitcoin in order to use it. You can use bitcoin whether you have 20 sats, 3000 sats — or 100,000,000 sats (which you now know is equal to one bitcoin).", title: 'I just earned a “Sat". What is that?', }, whereBitcoinExist: { @@ -101,7 +108,8 @@ const en: BaseTranslation = { "Wrong. Please try again.", ], question: "Where do the bitcoins exist?", - text: "Bitcoin is a new form of money. It can be used by anyone, anytime -- anywhere in the world. \n\nIt is not tied to a specific government or region (like US Dollars). There are also no paper bills, metal coins or plastic cards. \n\nEverything is 100% digital. Bitcoin is a network of computers running on the internet. \n\nYour bitcoin is easily managed with software on your smartphone or computer!", + text: + "Bitcoin is a new form of money. It can be used by anyone, anytime -- anywhere in the world. \n\nIt is not tied to a specific government or region (like US Dollars). There are also no paper bills, metal coins or plastic cards. \n\nEverything is 100% digital. Bitcoin is a network of computers running on the internet. \n\nYour bitcoin is easily managed with software on your smartphone or computer!", title: "Where do the bitcoins exist?", }, whoControlsBitcoin: { @@ -116,7 +124,8 @@ const en: BaseTranslation = { "Wrong. There is no company nor government that controls Bitcoin.", ], question: "Who controls Bitcoin?", - text: "Bitcoin is not controlled by any person, company or government. \n\nIt is run by the community of users -- people and companies all around the world -- voluntarily running bitcoin software on their computers and smartphones.", + text: + "Bitcoin is not controlled by any person, company or government. \n\nIt is run by the community of users -- people and companies all around the world -- voluntarily running bitcoin software on their computers and smartphones.", title: "Who controls Bitcoin?", }, copyBitcoin: { @@ -132,7 +141,8 @@ const en: BaseTranslation = { ], question: "If Bitcoin is digital money, can’t someone just copy it — and create free money?", - text: "The value of a bitcoin can never be copied. This is the very reason why Bitcoin is such a powerful new invention!!\n\nMost digital files — such as an iPhone photo, an MP3 song, or a Microsoft Word document — can easily be duplicated and shared. \n\nThe Bitcoin software uniquely prevents the duplication — or “double spending” — of digital money. We will share exactly how this works later on!", + text: + "The value of a bitcoin can never be copied. This is the very reason why Bitcoin is such a powerful new invention!!\n\nMost digital files — such as an iPhone photo, an MP3 song, or a Microsoft Word document — can easily be duplicated and shared. \n\nThe Bitcoin software uniquely prevents the duplication — or “double spending” — of digital money. We will share exactly how this works later on!", title: "If Bitcoin is digital money, can’t someone just copy it — and create free money?", }, @@ -153,7 +163,8 @@ const en: BaseTranslation = { "Nope. In the past you could exchange US dollars for gold. But this is no longer the case.", ], question: "Why does money have value?", - text: "Money requires people to trust. \n\nPeople trust the paper dollar bills in their pocket. They trust the digits in their online bank account. They trust the balance on a store gift card will be redeemable. \n\nHaving money allows people to easy trade it immediately for a good, or a service.", + text: + "Money requires people to trust. \n\nPeople trust the paper dollar bills in their pocket. They trust the digits in their online bank account. They trust the balance on a store gift card will be redeemable. \n\nHaving money allows people to easy trade it immediately for a good, or a service.", title: "Money is a social agreement.", }, coincidenceOfWants: { @@ -168,7 +179,8 @@ const en: BaseTranslation = { "Not quite. We call that a solar eclipse 🌚", ], question: "Which coincidence does money solve?", - text: "Centuries ago, before people had money, they would barter -- or haggle over how to trade one unique item, in exchange for another item or service. \n\nLet’s say you wanted to have a meal at the local restaurant, and offered the owner a broom. The owner might say “no” -- but I will accept three hats instead, if you happen to have them. \n\nYou can imagine how difficult and inefficient a “barter economy” would be ! \n\nBy contrast, with money, you can simply present a $20 bill. And you know that the restaurant owner will readily accept it.", + text: + "Centuries ago, before people had money, they would barter -- or haggle over how to trade one unique item, in exchange for another item or service. \n\nLet’s say you wanted to have a meal at the local restaurant, and offered the owner a broom. The owner might say “no” -- but I will accept three hats instead, if you happen to have them. \n\nYou can imagine how difficult and inefficient a “barter economy” would be ! \n\nBy contrast, with money, you can simply present a $20 bill. And you know that the restaurant owner will readily accept it.", title: "Money solves the “coincidence of wants”... What is that??", }, moneyEvolution: { @@ -184,7 +196,8 @@ const en: BaseTranslation = { ], question: "What are some items that have been historically used as a unit of money?", - text: "Thousands of years ago, society in Micronesia used very large and scarce stones as a form of agreed currency. \n\nStarting in the 1500’s, rare Cowrie shells (found in the ocean) became commonly used in many nations as a form of money.\n\nAnd for millennia, gold has been used as a form of money for countries around the world -- including the United States (until 1971).", + text: + "Thousands of years ago, society in Micronesia used very large and scarce stones as a form of agreed currency. \n\nStarting in the 1500’s, rare Cowrie shells (found in the ocean) became commonly used in many nations as a form of money.\n\nAnd for millennia, gold has been used as a form of money for countries around the world -- including the United States (until 1971).", title: "Money has evolved, since almost the beginning of time.", }, whyStonesShellGold: { @@ -199,7 +212,8 @@ const en: BaseTranslation = { "Not quite. Although these items were surely portable, that alone was not the reason to be used as money.", ], question: "Why were stones, seashells and gold used as units of money?", - text: "Well, these items all had some -- but not all -- of the characteristics of good money. \n\nSo what characteristics make for “good” money? \nScarce: not abundant, nor easy to reproduce or copy \nAccepted: relatively easy for people to verify its authenticity \nDurable: easy to maintain, and does not perish or fall apart\nUniform: readily interchangeable with another item of the same form\nPortable: easy to transport\nDivisible: can be split and shared in smaller pieces", + text: + "Well, these items all had some -- but not all -- of the characteristics of good money. \n\nSo what characteristics make for “good” money? \nScarce: not abundant, nor easy to reproduce or copy \nAccepted: relatively easy for people to verify its authenticity \nDurable: easy to maintain, and does not perish or fall apart\nUniform: readily interchangeable with another item of the same form\nPortable: easy to transport\nDivisible: can be split and shared in smaller pieces", title: "Why were stones, shells and gold commonly used as money in the past?", }, moneyIsImportant: { @@ -214,7 +228,8 @@ const en: BaseTranslation = { "Not quite. Although some people may believe such, this answer does not address the primary purpose of money.", ], question: "What is the primary reason money is important?", - text: "Everybody knows that money matters.\n\nMost people exchange their time and energy -- in the form of work -- to obtain money. People do so, to be able to buy goods and services today -- and in the future.", + text: + "Everybody knows that money matters.\n\nMost people exchange their time and energy -- in the form of work -- to obtain money. People do so, to be able to buy goods and services today -- and in the future.", title: "Money is important to individuals", }, moneyImportantGovernement: { @@ -229,7 +244,8 @@ const en: BaseTranslation = { "No. Whilst some people do create fake dollar bills, it is definitely not legal!", ], question: "Who can legally print US Dollars, anytime they wish?", - text: "Modern-day economies are organized by nation-states: USA, Japan, Switzerland, Brazil, Norway, China, etc. \n\nAccordingly, in most every nation, the government holds the power to issue and control money. \n\nIn the United States, the Central Bank (known as the Federal Reserve, or “Fed”) can print, or create, more US Dollars at any time it wants. \n\nThe “Fed” does not need permission from the President, nor Congress, and certainly not from US citizens. \n\nImagine if you had the ability to print US Dollars anytime you wanted to -- what would you do??", + text: + "Modern-day economies are organized by nation-states: USA, Japan, Switzerland, Brazil, Norway, China, etc. \n\nAccordingly, in most every nation, the government holds the power to issue and control money. \n\nIn the United States, the Central Bank (known as the Federal Reserve, or “Fed”) can print, or create, more US Dollars at any time it wants. \n\nThe “Fed” does not need permission from the President, nor Congress, and certainly not from US citizens. \n\nImagine if you had the ability to print US Dollars anytime you wanted to -- what would you do??", title: "Money is also important to governments", }, }, @@ -249,7 +265,8 @@ const en: BaseTranslation = { "Nope. Try again!", ], question: "Who creates fiat money, such as US Dollars or Swiss Francs?", - text: "All national currencies in circulation today are called “fiat” money. This includes US Dollars, Japanese Yen, Swiss Francs, and so forth. \n\nThe word “fiat” is latin for “by decree” -- which means “by official order”. \n\nThis means that all fiat money -- including the US Dollar -- is simply created by the order of the national government.", + text: + "All national currencies in circulation today are called “fiat” money. This includes US Dollars, Japanese Yen, Swiss Francs, and so forth. \n\nThe word “fiat” is latin for “by decree” -- which means “by official order”. \n\nThis means that all fiat money -- including the US Dollar -- is simply created by the order of the national government.", title: "Fiat Currency: What is that?", }, whyCareAboutFiatMoney: { @@ -264,7 +281,8 @@ const en: BaseTranslation = { "Wrong. Please try again.", ], question: "Why should I care about the government controlling fiat money?", - text: "As shared in a prior quiz, the US Central Bank is the Federal Reserve, or the “Fed”.\n\nThe Fed can print more dollars at any time -- and does not need permission from the President, nor Congress, and certainly not from US citizens. \n\nHaving control of money can be very tempting for authorities to abuse -- and often time leads to massive inflation, arbitrary confiscation and corruption. \n\nIn fact, Alan Greenspan, the famous former chairman of The Fed, famously said the US “can pay any debt that it has, because we can always print more to do that”.", + text: + "As shared in a prior quiz, the US Central Bank is the Federal Reserve, or the “Fed”.\n\nThe Fed can print more dollars at any time -- and does not need permission from the President, nor Congress, and certainly not from US citizens. \n\nHaving control of money can be very tempting for authorities to abuse -- and often time leads to massive inflation, arbitrary confiscation and corruption. \n\nIn fact, Alan Greenspan, the famous former chairman of The Fed, famously said the US “can pay any debt that it has, because we can always print more to do that”.", title: "I trust my government. \nWhy should I care about fiat money?", }, GovernementCanPrintMoney: { @@ -279,7 +297,8 @@ const en: BaseTranslation = { "Incorrect. Although the government may issue new looks for bills, this has nothing to do with increasing the money supply.", ], question: "What does it mean when the government prints money?", - text: "Well, everybody should care! \n\nThe practice of government printing money -- or increasing the supply of dollars -- leads to inflation.\n\nInflation is an increase in the price of goods and services. In other words, the price for something in the future will be more expensive than today.\n\nSo what does inflation mean for citizens? \n\nIn the United Kingdom, the Pound Sterling has lost 99.5% of its value since being introduced over 300 years ago. \n\nIn the United States, the dollar has lost 97% of its value since the end of WWI, about 100 years ago. \n\nThis means a steak that cost $0.30 in 1920... was $3 in 1990… and about $15 today, in the year 2020!", + text: + "Well, everybody should care! \n\nThe practice of government printing money -- or increasing the supply of dollars -- leads to inflation.\n\nInflation is an increase in the price of goods and services. In other words, the price for something in the future will be more expensive than today.\n\nSo what does inflation mean for citizens? \n\nIn the United Kingdom, the Pound Sterling has lost 99.5% of its value since being introduced over 300 years ago. \n\nIn the United States, the dollar has lost 97% of its value since the end of WWI, about 100 years ago. \n\nThis means a steak that cost $0.30 in 1920... was $3 in 1990… and about $15 today, in the year 2020!", title: "Who should care that the government can print unlimited money?", }, FiatLosesValueOverTime: { @@ -294,7 +313,8 @@ const en: BaseTranslation = { "Not quite. Although the design of papers bills may change, this has nothing to do with their value.", ], question: "What happens to the value of all fiat money over time?", - text: "That is correct. \n\nIn the history of the world, there have been 775 fiat currencies created. Most no longer exist, and the average life for any fiat money is only 27 years.\n\nThe British Pound is the oldest fiat currency. It has lost more than 99% of its value since 1694. \n\nThere is no precedent for any fiat money maintaining its value over time. This is inflation. \nIt is effectively a form of theft of your own hard earned money !", + text: + "That is correct. \n\nIn the history of the world, there have been 775 fiat currencies created. Most no longer exist, and the average life for any fiat money is only 27 years.\n\nThe British Pound is the oldest fiat currency. It has lost more than 99% of its value since 1694. \n\nThere is no precedent for any fiat money maintaining its value over time. This is inflation. \nIt is effectively a form of theft of your own hard earned money !", title: "Does this mean that all fiat money loses value over time?", }, OtherIssues: { @@ -309,7 +329,8 @@ const en: BaseTranslation = { "While some may believe this to be so, it is not the answer we are looking for here.", ], question: "What are some other issues that exist with fiat money?", - text: "Yes, there are many other issues that exist with modern fiat money. \n\nFirst, it can be extremely difficult to move money around the world. Often, governments will outright restrict the movement -- and sometimes even confiscate money -- without a valid reason or explanation. And even when you can send money, high transaction fees make it very expensive.\n\nSecond, even in the US, there has been a complete loss of privacy, as the majority of commerce takes places with debit and credit cards, as well as online with other systems such as PayPal and Apple Pay.\n\nEver notice how an ad appears in your social media or Gmail just moments after searching for a certain product or service? This is known as “surveillance capitalism”, and is based on companies selling your personal financial data.", + text: + "Yes, there are many other issues that exist with modern fiat money. \n\nFirst, it can be extremely difficult to move money around the world. Often, governments will outright restrict the movement -- and sometimes even confiscate money -- without a valid reason or explanation. And even when you can send money, high transaction fees make it very expensive.\n\nSecond, even in the US, there has been a complete loss of privacy, as the majority of commerce takes places with debit and credit cards, as well as online with other systems such as PayPal and Apple Pay.\n\nEver notice how an ad appears in your social media or Gmail just moments after searching for a certain product or service? This is known as “surveillance capitalism”, and is based on companies selling your personal financial data.", title: "OK, fiat money loses value over time. Are there other issues?", }, }, @@ -329,7 +350,8 @@ const en: BaseTranslation = { "Incorrect. One of the key attributes of bitcoin is that the supply is limited forever.", ], question: "Is the supply of bitcoin limited forever?", - text: "Governments can print fiat money in unlimited quantities. \n\nBy way of contrast, the supply of Bitcoin is fixed — and can never exceed 21 million coins. \n\nA continually increasing supply of fiat money creates inflation. This means that the money you hold today is less valuable in the future. \n\nOne simple example: \nA loaf of bread that cost about 8 cents in 1920. In the year 1990 one loaf cost about $1.00, and today the price is closer to $2.50 ! \n\nThe limited supply of bitcoin has the opposite effect, one of deflation. \n\nThis means that the bitcoin you hold today is designed to be more valuable in the future — because it is scarce.", + text: + "Governments can print fiat money in unlimited quantities. \n\nBy way of contrast, the supply of Bitcoin is fixed — and can never exceed 21 million coins. \n\nA continually increasing supply of fiat money creates inflation. This means that the money you hold today is less valuable in the future. \n\nOne simple example: \nA loaf of bread that cost about 8 cents in 1920. In the year 1990 one loaf cost about $1.00, and today the price is closer to $2.50 ! \n\nThe limited supply of bitcoin has the opposite effect, one of deflation. \n\nThis means that the bitcoin you hold today is designed to be more valuable in the future — because it is scarce.", title: "Special Characteristic #1:\nLimited Supply", }, Decentralized: { @@ -344,7 +366,8 @@ const en: BaseTranslation = { "Incorrect. You already know this is not true!", ], question: "Is bitcoin centralized?", - text: "Fiat money is controlled by banks and governments — which is why people refer to it as a “centralized” currency.\n\nBitcoin is not controlled by any person, government or company — which makes it “decentralized” \n\nNot having banks involved means that nobody can deny you access to bitcoin — because of race, gender, income, credit history, geographical location — or any other factor. \n\nAnybody — anywhere in the world — can access and use Bitcoin anytime you want. All you need is a computer or smartphone, and an internet connection!", + text: + "Fiat money is controlled by banks and governments — which is why people refer to it as a “centralized” currency.\n\nBitcoin is not controlled by any person, government or company — which makes it “decentralized” \n\nNot having banks involved means that nobody can deny you access to bitcoin — because of race, gender, income, credit history, geographical location — or any other factor. \n\nAnybody — anywhere in the world — can access and use Bitcoin anytime you want. All you need is a computer or smartphone, and an internet connection!", title: "Special Characteristic #2: Decentralized", }, NoCounterfeitMoney: { @@ -359,7 +382,8 @@ const en: BaseTranslation = { "Wrong. Although the government can print unlimited dollars, it can not print bitcoin.", ], question: "Can people counterfeit Bitcoin?", - text: "Paper money, checks and credit card transactions can all be counterfeit, or faked. \n\nThe unique software that runs the Bitcoin network eliminates the possibility of duplicating money for counterfeit purposes. \n\nNew bitcoin can only be issued if there is agreement amongst the participants in the network. People who are voluntarily running bitcoin software on their own computers and smartphones.\n\nThis ensures that it is impossible to counterfeit, or create fake bitcoins.", + text: + "Paper money, checks and credit card transactions can all be counterfeit, or faked. \n\nThe unique software that runs the Bitcoin network eliminates the possibility of duplicating money for counterfeit purposes. \n\nNew bitcoin can only be issued if there is agreement amongst the participants in the network. People who are voluntarily running bitcoin software on their own computers and smartphones.\n\nThis ensures that it is impossible to counterfeit, or create fake bitcoins.", title: "Special Characteristic #3: \nNo Counterfeit Money", }, HighlyDivisible: { @@ -374,7 +398,8 @@ const en: BaseTranslation = { "Incorrect. Although the smallest unit of US currency is one penny, a bitcoin is divisible by much more than 100x.", ], question: "What is the smallest amount of Bitcoin one can own, or use?", - text: 'Old-fashioned fiat money can only be spent in amounts as small as one penny — or two decimal places for one US Dollar ($0.01).\n\nOn the other hand, Bitcoin can be divided 100,000,000 times over. This means that you could spend as little as ₿0.00000001. You will note the "₿" symbol, which is the Bitcoin equivalent of "$". Sometimes you will also see the use of BTC, instead of ₿.\n\nBy way of contrast, Bitcoin can handle very small payments — even those less than one US penny!', + text: + 'Old-fashioned fiat money can only be spent in amounts as small as one penny — or two decimal places for one US Dollar ($0.01).\n\nOn the other hand, Bitcoin can be divided 100,000,000 times over. This means that you could spend as little as ₿0.00000001. You will note the "₿" symbol, which is the Bitcoin equivalent of "$". Sometimes you will also see the use of BTC, instead of ₿.\n\nBy way of contrast, Bitcoin can handle very small payments — even those less than one US penny!', title: "Special Characteristic #4: \nHighly Divisible", }, securePartOne: { @@ -389,7 +414,8 @@ const en: BaseTranslation = { "Icorrect. Although bitcoin is indeed “open source” software — or available to the public for free — is still extremely secure.", ], question: "Is the Bitcoin network secure?", - text: "The bitcoin network is worth well over $100 billion today. Accordingly, the network must be very secure — so that money is never stolen. \n\nBitcoin is known as the world’s first cryptocurrency. \n\nThe “crypto” part of the name comes from cryptography. Simply put, cryptography protects information through very complex math functions. \n\nMost people do not realize — but Bitcoin is actually the most secure computer network in the world ! \n\n(you may have heard about bitcoin “hacks” — which we will debunk in the next quiz)", + text: + "The bitcoin network is worth well over $100 billion today. Accordingly, the network must be very secure — so that money is never stolen. \n\nBitcoin is known as the world’s first cryptocurrency. \n\nThe “crypto” part of the name comes from cryptography. Simply put, cryptography protects information through very complex math functions. \n\nMost people do not realize — but Bitcoin is actually the most secure computer network in the world ! \n\n(you may have heard about bitcoin “hacks” — which we will debunk in the next quiz)", title: "Special Characteristic #5: \nSecure -- Part I", }, securePartTwo: { @@ -404,10 +430,11 @@ const en: BaseTranslation = { "No silly, you know that is not the correct answer.", ], question: "Has Bitcoin ever been hacked?", - text: "To be direct: the bitcoin network itself has never been hacked. Never once.\n\nThen what exactly has been hacked? \n\nCertain digital wallets that did not have proper security in place. \n\nJust like a physical wallet holds fiat currency (in the form of paper bills), digital wallets hold some amount of bitcoin. \n\nIn the physical world, criminals rob banks — and walk away with US Dollars. The fact that someone robbed a bank does not have any relationship as to whether the US Dollar is stable or reliable money. \n\nSimilarly, some computer hackers have stolen bitcoin from insecure digital wallets — the online equivalent of a bank robbery. \n\nHowever, it is important to know that the bitcoin network has never been hacked or compromised !", + text: + "To be direct: the bitcoin network itself has never been hacked. Never once.\n\nThen what exactly has been hacked? \n\nCertain digital wallets that did not have proper security in place. \n\nJust like a physical wallet holds fiat currency (in the form of paper bills), digital wallets hold some amount of bitcoin. \n\nIn the physical world, criminals rob banks — and walk away with US Dollars. The fact that someone robbed a bank does not have any relationship as to whether the US Dollar is stable or reliable money. \n\nSimilarly, some computer hackers have stolen bitcoin from insecure digital wallets — the online equivalent of a bank robbery. \n\nHowever, it is important to know that the bitcoin network has never been hacked or compromised !", title: "Special Characteristic #5: \nSecure -- Part II", }, - } + }, }, }, finishText: "That's all for now, we'll let you know when there's more to unearth", @@ -436,13 +463,14 @@ const en: BaseTranslation = { startWithTrialAccount: "Start with trial account", registerPhoneAccount: "Register phone account", trialAccountCreationFailed: "Trial account creation failed", - trialAccountCreationFailedMessage: "Unfortunately, we were unable to create your trial account. Try again later or create an account with a phone number.", + trialAccountCreationFailedMessage: + "Unfortunately, we were unable to create your trial account. Try again later or create an account with a phone number.", trialAccountHasLimits: "Trial account has limits", trialAccountLimits: { noBackup: "No backup option", sendingLimit: "Reduced daily sending limit", noOnchain: "No receiving bitcoin onchain", - } + }, }, MapScreen: { locationPermissionMessage: @@ -488,7 +516,7 @@ const en: BaseTranslation = { PrimaryScreen: { title: "Home", }, - ReceiveWrapperScreen: { + ReceiveScreen: { activateNotifications: "Do you want to activate notifications to be notified when the payment has arrived?", copyClipboard: "Invoice has been copied in the clipboard", @@ -499,8 +527,8 @@ const en: BaseTranslation = { title: "Receive Bitcoin", usdTitle: "Receive USD", error: "Failed to generate invoice. Please contact support if this problem persists.", - copyInvoice: "Copy Invoice", - shareInvoice: "Share Invoice", + copyInvoice: "Copy", + shareInvoice: "Share", addAmount: "Request Specific Amount", expired: "The invoice has expired", expiresIn: "Expires in", @@ -514,14 +542,41 @@ const en: BaseTranslation = { useALightningInvoice: "Use a Lightning Invoice", setANote: "Set a Note", invoiceAmount: "Invoice Amount", - fees: "{minBankFee: string} sats fees for onchain payment below {minBankFeeThreshold: string} sats" + fees: + "{minBankFee: string} sats fees for onchain payment below {minBankFeeThreshold: string} sats", + invoice: "Invoice", + paycode: "Paycode", + onchain: "On-chain", + bitcoin: "Bitcoin", + stablesats: "Stablesats", + regenerateInvoiceButtonTitle: "Regenerate Invoice", + setUsernameButtonTitle: "Set Username", + invoiceHasExpired: "Invoice has expired", + setUsernameToAcceptViaPaycode: + "Set your username to accept via Paycode QR (LNURL) and Lightning Address", + singleUse: "Single Use", + invoiceExpired: "Expired Invoice", + invoiceValidity: { + validFor1Day: "Valid for 1 day", + validForNext: "Valid for next {duration: string}", + validBefore: "Valid before {time: string}", + expiresIn: "Expires in {duration: string}", + expiresNow: "Expires now", + }, + invoiceHasBeenPaid: "Invoice has been paid", + yourBitcoinOnChainAddress: "Your Bitcoin Onchain Address", + receiveViaInvoice: "Receive via Invoice", + receiveViaPaycode: "Receive via Paycode", + receiveViaOnchain: "Receive via Onchain", }, RedeemBitcoinScreen: { title: "Redeem Bitcoin", usdTitle: "Redeem for USD", error: "Failed to generate invoice. Please contact support if this problem persists.", - redeemingError: "Failed to redeem Bitcoin. Please contact support if this problem persists.", - submissionError: "Failed to submit withdrawal request. Please contact support if this problem persists.", + redeemingError: + "Failed to redeem Bitcoin. Please contact support if this problem persists.", + submissionError: + "Failed to submit withdrawal request. Please contact support if this problem persists.", minMaxRange: "Min: {minimumAmount: string}, Max: {maximumAmount: string}", redeemBitcoin: "Redeem Bitcoin", amountToRedeemFrom: "Amount to redeem from {domain: string}", @@ -530,8 +585,7 @@ const en: BaseTranslation = { ScanningQRCodeScreen: { invalidContent: "We found:\n\n{found: string}\n\nThis is not a valid Bitcoin address or Lightning invoice", - expiredContent: - "We found:\n\n{found: string}\n\nThis invoice has expired", + expiredContent: "We found:\n\n{found: string}\n\nThis invoice has expired", invalidTitle: "Invalid QR Code", noQrCode: "We could not find a QR code in the image", title: "Scan QR", @@ -569,32 +623,45 @@ const en: BaseTranslation = { feeError: "Failed to calculate fee", }, SendBitcoinDestinationScreen: { - usernameNowAddress: "{bankName: string} usernames are now {bankName: string} addresses.", - usernameNowAddressInfo: "When you enter a {bankName: string} username, we will add \"@{lnDomain: string}\" to it (e.g maria@{lnDomain: string}) to make it an address. Your username is now a {bankName: string} address too.\n\nGo to your {bankName: string} address page from your Settings to learn how to use it or to share it to receive payments.", + usernameNowAddress: + "{bankName: string} usernames are now {bankName: string} addresses.", + usernameNowAddressInfo: + 'When you enter a {bankName: string} username, we will add "@{lnDomain: string}" to it (e.g maria@{lnDomain: string}) to make it an address. Your username is now a {bankName: string} address too.\n\nGo to your {bankName: string} address page from your Settings to learn how to use it or to share it to receive payments.', enterValidDestination: "Please enter a valid destination", - destinationOptions: "You can send to a {bankName: string} address, LN address, LN invoice, or BTC address.", + destinationOptions: + "You can send to a {bankName: string} address, LN address, LN invoice, or BTC address.", expiredInvoice: "This invoice has expired. Please generate a new invoice.", - wrongNetwork: "This invoice is for a different network. Please generate a new invoice.", - invalidAmount: "This contains an invalid amount. Please regenerate with a valid amount.", - usernameDoesNotExist: "{lnAddress: string} doesn't seem to be a {bankName: string} address that exists.", - usernameDoesNotExistAdvice: "Either make sure the spelling is right or ask the recipient for an LN invoice or BTC address instead.", + wrongNetwork: + "This invoice is for a different network. Please generate a new invoice.", + invalidAmount: + "This contains an invalid amount. Please regenerate with a valid amount.", + usernameDoesNotExist: + "{lnAddress: string} doesn't seem to be a {bankName: string} address that exists.", + usernameDoesNotExistAdvice: + "Either make sure the spelling is right or ask the recipient for an LN invoice or BTC address instead.", selfPaymentError: "{lnAddress: string} is your {bankName: string} address.", - selfPaymentAdvice: "If you want to send money to another account that you own, you can use an invoice, LN or BTC address instead.", - lnAddressError: "We can't reach this Lightning address. If you are sure it exists, you can try again later.", - lnAddressAdvice: "Either make sure the spelling is right or ask the recipient for an invoice or BTC address instead.", + selfPaymentAdvice: + "If you want to send money to another account that you own, you can use an invoice, LN or BTC address instead.", + lnAddressError: + "We can't reach this Lightning address. If you are sure it exists, you can try again later.", + lnAddressAdvice: + "Either make sure the spelling is right or ask the recipient for an invoice or BTC address instead.", unknownLightning: "We can't parse this Lightning address. Please try again.", unknownOnchain: "We can't parse this Bitcoin address. Please try again.", - newBankAddressUsername: "{lnAddress: string} exists as a {bankName: string} address, but you've never sent money to it.", + newBankAddressUsername: + "{lnAddress: string} exists as a {bankName: string} address, but you've never sent money to it.", confirmModal: { title: "You've never sent money to this address", body1: "Please make sure the recipient gave you a {bankName: string} address,", bold2bold: "not a username from another wallet.", - body3: "Otherwise, the money will go to a {bankName: string} Account that has the “{lnAddress: string}” address.\n\nCheck the spelling of the first part of the address as well. e.g. jackie and jack1e are 2 different addresses", - warning: "If the {bankName: string} address is entered incorrectly, {bankName: string} can't undo the transaction.", + body3: + "Otherwise, the money will go to a {bankName: string} Account that has the “{lnAddress: string}” address.\n\nCheck the spelling of the first part of the address as well. e.g. jackie and jack1e are 2 different addresses", + warning: + "If the {bankName: string} address is entered incorrectly, {bankName: string} can't undo the transaction.", checkBox: "{lnAddress: string} is the right address.", confirmButton: "I'm 100% sure", }, - clipboardError: "Error getting value from clipboard" + clipboardError: "Error getting value from clipboard", }, SendBitcoinScreen: { amount: "Amount", @@ -609,16 +676,20 @@ const en: BaseTranslation = { feeCalculationUnsuccessful: "Calculation unsuccessful ⚠️", placeholder: "Username, invoice, or address", invalidUsername: "Invalid username", - noAmount: "This invoice doesn't have an amount, so you need to manually specify how much money you want to send", - notConfirmed: "Payment has been sent\nbut is not confirmed yet\n\nYou can check the status\nof the payment in Transactions", + noAmount: + "This invoice doesn't have an amount, so you need to manually specify how much money you want to send", + notConfirmed: + "Payment has been sent\nbut is not confirmed yet\n\nYou can check the status\nof the payment in Transactions", note: "Note or label", success: "Payment has been sent successfully", max: "Max", maxAmount: "Max Amount", title: "Send Bitcoin", failedToFetchLnurlInvoice: "Failed to fetch lnurl invoice", - lnurlInvoiceIncorrectAmount: "The lnurl server responded with an invoice with an incorrect amount.", - lnurlInvoiceIncorrectDescription: "The lnurl server responded with an invoice with an incorrect description hash.", + lnurlInvoiceIncorrectAmount: + "The lnurl server responded with an invoice with an incorrect amount.", + lnurlInvoiceIncorrectDescription: + "The lnurl server responded with an invoice with an incorrect description hash.", }, SettingsScreen: { activated: "Activated", @@ -640,7 +711,8 @@ const en: BaseTranslation = { theme: "Theme", nfc: "Receive from NFC", nfcError: "Error reading NFC tag. Please try again.", - nfcNotCompatible: "The information fetch from the card is not a compatible lnurl-withdraw link.", + nfcNotCompatible: + "The information fetch from the card is not a compatible lnurl-withdraw link.", nfcOnlyReceive: "Only receive from NFC is available for now", nfcScanNow: "Scan NFC Now", nfcNotSupported: "NFC is not supported on this device", @@ -675,23 +747,26 @@ const en: BaseTranslation = { }, DefaultWalletScreen: { title: "Default Account", - info: "Pick which account to set as default for receiving and sending. You can adjust the send and receive account for individual payments in the mobile app. Payments received through the cash register or your Lightning address will always go to the default account.\n\nTo avoid Bitcoin's volatility, choose Stablesats. This allows you to maintain a stable amount of money while still being able to send and receive payments.\n\nYou can change this setting at any time, and it won't affect your current balance.", + info: + "Pick which account to set as default for receiving and sending. You can adjust the send and receive account for individual payments in the mobile app. Payments received through the cash register or your Lightning address will always go to the default account.\n\nTo avoid Bitcoin's volatility, choose Stablesats. This allows you to maintain a stable amount of money while still being able to send and receive payments.\n\nYou can change this setting at any time, and it won't affect your current balance.", }, ThemeScreen: { title: "Theme", - info: "Pick your preferred theme for using Blink, or choose to keep it synced with your system settings.", + info: + "Pick your preferred theme for using Blink, or choose to keep it synced with your system settings.", system: "Use System setting", light: "Use Light Mode", dark: "Use Dark Mode", }, Languages: { - "DEFAULT": "Default (OS)", + DEFAULT: "Default (OS)", }, StablesatsModal: { header: "With Stablesats, you now have a USD account added to your wallet!", - body: "You can use it to send and receive Bitcoin, and instantly transfer value between your BTC and USD account. Value in the USD account will not fluctuate with the price of Bitcoin. This feature is not compatible with the traditional banking system.", + body: + "You can use it to send and receive Bitcoin, and instantly transfer value between your BTC and USD account. Value in the USD account will not fluctuate with the price of Bitcoin. This feature is not compatible with the traditional banking system.", termsAndConditions: "Read the Terms & Conditions.", - learnMore: "Learn more about Stablesats" + learnMore: "Learn more about Stablesats", }, SplashScreen: { update: @@ -703,7 +778,8 @@ const en: BaseTranslation = { spent: "You spent", receivingAccount: "Receiving Account", sendingAccount: "Sending Account", - txNotBroadcast: "Your transaction is currently pending and will be broadcasted to the Bitcoin network in a moment." + txNotBroadcast: + "Your transaction is currently pending and will be broadcasted to the Bitcoin network in a moment.", }, TransactionLimitsScreen: { receive: "Receive", @@ -715,9 +791,11 @@ const en: BaseTranslation = { stablesatTransfers: "Stablesat Transfers", internalSend: "Send to {bankName: string} User", error: "Unable to fetch limits at this time", - contactUsMessageBody: "Hi, I will like to increase the transaction limits of my {bankName: string} account.", + contactUsMessageBody: + "Hi, I will like to increase the transaction limits of my {bankName: string} account.", contactUsMessageSubject: "Request To Increase Transaction Limits", - contactSupportToPerformKyc: "Contact support to perform manual KYC to increase your limit", + contactSupportToPerformKyc: + "Contact support to perform manual KYC to increase your limit", increaseLimits: "Increase your limits", }, TransactionScreen: { @@ -748,11 +826,13 @@ const en: BaseTranslation = { addressUnavailable: "Sorry, this address is already taken", unknownError: "An unknown error occurred, please try again later", }, - receiveMoney: "Receive money from other lightning wallets and {bankName: string} users with this address.", + receiveMoney: + "Receive money from other lightning wallets and {bankName: string} users with this address.", itCannotBeChanged: "It can't be changed later.", }, WelcomeFirstScreen: { - bank: "Bitcoin is designed to let you store, send and receive money, without relying on a bank or credit card.", + bank: + "Bitcoin is designed to let you store, send and receive money, without relying on a bank or credit card.", before: "Before Bitcoin, people had to rely on banks or credit card providers, to spend, send and receive money.", care: "Why should I care?", @@ -763,9 +843,12 @@ const en: BaseTranslation = { title: "Account set up", header: "Enter your phone number, and we'll text you an access code.", headerVerify: "Verify you are human", - errorRequestingCaptcha: "Something went wrong verifying you are human, please try again later.", - errorRequestingCode: "Something went wrong requesting the phone code, please try again later.", - errorInvalidPhoneNumber: "Invalid phone number. Are you sure you entered the right number?", + errorRequestingCaptcha: + "Something went wrong verifying you are human, please try again later.", + errorRequestingCode: + "Something went wrong requesting the phone code, please try again later.", + errorInvalidPhoneNumber: + "Invalid phone number. Are you sure you entered the right number?", errorUnsupportedCountry: "We are unable to support customers in your country.", placeholder: "Phone Number", verify: "Click to Verify", @@ -775,13 +858,15 @@ const en: BaseTranslation = { PhoneValidationScreen: { errorLoggingIn: "Error logging in. Did you use the right code?", errorTooManyAttempts: "Too many attempts. Please try again later.", - errorCannotUpgradeToExistingAccount: "This phone account already exists. Please log out of your trial account and then log in with your phone number.", + errorCannotUpgradeToExistingAccount: + "This phone account already exists. Please log out of your trial account and then log in with your phone number.", header: "To confirm your phone number, enter the code we just sent you by {channel: string} on {phoneNumber: string}", placeholder: "6 Digit Code", sendAgain: "Send Again", tryAgain: "Try Again", - sendViaOtherChannel: "You selected to receive the code via {channel: string}. You can try receiving via {other: string} instead", + sendViaOtherChannel: + "You selected to receive the code via {channel: string}. You can try receiving via {other: string} instead", }, EmailRegistrationInitiateScreen: { title: "Add your email", @@ -891,7 +976,8 @@ const en: BaseTranslation = { yesterday: "Yesterday", thisMonth: "This month", prevMonths: "Previous months", - problemMaybeReauth: "There was a problem with your request. Please retry in one minute. If the problem persist, we recommend that you log out and log back in. You can log out by going into Settings > Account > Log out", + problemMaybeReauth: + "There was a problem with your request. Please retry in one minute. If the problem persist, we recommend that you log out and log back in. You can log out by going into Settings > Account > Log out", warning: "Warning", }, errors: { @@ -928,17 +1014,23 @@ const en: BaseTranslation = { telegram: "Telegram", mattermost: "Mattermost", defaultEmailSubject: "{bankName: string} - Support", - defaultSupportMessage: "Hey there! I need some help with {bankName: string}, I'm using the version {version: string} on {os: string}.", + defaultSupportMessage: + "Hey there! I need some help with {bankName: string}, I'm using the version {version: string} on {os: string}.", emailCopied: "email {email: string} copied to clipboard", deleteAccount: "Delete account", delete: "delete", - typeDelete: "Please type \"{delete: string}\" to confirm account deletion", + typeDelete: 'Please type "{delete: string}" to confirm account deletion', finalConfirmationAccountDeletionTitle: "Final Confirmation Required", - finalConfirmationAccountDeletionMessage: "Are you sure you want to delete your account? This action is irreversible.", - deleteAccountBalanceWarning: "Deleting your account will cause you to lose access to your current balance. Are you sure you want to proceed?", - deleteAccountConfirmation: "Your account has been written for deletion.\n\nWhen the probation period related to regulatory requirement is over, the remaining data related to your account will be permanently deleted.", - deleteAccountFromPhone: "Hey there!, please delete my account. My phone number is {phoneNumber: string}.", - deleteAccountError: "Something went wrong. Contact {email: string} for further assistance.", + finalConfirmationAccountDeletionMessage: + "Are you sure you want to delete your account? This action is irreversible.", + deleteAccountBalanceWarning: + "Deleting your account will cause you to lose access to your current balance. Are you sure you want to proceed?", + deleteAccountConfirmation: + "Your account has been written for deletion.\n\nWhen the probation period related to regulatory requirement is over, the remaining data related to your account will be permanently deleted.", + deleteAccountFromPhone: + "Hey there!, please delete my account. My phone number is {phoneNumber: string}.", + deleteAccountError: + "Something went wrong. Contact {email: string} for further assistance.", bye: "Bye!", }, lnurl: { @@ -948,7 +1040,7 @@ const en: BaseTranslation = { viewPrintable: "View Printable Version", }, DisplayCurrencyScreen: { - errorLoading: "Error loading list of currencies" + errorLoading: "Error loading list of currencies", }, AmountInputScreen: { enterAmount: "Enter Amount", @@ -960,12 +1052,13 @@ const en: BaseTranslation = { tapToSetAmount: "Tap to set amount", }, AppUpdate: { - needToUpdateSupportMessage: "I need to update my app to the latest version. I'm using the {os: string} app with version {version: string}.", + needToUpdateSupportMessage: + "I need to update my app to the latest version. I'm using the {os: string} app with version {version: string}.", versionNotSupported: "This mobile version is no longer supported", updateMandatory: "Update is mandatory", tapHereUpdate: "Tap here to update now", contactSupport: "Contact Support", - } + }, } export default en diff --git a/app/i18n/i18n-types.ts b/app/i18n/i18n-types.ts index 18ee1a8fb4..91516b52e5 100644 --- a/app/i18n/i18n-types.ts +++ b/app/i18n/i18n-types.ts @@ -1591,7 +1591,7 @@ type RootTranslation = { */ title: string } - ReceiveWrapperScreen: { + ReceiveScreen: { /** * D​o​ ​y​o​u​ ​w​a​n​t​ ​t​o​ ​a​c​t​i​v​a​t​e​ ​n​o​t​i​f​i​c​a​t​i​o​n​s​ ​t​o​ ​b​e​ ​n​o​t​i​f​i​e​d​ ​w​h​e​n​ ​t​h​e​ ​p​a​y​m​e​n​t​ ​h​a​s​ ​a​r​r​i​v​e​d​? */ @@ -1629,11 +1629,11 @@ type RootTranslation = { */ error: string /** - * C​o​p​y​ ​I​n​v​o​i​c​e + * C​o​p​y */ copyInvoice: string /** - * S​h​a​r​e​ ​I​n​v​o​i​c​e + * S​h​a​r​e */ shareInvoice: string /** @@ -1694,6 +1694,95 @@ type RootTranslation = { * @param {string} minBankFeeThreshold */ fees: RequiredParams<'minBankFee' | 'minBankFeeThreshold'> + /** + * I​n​v​o​i​c​e + */ + invoice: string + /** + * P​a​y​c​o​d​e + */ + paycode: string + /** + * O​n​-​c​h​a​i​n + */ + onchain: string + /** + * B​i​t​c​o​i​n + */ + bitcoin: string + /** + * S​t​a​b​l​e​s​a​t​s + */ + stablesats: string + /** + * R​e​g​e​n​e​r​a​t​e​ ​I​n​v​o​i​c​e + */ + regenerateInvoiceButtonTitle: string + /** + * S​e​t​ ​U​s​e​r​n​a​m​e + */ + setUsernameButtonTitle: string + /** + * I​n​v​o​i​c​e​ ​h​a​s​ ​e​x​p​i​r​e​d + */ + invoiceHasExpired: string + /** + * S​e​t​ ​y​o​u​r​ ​u​s​e​r​n​a​m​e​ ​t​o​ ​a​c​c​e​p​t​ ​v​i​a​ ​P​a​y​c​o​d​e​ ​Q​R​ ​(​L​N​U​R​L​)​ ​a​n​d​ ​L​i​g​h​t​n​i​n​g​ ​A​d​d​r​e​s​s + */ + setUsernameToAcceptViaPaycode: string + /** + * S​i​n​g​l​e​ ​U​s​e + */ + singleUse: string + /** + * E​x​p​i​r​e​d​ ​I​n​v​o​i​c​e + */ + invoiceExpired: string + invoiceValidity: { + /** + * V​a​l​i​d​ ​f​o​r​ ​1​ ​d​a​y + */ + validFor1Day: string + /** + * V​a​l​i​d​ ​f​o​r​ ​n​e​x​t​ ​{​d​u​r​a​t​i​o​n​} + * @param {string} duration + */ + validForNext: RequiredParams<'duration'> + /** + * V​a​l​i​d​ ​b​e​f​o​r​e​ ​{​t​i​m​e​} + * @param {string} time + */ + validBefore: RequiredParams<'time'> + /** + * E​x​p​i​r​e​s​ ​i​n​ ​{​d​u​r​a​t​i​o​n​} + * @param {string} duration + */ + expiresIn: RequiredParams<'duration'> + /** + * E​x​p​i​r​e​s​ ​n​o​w + */ + expiresNow: string + } + /** + * I​n​v​o​i​c​e​ ​h​a​s​ ​b​e​e​n​ ​p​a​i​d + */ + invoiceHasBeenPaid: string + /** + * Y​o​u​r​ ​B​i​t​c​o​i​n​ ​O​n​c​h​a​i​n​ ​A​d​d​r​e​s​s + */ + yourBitcoinOnChainAddress: string + /** + * R​e​c​e​i​v​e​ ​v​i​a​ ​I​n​v​o​i​c​e + */ + receiveViaInvoice: string + /** + * R​e​c​e​i​v​e​ ​v​i​a​ ​P​a​y​c​o​d​e + */ + receiveViaPaycode: string + /** + * R​e​c​e​i​v​e​ ​v​i​a​ ​O​n​c​h​a​i​n + */ + receiveViaOnchain: string } RedeemBitcoinScreen: { /** @@ -4854,7 +4943,7 @@ export type TranslationFunctions = { */ title: () => LocalizedString } - ReceiveWrapperScreen: { + ReceiveScreen: { /** * Do you want to activate notifications to be notified when the payment has arrived? */ @@ -4892,11 +4981,11 @@ export type TranslationFunctions = { */ error: () => LocalizedString /** - * Copy Invoice + * Copy */ copyInvoice: () => LocalizedString /** - * Share Invoice + * Share */ shareInvoice: () => LocalizedString /** @@ -4955,6 +5044,92 @@ export type TranslationFunctions = { * {minBankFee} sats fees for onchain payment below {minBankFeeThreshold} sats */ fees: (arg: { minBankFee: string, minBankFeeThreshold: string }) => LocalizedString + /** + * Invoice + */ + invoice: () => LocalizedString + /** + * Paycode + */ + paycode: () => LocalizedString + /** + * On-chain + */ + onchain: () => LocalizedString + /** + * Bitcoin + */ + bitcoin: () => LocalizedString + /** + * Stablesats + */ + stablesats: () => LocalizedString + /** + * Regenerate Invoice + */ + regenerateInvoiceButtonTitle: () => LocalizedString + /** + * Set Username + */ + setUsernameButtonTitle: () => LocalizedString + /** + * Invoice has expired + */ + invoiceHasExpired: () => LocalizedString + /** + * Set your username to accept via Paycode QR (LNURL) and Lightning Address + */ + setUsernameToAcceptViaPaycode: () => LocalizedString + /** + * Single Use + */ + singleUse: () => LocalizedString + /** + * Expired Invoice + */ + invoiceExpired: () => LocalizedString + invoiceValidity: { + /** + * Valid for 1 day + */ + validFor1Day: () => LocalizedString + /** + * Valid for next {duration} + */ + validForNext: (arg: { duration: string }) => LocalizedString + /** + * Valid before {time} + */ + validBefore: (arg: { time: string }) => LocalizedString + /** + * Expires in {duration} + */ + expiresIn: (arg: { duration: string }) => LocalizedString + /** + * Expires now + */ + expiresNow: () => LocalizedString + } + /** + * Invoice has been paid + */ + invoiceHasBeenPaid: () => LocalizedString + /** + * Your Bitcoin Onchain Address + */ + yourBitcoinOnChainAddress: () => LocalizedString + /** + * Receive via Invoice + */ + receiveViaInvoice: () => LocalizedString + /** + * Receive via Paycode + */ + receiveViaPaycode: () => LocalizedString + /** + * Receive via Onchain + */ + receiveViaOnchain: () => LocalizedString } RedeemBitcoinScreen: { /** diff --git a/app/i18n/raw-i18n/source/en.json b/app/i18n/raw-i18n/source/en.json index 3636a9d33b..c3588a568d 100644 --- a/app/i18n/raw-i18n/source/en.json +++ b/app/i18n/raw-i18n/source/en.json @@ -487,7 +487,7 @@ "PrimaryScreen": { "title": "Home" }, - "ReceiveWrapperScreen": { + "ReceiveScreen": { "activateNotifications": "Do you want to activate notifications to be notified when the payment has arrived?", "copyClipboard": "Invoice has been copied in the clipboard", "copyClipboardBitcoin": "Bitcoin address has been copied in the clipboard", @@ -497,8 +497,8 @@ "title": "Receive Bitcoin", "usdTitle": "Receive USD", "error": "Failed to generate invoice. Please contact support if this problem persists.", - "copyInvoice": "Copy Invoice", - "shareInvoice": "Share Invoice", + "copyInvoice": "Copy", + "shareInvoice": "Share", "addAmount": "Request Specific Amount", "expired": "The invoice has expired", "expiresIn": "Expires in", @@ -512,7 +512,30 @@ "useALightningInvoice": "Use a Lightning Invoice", "setANote": "Set a Note", "invoiceAmount": "Invoice Amount", - "fees": "{minBankFee: string} sats fees for onchain payment below {minBankFeeThreshold: string} sats" + "fees": "{minBankFee: string} sats fees for onchain payment below {minBankFeeThreshold: string} sats", + "invoice": "Invoice", + "paycode": "Paycode", + "onchain": "On-chain", + "bitcoin": "Bitcoin", + "stablesats": "Stablesats", + "regenerateInvoiceButtonTitle": "Regenerate Invoice", + "setUsernameButtonTitle": "Set Username", + "invoiceHasExpired": "Invoice has expired", + "setUsernameToAcceptViaPaycode": "Set your username to accept via Paycode QR (LNURL) and Lightning Address", + "singleUse": "Single Use", + "invoiceExpired": "Expired Invoice", + "invoiceValidity": { + "validFor1Day": "Valid for 1 day", + "validForNext": "Valid for next {duration: string}", + "validBefore": "Valid before {time: string}", + "expiresIn": "Expires in {duration: string}", + "expiresNow": "Expires now" + }, + "invoiceHasBeenPaid": "Invoice has been paid", + "yourBitcoinOnChainAddress": "Your Bitcoin Onchain Address", + "receiveViaInvoice": "Receive via Invoice", + "receiveViaPaycode": "Receive via Paycode", + "receiveViaOnchain": "Receive via Onchain" }, "RedeemBitcoinScreen": { "title": "Redeem Bitcoin", diff --git a/app/i18n/raw-i18n/translations/af.json b/app/i18n/raw-i18n/translations/af.json index 8234ea9544..2cc1bab515 100644 --- a/app/i18n/raw-i18n/translations/af.json +++ b/app/i18n/raw-i18n/translations/af.json @@ -485,7 +485,7 @@ "PrimaryScreen": { "title": "Tuis" }, - "ReceiveWrapperScreen": { + "ReceiveScreen": { "activateNotifications": "Wil jy kennisgewings aktiveer en kennisgewings ontvang wanneer die betaling arriveer het? ", "copyClipboard": "Faktuur is na die knipbord oorgedra", "copyClipboardBitcoin": "Bitcoin-adres is na die knipbord oorgedra", diff --git a/app/i18n/raw-i18n/translations/ar.json b/app/i18n/raw-i18n/translations/ar.json index 720de0d173..f747d0cfdc 100644 --- a/app/i18n/raw-i18n/translations/ar.json +++ b/app/i18n/raw-i18n/translations/ar.json @@ -479,7 +479,7 @@ "PrimaryScreen": { "title": "الرئيسية" }, - "ReceiveWrapperScreen": { + "ReceiveScreen": { "activateNotifications": "هل تريد تفعيل الإشعارات لتُشعَر عندما يصلك الدفع؟", "copyClipboard": "نُسخت الفاتورة إلى الحافظة", "copyClipboardBitcoin": "نُسخ عنوان البيتكوين إلى المحفظة", diff --git a/app/i18n/raw-i18n/translations/ca.json b/app/i18n/raw-i18n/translations/ca.json index 5550cca26c..74346018f3 100644 --- a/app/i18n/raw-i18n/translations/ca.json +++ b/app/i18n/raw-i18n/translations/ca.json @@ -479,7 +479,7 @@ "PrimaryScreen": { "title": "Casa" }, - "ReceiveWrapperScreen": { + "ReceiveScreen": { "activateNotifications": "Vols activar les notificacions per ser avisat quan arribi el pagament?", "copyClipboard": "La factura ha estat copiada al porta-retalls", "copyClipboardBitcoin": "L'adreça bitcoin ha estat copiada al porta-retalls", diff --git a/app/i18n/raw-i18n/translations/cs.json b/app/i18n/raw-i18n/translations/cs.json index 7a683555d3..35ae972772 100644 --- a/app/i18n/raw-i18n/translations/cs.json +++ b/app/i18n/raw-i18n/translations/cs.json @@ -479,7 +479,7 @@ "PrimaryScreen": { "title": "Domů" }, - "ReceiveWrapperScreen": { + "ReceiveScreen": { "activateNotifications": "Chcete aktivovat oznámení, abyste byli informováni o příchozích platbách?", "copyClipboard": "Faktura byla zkopírována do schránky", "copyClipboardBitcoin": "Bitcoinová adresa byla zkopírována do schránky", diff --git a/app/i18n/raw-i18n/translations/de.json b/app/i18n/raw-i18n/translations/de.json index 10d7792dfd..9187de857e 100644 --- a/app/i18n/raw-i18n/translations/de.json +++ b/app/i18n/raw-i18n/translations/de.json @@ -485,7 +485,7 @@ "PrimaryScreen": { "title": "Home" }, - "ReceiveWrapperScreen": { + "ReceiveScreen": { "activateNotifications": "Möchtest Du die Benachrichtigungen aktivieren, um zu sehen, wenn die Zahlung eingegangen ist?", "copyClipboard": "Invoice wurde in die Zwischenablage kopiert", "copyClipboardBitcoin": "Bitcoin Adresse wurde in die Zwischenablage kopiert", diff --git a/app/i18n/raw-i18n/translations/el.json b/app/i18n/raw-i18n/translations/el.json index c782f50ffb..b6aeca0a13 100644 --- a/app/i18n/raw-i18n/translations/el.json +++ b/app/i18n/raw-i18n/translations/el.json @@ -485,7 +485,7 @@ "PrimaryScreen": { "title": "Αρχική Σελίδα " }, - "ReceiveWrapperScreen": { + "ReceiveScreen": { "activateNotifications": "Θέλετε να ενεργοποιήσετε τις ειδοποιήσεις για να ενημερώνεστε όταν ολοκληρωθεί η πληρωμή;", "copyClipboard": "Το τιμολόγιο αντιγράφηκε στο πρόχειρο", "copyClipboardBitcoin": "Η διεύθυνση Bitcoin έχει αντιγραφεί στο πρόχειρο", diff --git a/app/i18n/raw-i18n/translations/es.json b/app/i18n/raw-i18n/translations/es.json index 3867e731a8..1899d29915 100644 --- a/app/i18n/raw-i18n/translations/es.json +++ b/app/i18n/raw-i18n/translations/es.json @@ -485,7 +485,7 @@ "PrimaryScreen": { "title": "Inicio" }, - "ReceiveWrapperScreen": { + "ReceiveScreen": { "activateNotifications": "¿Quieres activar notificaciones para que te avisen cuando haya llegado el pago?", "copyClipboard": "La factura se ha copiado al portapapeles", "copyClipboardBitcoin": "La dirección de Bitcoin se ha copiado al portapapeles", diff --git a/app/i18n/raw-i18n/translations/fr.json b/app/i18n/raw-i18n/translations/fr.json index 21294fc4ac..2127789743 100644 --- a/app/i18n/raw-i18n/translations/fr.json +++ b/app/i18n/raw-i18n/translations/fr.json @@ -485,7 +485,7 @@ "PrimaryScreen": { "title": "Accueil" }, - "ReceiveWrapperScreen": { + "ReceiveScreen": { "activateNotifications": "Souhaitez-vous activer les notifications pour être prévenu lorsque le paiement est bien parvenu ?", "copyClipboard": "La facture a été copiée dans le presse-papier", "copyClipboardBitcoin": "L’adresse Bitcoin a été copiée dans le presse-papier", diff --git a/app/i18n/raw-i18n/translations/hr.json b/app/i18n/raw-i18n/translations/hr.json index 2a122aa55f..08c8451433 100644 --- a/app/i18n/raw-i18n/translations/hr.json +++ b/app/i18n/raw-i18n/translations/hr.json @@ -479,7 +479,7 @@ "PrimaryScreen": { "title": "Doma" }, - "ReceiveWrapperScreen": { + "ReceiveScreen": { "activateNotifications": "Želite li aktivirati obavijesti kako biste bili obaviješteni kada uplata stigne?", "copyClipboard": "Faktura je kopirana u međuspremnik", "copyClipboardBitcoin": "Bitcoin adresa je kopirana u međuspremnik", diff --git a/app/i18n/raw-i18n/translations/it.json b/app/i18n/raw-i18n/translations/it.json index 9925d47bd3..7b70635b93 100644 --- a/app/i18n/raw-i18n/translations/it.json +++ b/app/i18n/raw-i18n/translations/it.json @@ -479,7 +479,7 @@ "PrimaryScreen": { "title": "Home" }, - "ReceiveWrapperScreen": { + "ReceiveScreen": { "activateNotifications": "Vuoi attivare le notifiche per essere avvisato quando ricevi un pagamento?", "copyClipboard": "L'invoice è stata copiata negli appunti", "copyClipboardBitcoin": "L'indirizzo Bitcoin è stato copiato negli appunti", diff --git a/app/i18n/raw-i18n/translations/ms.json b/app/i18n/raw-i18n/translations/ms.json index c785f6ef6f..0577ec65d9 100644 --- a/app/i18n/raw-i18n/translations/ms.json +++ b/app/i18n/raw-i18n/translations/ms.json @@ -479,7 +479,7 @@ "PrimaryScreen": { "title": "Rumah" }, - "ReceiveWrapperScreen": { + "ReceiveScreen": { "activateNotifications": "Anda nak aktifkan notifikasi jika terima bayaran?", "copyClipboard": "Invois telah disalin pada clipboard", "copyClipboardBitcoin": "Alamat bitcoin telah disalin ke dalam clipboard", diff --git a/app/i18n/raw-i18n/translations/nl.json b/app/i18n/raw-i18n/translations/nl.json index e0c08e2e72..1a63b7f4d8 100644 --- a/app/i18n/raw-i18n/translations/nl.json +++ b/app/i18n/raw-i18n/translations/nl.json @@ -479,7 +479,7 @@ "PrimaryScreen": { "title": "Thuisscherm" }, - "ReceiveWrapperScreen": { + "ReceiveScreen": { "activateNotifications": "Wil je notificaties activeren om op de hoogte te worden gehouden wanneer de betaling is binnengekomen?", "copyClipboard": "Factuur is gekopieerd naar het klembord", "copyClipboardBitcoin": "Bitcoin-adres is gekopieerd naar het klembord", diff --git a/app/i18n/raw-i18n/translations/pt.json b/app/i18n/raw-i18n/translations/pt.json index 25c376edd5..ea2d78d347 100644 --- a/app/i18n/raw-i18n/translations/pt.json +++ b/app/i18n/raw-i18n/translations/pt.json @@ -479,7 +479,7 @@ "PrimaryScreen": { "title": "Home" }, - "ReceiveWrapperScreen": { + "ReceiveScreen": { "activateNotifications": "Deseja ativar as notificações para ser avisado quando o pagamento chegar?", "copyClipboard": "A fatura foi copiada na área de transferência", "copyClipboardBitcoin": "O endereço Bitcoin foi copiado na área de transferência", diff --git a/app/i18n/raw-i18n/translations/qu.json b/app/i18n/raw-i18n/translations/qu.json index a9c3cc304f..be29d98eaa 100644 --- a/app/i18n/raw-i18n/translations/qu.json +++ b/app/i18n/raw-i18n/translations/qu.json @@ -479,7 +479,7 @@ "PrimaryScreen": { "title": "Ayllu" }, - "ReceiveWrapperScreen": { + "ReceiveScreen": { "activateNotifications": "¿Imaynataqmi notificaciones tukuyta kani, qhipa rurasqan kani kashanmi?", "copyClipboard": "Kawsayniyta qillqa churayta rikusqa", "copyClipboardBitcoin": "Bitcoin kaniyta churayta rikusqa", diff --git a/app/i18n/raw-i18n/translations/sr.json b/app/i18n/raw-i18n/translations/sr.json index 6a0be955d2..4eb6c1c77b 100644 --- a/app/i18n/raw-i18n/translations/sr.json +++ b/app/i18n/raw-i18n/translations/sr.json @@ -479,7 +479,7 @@ "PrimaryScreen": { "title": "Почетна" }, - "ReceiveWrapperScreen": { + "ReceiveScreen": { "activateNotifications": "Да ли желите да активирате нотификације да бисте били обавештени када уплата пристигне?", "copyClipboard": "Рачун је копиран у клипборд", "copyClipboardBitcoin": "Биткоин адреса је копирана у клипборд", diff --git a/app/i18n/raw-i18n/translations/sw.json b/app/i18n/raw-i18n/translations/sw.json index 9f058e8baa..0afa21207c 100644 --- a/app/i18n/raw-i18n/translations/sw.json +++ b/app/i18n/raw-i18n/translations/sw.json @@ -479,7 +479,7 @@ "PrimaryScreen": { "title": "Nyumbani" }, - "ReceiveWrapperScreen": { + "ReceiveScreen": { "activateNotifications": "Je, ungependa kuwezesha arifa ili uarifiwe malipo yanapofika?", "copyClipboard": "Ankara imenakiliwa kwenye ubao wa kunakili", "copyClipboardBitcoin": "Anwani ya Bitcoin imenakiliwa kwenye ubao wa kunakili", diff --git a/app/i18n/raw-i18n/translations/th.json b/app/i18n/raw-i18n/translations/th.json index 1b039f44cf..d8ab6849b7 100644 --- a/app/i18n/raw-i18n/translations/th.json +++ b/app/i18n/raw-i18n/translations/th.json @@ -479,7 +479,7 @@ "PrimaryScreen": { "title": "หน้าหลัก" }, - "ReceiveWrapperScreen": { + "ReceiveScreen": { "activateNotifications": "คุณต้องการเปิดการแจ้งเตือนเมื่อธุรกรรมเข้ามาหรือไม่?", "copyClipboard": "Invoice ได้ทำการคัดลอกไว้ในคลิปบอร์ดแล้ว", "copyClipboardBitcoin": "คัดลอก Bitcoin address ไว้ในคลิปบอร์ดแล้ว", diff --git a/app/i18n/raw-i18n/translations/tr.json b/app/i18n/raw-i18n/translations/tr.json index bc16b28d07..baa0458295 100644 --- a/app/i18n/raw-i18n/translations/tr.json +++ b/app/i18n/raw-i18n/translations/tr.json @@ -479,7 +479,7 @@ "PrimaryScreen": { "title": "Ev" }, - "ReceiveWrapperScreen": { + "ReceiveScreen": { "activateNotifications": "Do you want to activate notifications to be notified when the payment has arrived?", "copyClipboard": "Fatura panoya kopyalandı", "copyClipboardBitcoin": "Bitcoin adresi panoya kopyalandı", diff --git a/app/i18n/raw-i18n/translations/vi.json b/app/i18n/raw-i18n/translations/vi.json index 0d9ecc902c..759dee7b6f 100644 --- a/app/i18n/raw-i18n/translations/vi.json +++ b/app/i18n/raw-i18n/translations/vi.json @@ -479,7 +479,7 @@ "PrimaryScreen": { "title": "Nhà" }, - "ReceiveWrapperScreen": { + "ReceiveScreen": { "activateNotifications": "Bạn có muốn kích hoạt thông báo để được thông báo khi nhận được thanh toán không?", "copyClipboard": "Hóa đơn đã được sao chép vào bộ nhớ tạm", "copyClipboardBitcoin": "Địa chỉ bitcoin đã được sao chép vào bộ nhớ tạm", diff --git a/app/i18n/raw-i18n/translations/xh.json b/app/i18n/raw-i18n/translations/xh.json index 5fdfd4c9a8..c08a498b6f 100644 --- a/app/i18n/raw-i18n/translations/xh.json +++ b/app/i18n/raw-i18n/translations/xh.json @@ -479,7 +479,7 @@ "PrimaryScreen": { "title": "Ekhaya" }, - "ReceiveWrapperScreen": { + "ReceiveScreen": { "activateNotifications": "Ngaba uyafuna ukwenza izaziso zisebenze xa intlawulo ifikile?", "copyClipboard": "I-invoyisi ikhutshelwe kwibhodi eqhotyoshwayo", "copyClipboardBitcoin": "Idilesi yeBitcoin ikhutshelwe kwibhodi eqhotyoshwayo", diff --git a/app/navigation/root-navigator.tsx b/app/navigation/root-navigator.tsx index 834d1aec0e..baf9cbe2a7 100644 --- a/app/navigation/root-navigator.tsx +++ b/app/navigation/root-navigator.tsx @@ -31,7 +31,7 @@ import { } from "@app/screens/conversion-flow" import { GaloyAddressScreen } from "@app/screens/galoy-address-screen" import { PhoneInputScreen, PhoneValidationScreen } from "@app/screens/phone-auth-screen" -import ReceiveWrapperScreen from "@app/screens/receive-bitcoin-screen/receive-wrapper" +import ReceiveScreen from "@app/screens/receive-bitcoin-screen/receive-screen" import RedeemBitcoinDetailScreen from "@app/screens/redeem-lnurl-withdrawal-screen/redeem-bitcoin-detail-screen" import RedeemBitcoinResultScreen from "@app/screens/redeem-lnurl-withdrawal-screen/redeem-bitcoin-result-screen" import SendBitcoinConfirmationScreen from "@app/screens/send-bitcoin-screen/send-bitcoin-confirmation-screen" @@ -163,9 +163,9 @@ export const RootStack = () => { /> { - if (type === TYPE_LIGHTNING_BTC || type === TYPE_LIGHTNING_USD) { - // TODO add lightning: - return uppercase ? input.toUpperCase() : input - } - - const uriPrefix = prefix ? prefixByType[type] : "" - const uri = `${uriPrefix}${input}` - const params = new URLSearchParams() - - if (amount) params.append("amount", `${satsToBTC(amount)}`) - - if (memo) { - params.append("message", encodeURI(memo)) - return `${uri}?${params.toString()}` - } - - return uri + (params.toString() ? "?" + params.toString() : "") -} - -export const satsToBTC = (satsAmount: number): number => satsAmount / 10 ** 8 - -export const getDefaultMemo = (bankName: string) => { - return `Pay to ${bankName} Wallet user` -} diff --git a/app/screens/receive-bitcoin-screen/payment-requests/index.ts b/app/screens/receive-bitcoin-screen/payment-requests/index.ts deleted file mode 100644 index d5ae132cae..0000000000 --- a/app/screens/receive-bitcoin-screen/payment-requests/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./payment-request-details" -export * from "./payment-request" -export * from "./helpers" diff --git a/app/screens/receive-bitcoin-screen/payment-requests/index.types.ts b/app/screens/receive-bitcoin-screen/payment-requests/index.types.ts deleted file mode 100644 index 155c60caa6..0000000000 --- a/app/screens/receive-bitcoin-screen/payment-requests/index.types.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { - GraphQlApplicationError, - LnInvoice, - LnNoAmountInvoice, - Network, - useLnInvoiceCreateMutation, - useLnNoAmountInvoiceCreateMutation, - useLnUsdInvoiceCreateMutation, - useOnChainAddressCurrentMutation, - WalletCurrency, -} from "@app/graphql/generated" -import { - BtcMoneyAmount, - WalletOrDisplayCurrency, - MoneyAmount, - WalletAmount, -} from "@app/types/amounts" -import { WalletDescriptor } from "@app/types/wallets" -import { GraphQLError } from "graphql" - -export type CreatePaymentRequestParams = { - paymentRequestData: PaymentRequestData - network: Network -} - -export type PaymentRequest = { - expiration?: Date - getFullUri: GetFullUriFn - paymentRequestDisplay: string // what is displayed to the user - paymentRequestData: LightningPaymentRequestData | OnChainPaymentRequestData | undefined -} - -export type GetFullUriFn = (params: { uppercase?: boolean; prefix?: boolean }) => string - -export type PaymentRequestDetails = { - unitOfAccountAmount?: MoneyAmount // amount in the currency that the user denominates the payment in - settlementAmount?: WalletAmount // amount in the currency of the receiving wallet - convertMoneyAmount: ConvertMoneyAmount - receivingWalletDescriptor: WalletDescriptor - memo?: string - paymentRequestType: PaymentRequestType - generatePaymentRequest: ( - mutations: GqlGeneratePaymentRequestMutations, - ) => Promise -} & PaymentRequestAmountData - -export type GeneratePaymentRequestResult = { - paymentRequest?: PaymentRequest - gqlErrors: readonly GraphQLError[] - applicationErrors: readonly GraphQlApplicationError[] -} - -export const PaymentRequest = { - Lightning: "Lightning", - OnChain: "OnChain", -} as const - -export type PaymentRequestType = (typeof PaymentRequest)[keyof typeof PaymentRequest] - -export type ConvertMoneyAmount = ( - moneyAmount: MoneyAmount, - toCurrency: W, -) => MoneyAmount - -// Rule that ensures amount are either all undefined or all defined -export type PaymentRequestAmountData = - | { - unitOfAccountAmount: MoneyAmount - settlementAmount: WalletAmount - } - | { - unitOfAccountAmount?: undefined - settlementAmount?: undefined - } - -export type GqlGeneratePaymentRequestParams = { - mutations: GqlGeneratePaymentRequestMutations - memo?: string - paymentRequestType: PaymentRequestType - receivingWalletDescriptor: WalletDescriptor - amount?: WalletAmount -} - -export type GqlGeneratePaymentRequestMutations = { - lnNoAmountInvoiceCreate: ReturnType["0"] - lnInvoiceCreate: ReturnType["0"] - lnUsdInvoiceCreate: ReturnType["0"] - onChainAddressCurrent: ReturnType["0"] -} - -export type GqlGeneratePaymentRequestResult = { - gqlErrors: readonly GraphQLError[] | undefined - applicationErrors: readonly GraphQlApplicationError[] | undefined - paymentRequestData: PaymentRequestData | undefined -} - -export type PaymentRequestData = LightningPaymentRequestData | OnChainPaymentRequestData - -export type LightningPaymentRequestData = (LnInvoice | LnNoAmountInvoice) & { - paymentRequestType: typeof PaymentRequest.Lightning -} - -export type OnChainPaymentRequestData = { - address: string - amount?: BtcMoneyAmount | undefined - memo?: string - paymentRequestType: typeof PaymentRequest.OnChain -} diff --git a/app/screens/receive-bitcoin-screen/payment-requests/payment-request-details.ts b/app/screens/receive-bitcoin-screen/payment-requests/payment-request-details.ts deleted file mode 100644 index 8fa4bf59b9..0000000000 --- a/app/screens/receive-bitcoin-screen/payment-requests/payment-request-details.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { GraphQlApplicationError, Network, WalletCurrency } from "@app/graphql/generated" -import { - BtcMoneyAmount, - MoneyAmount, - WalletAmount, - WalletOrDisplayCurrency, -} from "@app/types/amounts" -import { WalletDescriptor } from "@app/types/wallets" -import { GraphQLError } from "graphql" -import { - ConvertMoneyAmount, - GeneratePaymentRequestResult, - GqlGeneratePaymentRequestMutations, - GqlGeneratePaymentRequestParams, - GqlGeneratePaymentRequestResult, - PaymentRequestAmountData, - PaymentRequestDetails, - PaymentRequest, - LightningPaymentRequestData, - PaymentRequestType, -} from "./index.types" -import { createPaymentRequest } from "./payment-request" - -export type CreatePaymentRequestDetailsParams = { - memo?: string - unitOfAccountAmount?: MoneyAmount - paymentRequestType: PaymentRequestType - receivingWalletDescriptor: WalletDescriptor - convertMoneyAmount: ConvertMoneyAmount - bitcoinNetwork: Network -} - -export const createPaymentRequestDetails = ( - params: CreatePaymentRequestDetailsParams, -): PaymentRequestDetails => { - const { - memo, - unitOfAccountAmount, - paymentRequestType, - receivingWalletDescriptor, - bitcoinNetwork, - convertMoneyAmount, - } = params - - let paymentRequestAmountData: PaymentRequestAmountData = {} - - if (unitOfAccountAmount) { - const settlementAmount = convertMoneyAmount( - unitOfAccountAmount, - receivingWalletDescriptor.currency, - ) - paymentRequestAmountData = { - unitOfAccountAmount, - settlementAmount, - } - } - - const generatePaymentRequest = async ( - mutations: GqlGeneratePaymentRequestMutations, - ): Promise => { - const { paymentRequestData, gqlErrors, applicationErrors } = - await gqlGeneratePaymentRequest({ - memo, - paymentRequestType, - receivingWalletDescriptor, - amount: paymentRequestAmountData.settlementAmount, - mutations, - }) - - const paymentRequest = paymentRequestData - ? createPaymentRequest({ paymentRequestData, network: bitcoinNetwork }) - : undefined - - return { - gqlErrors: gqlErrors || [], - applicationErrors: applicationErrors || [], - paymentRequest, - } - } - - return { - ...paymentRequestAmountData, - paymentRequestType, - memo, - receivingWalletDescriptor, - convertMoneyAmount, - generatePaymentRequest, - } -} - -// throws if receivingWalletDescriptor.currency !== WalletCurrency.Btc and paymentRequestType === InvoiceType.Onchain -const gqlGeneratePaymentRequest = async ({ - memo, - paymentRequestType, - receivingWalletDescriptor, - amount, - mutations, -}: GqlGeneratePaymentRequestParams): Promise => { - let gqlErrors: readonly GraphQLError[] | undefined = undefined - let applicationErrors: readonly GraphQlApplicationError[] | undefined = undefined - - if (paymentRequestType === PaymentRequest.Lightning) { - let paymentRequestData: LightningPaymentRequestData | undefined = undefined - if (amount && amount.amount) { - const lnAmountInput = { - variables: { - input: { - amount: amount.amount, - walletId: receivingWalletDescriptor.id, - memo, - }, - }, - } - - if (receivingWalletDescriptor.currency === WalletCurrency.Btc) { - const { errors, data } = await mutations.lnInvoiceCreate(lnAmountInput) - applicationErrors = data?.lnInvoiceCreate?.errors - paymentRequestData = data?.lnInvoiceCreate?.invoice - ? { - ...data.lnInvoiceCreate.invoice, - paymentRequestType: PaymentRequest.Lightning, - } - : undefined - gqlErrors = errors - } else { - const { data, errors } = await mutations.lnUsdInvoiceCreate(lnAmountInput) - applicationErrors = data?.lnUsdInvoiceCreate?.errors - paymentRequestData = data?.lnUsdInvoiceCreate?.invoice - ? { - ...data.lnUsdInvoiceCreate.invoice, - paymentRequestType: PaymentRequest.Lightning, - } - : undefined - gqlErrors = errors - } - } else { - const lnNoAmountInput = { - variables: { - input: { - walletId: receivingWalletDescriptor.id, - memo, - }, - }, - } - const { data, errors } = await mutations.lnNoAmountInvoiceCreate(lnNoAmountInput) - - applicationErrors = data?.lnNoAmountInvoiceCreate?.errors - paymentRequestData = data?.lnNoAmountInvoiceCreate?.invoice - ? { - ...data.lnNoAmountInvoiceCreate.invoice, - paymentRequestType: PaymentRequest.Lightning, - } - : undefined - gqlErrors = errors - } - return { - paymentRequestData, - gqlErrors, - applicationErrors, - } - } - - const { data, errors } = await mutations.onChainAddressCurrent({ - variables: { - input: { - walletId: receivingWalletDescriptor.id, - }, - }, - }) - - applicationErrors = data?.onChainAddressCurrent?.errors - const address = data?.onChainAddressCurrent?.address || undefined - gqlErrors = errors - - if (amount && !isBtcMoneyAmount(amount)) { - throw new Error("On-chain invoices only support BTC") - } - - const paymentRequestData = address - ? { - address, - memo, - amount, - paymentRequestType: PaymentRequest.OnChain, - } - : undefined - - return { - paymentRequestData, - gqlErrors, - applicationErrors, - } -} - -const isBtcMoneyAmount = ( - moneyAmount: WalletAmount, -): moneyAmount is BtcMoneyAmount => { - return moneyAmount.currency === WalletCurrency.Btc -} diff --git a/app/screens/receive-bitcoin-screen/payment-requests/payment-request.ts b/app/screens/receive-bitcoin-screen/payment-requests/payment-request.ts deleted file mode 100644 index fc7115d209..0000000000 --- a/app/screens/receive-bitcoin-screen/payment-requests/payment-request.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { decodeInvoiceString, Network as NetworkLibGaloy } from "@galoymoney/client" -import { getPaymentRequestFullUri, TYPE_LIGHTNING_BTC } from "./helpers" -import { CreatePaymentRequestParams, GetFullUriFn, PaymentRequest } from "./index.types" - -export const createPaymentRequest = ( - params: CreatePaymentRequestParams, -): PaymentRequest => { - const { paymentRequestData, network: bitcoinNetwork } = params - - if (paymentRequestData.paymentRequestType === PaymentRequest.Lightning) { - const { paymentRequest: lnPaymentRequest } = paymentRequestData - const paymentRequestDisplay = lnPaymentRequest - const getFullUri: GetFullUriFn = ({ uppercase, prefix }) => - getPaymentRequestFullUri({ - input: lnPaymentRequest, - uppercase, - prefix, - type: TYPE_LIGHTNING_BTC, - }) - - const dateString = decodeInvoiceString( - lnPaymentRequest, - bitcoinNetwork as NetworkLibGaloy, - ).timeExpireDateString - const expiration = dateString ? new Date(dateString) : undefined - - return { - paymentRequestDisplay, - getFullUri, - expiration, - paymentRequestData, - } - } - - const { address, amount, memo } = paymentRequestData - - const getFullUri: GetFullUriFn = ({ uppercase, prefix }) => - getPaymentRequestFullUri({ - input: address, - amount: amount?.amount, - memo, - uppercase, - prefix, - }) - const paymentRequestDisplay = getFullUri({}) - - return { - paymentRequestDisplay, - getFullUri, - paymentRequestData, - } -} diff --git a/app/screens/receive-bitcoin-screen/payment/helpers.ts b/app/screens/receive-bitcoin-screen/payment/helpers.ts new file mode 100644 index 0000000000..e1065402af --- /dev/null +++ b/app/screens/receive-bitcoin-screen/payment/helpers.ts @@ -0,0 +1,73 @@ +import { Invoice, GetFullUriInput } from "./index.types" + +const prefixByType = { + [Invoice.OnChain]: "bitcoin:", + [Invoice.Lightning]: "lightning:", + [Invoice.PayCode]: "", +} + +export const getPaymentRequestFullUri = ({ + input, + amount, + memo, + uppercase = false, + prefix = true, + type = Invoice.OnChain, +}: GetFullUriInput): string => { + if (type === Invoice.Lightning) { + return uppercase ? input.toUpperCase() : input + } + + const uriPrefix = prefix ? prefixByType[type] : "" + const uri = `${uriPrefix}${input}` + const params = new URLSearchParams() + + if (amount) params.append("amount", `${satsToBTC(amount)}`) + + if (memo) { + params.append("message", encodeURI(memo)) + return `${uri}?${params.toString()}` + } + + return uri + (params.toString() ? "?" + params.toString() : "") +} + +export const satsToBTC = (satsAmount: number): number => satsAmount / 10 ** 8 + +export const getDefaultMemo = (bankName: string) => { + return `Pay to ${bankName} Wallet user` +} + +export const secondsToH = (seconds: number): string => { + const h = Math.floor(seconds / 3600) + const m = Math.floor((seconds % 3600) / 60) + const s = seconds % 60 + + const hDisplay = h > 0 ? h + "h" : "" + + return hDisplay +} + +export const secondsToHMS = (seconds: number): string => { + const h = Math.floor(seconds / 3600) + const m = Math.floor((seconds % 3600) / 60) + const s = seconds % 60 + + const hDisplay = h > 0 ? h + "h" : "" + const mDisplay = m > 0 ? m + "m" : "" + const sDisplay = s > 0 ? s + "s" : "" + + return hDisplay + mDisplay + sDisplay +} + +export const generateFutureLocalTime = (secondsToAdd: number): string => { + const date = new Date() // Get current date + date.setSeconds(date.getSeconds() + secondsToAdd) // Add seconds to the current date + + // Format to local time string + const hours = date.getHours() % 12 || 12 // Get hours (12 hour format), replace 0 with 12 + const minutes = String(date.getMinutes()).padStart(2, "0") // Get minutes + const period = date.getHours() >= 12 ? "PM" : "AM" // Get AM/PM + + return `${hours}:${minutes}${period}` +} diff --git a/app/screens/receive-bitcoin-screen/payment/index.types.ts b/app/screens/receive-bitcoin-screen/payment/index.types.ts new file mode 100644 index 0000000000..1920f20197 --- /dev/null +++ b/app/screens/receive-bitcoin-screen/payment/index.types.ts @@ -0,0 +1,183 @@ +/** + * Domain Nomenclature: + * ------------------- + * PaymentRequestCreationData - Clientside request data to create the actual request + * PaymentRequest - Generated quotation which contains the finalized invoice data + * Invoice - (not specific to LN) The quoted invoice that contains invoice type specific data + */ + +import { + GraphQlApplicationError, + LnInvoice, + LnNoAmountInvoice, + WalletCurrency, + LnNoAmountInvoiceCreateMutationHookResult, + LnInvoiceCreateMutationHookResult, + LnUsdInvoiceCreateMutationHookResult, + OnChainAddressCurrentMutationHookResult, + Network, +} from "@app/graphql/generated" +import { + BtcMoneyAmount, + MoneyAmount, + WalletAmount, + WalletOrDisplayCurrency, +} from "@app/types/amounts" +import { ConvertMoneyAmount } from "@app/screens/send-bitcoin-screen/payment-details" +import { BtcWalletDescriptor, WalletDescriptor } from "@app/types/wallets" +import { GraphQLError } from "graphql" + +// ------------------------ COMMONS ------------------------ + +/* Invoice */ +export const Invoice = { + Lightning: "Lightning", + OnChain: "OnChain", + PayCode: "PayCode", +} as const +export type InvoiceType = typeof Invoice[keyof typeof Invoice] + +/* Invoice Data */ +export type InvoiceData = ( + | LightningInvoiceData + | OnChainInvoiceData + | PayCodeInvoiceData +) & { + getFullUriFn: GetFullUriFn +} +export type LightningInvoiceData = (LnInvoice | LnNoAmountInvoice) & { + invoiceType: typeof Invoice.Lightning + expiresAt?: Date + memo?: string +} +export type OnChainInvoiceData = { + invoiceType: typeof Invoice.OnChain + address?: string // TODO: this should not be undefinable + amount?: BtcMoneyAmount | undefined + memo?: string +} +export type PayCodeInvoiceData = { + invoiceType: typeof Invoice.PayCode + username: string +} + +// Misc +export type GetFullUriFn = (params: { uppercase?: boolean; prefix?: boolean }) => string +export type GetFullUriInput = { + input: string + amount?: number + memo?: string + uppercase?: boolean + prefix?: boolean + type?: InvoiceType +} +export type ConvertMoneyAmountFn = ConvertMoneyAmount + +// ------------------------ REQUEST ------------------------ + +/* Stores clientside details and instructions to be sent to the server and generate a PaymentQuotation */ +export type PaymentRequestCreationData< + T extends WalletCurrency +> = BasePaymentRequestCreationData + +type BasePaymentRequestCreationData = { + // Invoice Type + type: InvoiceType + setType: (type: InvoiceType) => PaymentRequestCreationData + + // Default Wallet Descriptor + defaultWalletDescriptor: WalletDescriptor + setDefaultWalletDescriptor: ( + defaultWalletDescriptor: WalletDescriptor, + ) => PaymentRequestCreationData + + // Bitcoin Wallet Descriptor + bitcoinWalletDescriptor: BtcWalletDescriptor + + // Receive in which wallet information + receivingWalletDescriptor: WalletDescriptor + canSetReceivingWalletDescriptor: boolean + setReceivingWalletDescriptor?: ( + receivingWalletDescriptor: WalletDescriptor, + ) => PaymentRequestCreationData + + // Memo + canSetMemo: boolean + memo?: string + setMemo?: (memo: string) => PaymentRequestCreationData + + // Amount + canSetAmount: boolean + setAmount?: ( + unitOfAccountAmount: MoneyAmount, + ) => PaymentRequestCreationData + unitOfAccountAmount?: MoneyAmount + settlementAmount?: WalletAmount + + // For money conversion in case amount is given + convertMoneyAmount: ConvertMoneyAmount + setConvertMoneyAmount: ( + convertMoneyAmount: ConvertMoneyAmount, + ) => PaymentRequestCreationData + + // Can use paycode (if username is set) + canUsePaycode: boolean + username?: string + posUrl: string + + network: Network +} + +export type BaseCreatePaymentRequestCreationDataParams = { + type: InvoiceType + defaultWalletDescriptor: WalletDescriptor + bitcoinWalletDescriptor: BtcWalletDescriptor + convertMoneyAmount: ConvertMoneyAmount + username?: string + posUrl: string + receivingWalletDescriptor?: WalletDescriptor + memo?: string + unitOfAccountAmount?: MoneyAmount + network: Network +} + +// ------------------------ QUOTATION ------------------------ + +export const PaymentRequestState = { + Idle: "Idle", + Loading: "Loading", + Created: "Created", + Error: "Error", + Paid: "Paid", + Expired: "Expired", +} as const +export type PaymentRequestStateType = typeof PaymentRequestState[keyof typeof PaymentRequestState] + +export type GeneratePaymentRequestMutations = { + lnNoAmountInvoiceCreate: LnNoAmountInvoiceCreateMutationHookResult["0"] + lnInvoiceCreate: LnInvoiceCreateMutationHookResult["0"] + lnUsdInvoiceCreate: LnUsdInvoiceCreateMutationHookResult["0"] + onChainAddressCurrent: OnChainAddressCurrentMutationHookResult["0"] +} + +/* Has immutable payment quotation from the server and handles payment state for itself (via hook) */ +export type PaymentRequest = { + state: PaymentRequestStateType + setState: (state: PaymentRequestStateType) => PaymentRequest + generateRequest: () => Promise + info?: PaymentRequestInformation + creationData: PaymentRequestCreationData +} + +export type PaymentRequestInformation = { + data: InvoiceData | undefined + applicationErrors: readonly GraphQlApplicationError[] | undefined + gqlErrors: readonly GraphQLError[] | undefined +} + +export type CreatePaymentRequestParams = { + creationData: PaymentRequestCreationData + mutations: GeneratePaymentRequestMutations + state?: PaymentRequestStateType + info?: PaymentRequestInformation +} diff --git a/app/screens/receive-bitcoin-screen/payment/payment-request-creation-data.ts b/app/screens/receive-bitcoin-screen/payment/payment-request-creation-data.ts new file mode 100644 index 0000000000..4db865f153 --- /dev/null +++ b/app/screens/receive-bitcoin-screen/payment/payment-request-creation-data.ts @@ -0,0 +1,107 @@ +import { WalletCurrency } from "@app/graphql/generated" +import { WalletDescriptor } from "@app/types/wallets" +import { MoneyAmount, WalletAmount, WalletOrDisplayCurrency } from "@app/types/amounts" + +import { + InvoiceType, + PaymentRequestCreationData, + BaseCreatePaymentRequestCreationDataParams, + ConvertMoneyAmountFn, +} from "./index.types" + +export const createPaymentRequestCreationData = ( + params: BaseCreatePaymentRequestCreationDataParams, +): PaymentRequestCreationData => { + // These sets are always available + const setType = (type: InvoiceType) => + createPaymentRequestCreationData({ ...params, type }) + const setDefaultWalletDescriptor = (defaultWalletDescriptor: WalletDescriptor) => + createPaymentRequestCreationData({ ...params, defaultWalletDescriptor }) + const setConvertMoneyAmount = (convertMoneyAmount: ConvertMoneyAmountFn) => + createPaymentRequestCreationData({ ...params, convertMoneyAmount }) + + const { type, defaultWalletDescriptor, convertMoneyAmount, memo } = params + + // Permissions for the specified type + const permissions = { + canSetReceivingWalletDescriptor: false, + canSetMemo: false, + canSetAmount: false, + } + switch (type) { + case "Lightning": + permissions.canSetReceivingWalletDescriptor = true + case "OnChain": + permissions.canSetMemo = true + permissions.canSetAmount = true + } + + // Permission based sets + let setReceivingWalletDescriptor: + | ((receivingWalletDescriptor: WalletDescriptor) => PaymentRequestCreationData) + | undefined = undefined + if (permissions.canSetReceivingWalletDescriptor) { + setReceivingWalletDescriptor = (receivingWalletDescriptor) => + createPaymentRequestCreationData({ ...params, receivingWalletDescriptor }) + } + let setMemo: ((memo: string) => PaymentRequestCreationData) | undefined = undefined + if (permissions.canSetMemo) { + setMemo = (memo) => createPaymentRequestCreationData({ ...params, memo }) + } + let setAmount: + | (( + unitOfAccountAmount: MoneyAmount, + ) => PaymentRequestCreationData) + | undefined = undefined + if (permissions.canSetAmount) { + setAmount = (unitOfAccountAmount) => + createPaymentRequestCreationData({ ...params, unitOfAccountAmount }) + } + + // Set default receiving wallet descriptor + let { receivingWalletDescriptor } = params + if (!receivingWalletDescriptor) { + receivingWalletDescriptor = defaultWalletDescriptor + } + + // OnChain only on BTC + if (type === "OnChain") { + receivingWalletDescriptor = params.bitcoinWalletDescriptor as WalletDescriptor + } + + // Paycode only in Default + if (type === "PayCode") { + receivingWalletDescriptor = params.defaultWalletDescriptor + } + + // Set settlement amount if unit of account amount is set + let { unitOfAccountAmount } = params + let settlementAmount: WalletAmount | undefined = undefined + if (unitOfAccountAmount) { + settlementAmount = convertMoneyAmount( + unitOfAccountAmount, + receivingWalletDescriptor.currency, + ) + } + + return { + ...params, + ...permissions, + setType, + setDefaultWalletDescriptor, + setConvertMoneyAmount, + receivingWalletDescriptor, + + // optional sets + setReceivingWalletDescriptor, + setMemo, + setAmount, + + // optional data + unitOfAccountAmount, + settlementAmount, + memo, + + canUsePaycode: Boolean(params.username), + } +} diff --git a/app/screens/receive-bitcoin-screen/payment/payment-request.ts b/app/screens/receive-bitcoin-screen/payment/payment-request.ts new file mode 100644 index 0000000000..2f9bfa4475 --- /dev/null +++ b/app/screens/receive-bitcoin-screen/payment/payment-request.ts @@ -0,0 +1,262 @@ +import { WalletCurrency } from "@app/graphql/generated" +import { + CreatePaymentRequestParams, + GetFullUriFn, + Invoice, + PaymentRequest, + PaymentRequestState, + PaymentRequestStateType, + PaymentRequestInformation, +} from "./index.types" +import { decodeInvoiceString, Network as NetworkLibGaloy } from "@galoymoney/client" +import { BtcMoneyAmount } from "@app/types/amounts" +import { getPaymentRequestFullUri } from "./helpers" +import { bech32 } from "bech32" + +export const createPaymentRequest = ( + params: CreatePaymentRequestParams, +): PaymentRequest => { + let { state, info } = params + if (!state) state = PaymentRequestState.Idle + + const setState = (state: PaymentRequestStateType) => { + if (state === PaymentRequestState.Loading) + return createPaymentRequest({ ...params, state, info: undefined }) + return createPaymentRequest({ ...params, state }) + } + + // The hook should setState(Loading) before calling this + const generateQuote: () => Promise = async () => { + const { creationData, mutations } = params + const pr = {...creationData} // clone creation data object + + let info: PaymentRequestInformation | undefined + + // Default memo + if (!pr.memo) pr.memo = "Pay to Blink Wallet User" + + // On Chain BTC + if (pr.type === Invoice.OnChain) { + const { data, errors } = await mutations.onChainAddressCurrent({ + variables: { input: { walletId: pr.receivingWalletDescriptor.id } }, + }) + + if (pr.settlementAmount && pr.settlementAmount.currency !== WalletCurrency.Btc) + throw new Error("On-chain invoices only support BTC") + + const address = data?.onChainAddressCurrent?.address || undefined + + const getFullUriFn: GetFullUriFn = ({ uppercase, prefix }) => + getPaymentRequestFullUri({ + type: Invoice.OnChain, + input: address || "", + amount: pr.settlementAmount?.amount, + memo: pr.memo, + uppercase, + prefix, + }) + + info = { + data: address + ? { + invoiceType: Invoice.OnChain, + getFullUriFn, + address, + amount: pr.settlementAmount as BtcMoneyAmount, + memo: pr.memo, + } + : undefined, + applicationErrors: data?.onChainAddressCurrent?.errors, + gqlErrors: errors, + } + + // Lightning without Amount (or zero-amount) + } else if ( + pr.type === Invoice.Lightning && + (pr.settlementAmount === undefined || pr.settlementAmount.amount === 0) + ) { + const { data, errors } = await mutations.lnNoAmountInvoiceCreate({ + variables: { + input: { + walletId: pr.receivingWalletDescriptor.id, + memo: pr.memo, + }, + }, + }) + + const dateString = decodeInvoiceString( + data?.lnNoAmountInvoiceCreate.invoice?.paymentRequest ?? "", + pr.network as NetworkLibGaloy, + ).timeExpireDateString + + const getFullUriFn: GetFullUriFn = ({ uppercase, prefix }) => + getPaymentRequestFullUri({ + type: Invoice.Lightning, + input: data?.lnNoAmountInvoiceCreate.invoice?.paymentRequest || "", + amount: pr.settlementAmount?.amount, + memo: pr.memo, + uppercase, + prefix, + }) + + info = { + data: data?.lnNoAmountInvoiceCreate.invoice + ? { + invoiceType: Invoice.Lightning, + ...data?.lnNoAmountInvoiceCreate.invoice, + expiresAt: dateString ? new Date(dateString) : undefined, + getFullUriFn, + } + : undefined, + applicationErrors: data?.lnNoAmountInvoiceCreate?.errors, + gqlErrors: errors, + } + + // Lightning with BTC Amount + } else if ( + pr.type === Invoice.Lightning && + pr.settlementAmount && + pr.settlementAmount?.currency === WalletCurrency.Btc + ) { + const { data, errors } = await mutations.lnInvoiceCreate({ + variables: { + input: { + walletId: pr.receivingWalletDescriptor.id, + amount: pr.settlementAmount.amount, + memo: pr.memo, + }, + }, + }) + + const dateString = decodeInvoiceString( + data?.lnInvoiceCreate.invoice?.paymentRequest ?? "", + pr.network as NetworkLibGaloy, + ).timeExpireDateString + + const getFullUriFn: GetFullUriFn = ({ uppercase, prefix }) => + getPaymentRequestFullUri({ + type: Invoice.Lightning, + input: data?.lnInvoiceCreate.invoice?.paymentRequest || "", + amount: pr.settlementAmount?.amount, + memo: pr.memo, + uppercase, + prefix, + }) + + info = { + data: data?.lnInvoiceCreate.invoice + ? { + invoiceType: Invoice.Lightning, + ...data?.lnInvoiceCreate.invoice, + expiresAt: dateString ? new Date(dateString) : undefined, + getFullUriFn, + } + : undefined, + applicationErrors: data?.lnInvoiceCreate?.errors, + gqlErrors: errors, + } + // Lightning with USD Amount + } else if ( + pr.type === Invoice.Lightning && + pr.settlementAmount && + pr.settlementAmount?.currency === WalletCurrency.Usd + ) { + const { data, errors } = await mutations.lnUsdInvoiceCreate({ + variables: { + input: { + walletId: pr.receivingWalletDescriptor.id, + amount: pr.settlementAmount.amount, + memo: pr.memo, + }, + }, + }) + + const dateString = decodeInvoiceString( + data?.lnUsdInvoiceCreate.invoice?.paymentRequest ?? "", + pr.network as NetworkLibGaloy, + ).timeExpireDateString + + const getFullUriFn: GetFullUriFn = ({ uppercase, prefix }) => + getPaymentRequestFullUri({ + type: Invoice.Lightning, + input: data?.lnUsdInvoiceCreate.invoice?.paymentRequest || "", + amount: pr.settlementAmount?.amount, + memo: pr.memo, + uppercase, + prefix, + }) + + info = { + data: data?.lnUsdInvoiceCreate.invoice + ? { + invoiceType: Invoice.Lightning, + ...data?.lnUsdInvoiceCreate.invoice, + expiresAt: dateString ? new Date(dateString) : undefined, + getFullUriFn, + } + : undefined, + applicationErrors: data?.lnUsdInvoiceCreate?.errors, + gqlErrors: errors, + } + + // Paycode + } else if (pr.type === Invoice.PayCode && pr.username) { + const lnurl = await new Promise((resolve) => { + resolve( + bech32.encode( + "lnurl", + bech32.toWords( + Buffer.from(`${pr.posUrl}/.well-known/lnurlp/${pr.username}`, "utf8"), + ), + 1500, + ), + ) + }) + + // To make the page render at loading state + // (otherwise jittery becoz encode takes ~10ms on slower phones) + await new Promise((r) => setTimeout(r, 500)) + + const webURL = `${pr.posUrl}/${pr.username}` + const qrCodeURL = (webURL + "?lightning=" + lnurl).toUpperCase() + + const getFullUriFn: GetFullUriFn = ({ uppercase, prefix }) => + getPaymentRequestFullUri({ + type: Invoice.PayCode, + input: qrCodeURL, + uppercase, + prefix, + }) + + info = { + data: { + invoiceType: Invoice.PayCode, + username: pr.username, + getFullUriFn, + }, + applicationErrors: undefined, + gqlErrors: undefined, + } + } else if (pr.type === Invoice.PayCode && !pr.username) { + // Can't create paycode payment request for a user with no username set so info will be empty + return createPaymentRequest({ + ...params, + state: PaymentRequestState.Created, + info: undefined, + }) + } else { + info = undefined + console.log(JSON.stringify({ pr }, null, 2)) + throw new Error("Unknown Payment Request Type Encountered - Please Report") + } + + let state: PaymentRequestStateType = PaymentRequestState.Created + if (!info || info.applicationErrors?.length || info.gqlErrors?.length || !info.data) { + state = PaymentRequestState.Error + } + + return createPaymentRequest({ ...params, info, state }) + } + + return { ...params, state, info, generateRequest: generateQuote, setState } +} diff --git a/app/screens/receive-bitcoin-screen/qr-view.tsx b/app/screens/receive-bitcoin-screen/qr-view.tsx index e9244fe6e8..b8395bde24 100644 --- a/app/screens/receive-bitcoin-screen/qr-view.tsx +++ b/app/screens/receive-bitcoin-screen/qr-view.tsx @@ -2,42 +2,45 @@ import * as React from "react" import { useMemo } from "react" import { ActivityIndicator, - Text, useWindowDimensions, View, Platform, + StyleProp, + ViewStyle, + Pressable, + Animated, + Easing, } from "react-native" import QRCode from "react-native-qrcode-svg" import Logo from "@app/assets/logo/blink-logo-icon.png" -import { - TYPE_LIGHTNING_BTC, - TYPE_BITCOIN_ONCHAIN, - TYPE_LIGHTNING_USD, -} from "./payment-requests/helpers" +import { Invoice, InvoiceType } from "./payment/index.types" import { testProps } from "../../utils/testProps" import { GaloyIcon } from "@app/components/atomic/galoy-icon" -import { GetFullUriFn } from "./payment-requests/index.types" +import { GetFullUriFn } from "./payment/index.types" import { SuccessIconAnimation } from "@app/components/success-animation" -import { makeStyles, useTheme } from "@rneui/themed" +import { makeStyles, Text, useTheme } from "@rneui/themed" +import { GaloyTertiaryButton } from "@app/components/atomic/galoy-tertiary-button" +import { useI18nContext } from "@app/i18n/i18n-react" const configByType = { - [TYPE_LIGHTNING_BTC]: { - copyToClipboardLabel: "ReceiveWrapperScreen.copyClipboard", + [Invoice.Lightning]: { + copyToClipboardLabel: "ReceiveScreen.copyClipboard", shareButtonLabel: "common.shareLightning", ecl: "L" as const, icon: "ios-flash", }, - [TYPE_LIGHTNING_USD]: { - copyToClipboardLabel: "ReceiveWrapperScreen.copyClipboard", - shareButtonLabel: "common.shareLightning", - ecl: "L" as const, - icon: "ios-flash", + [Invoice.OnChain]: { + copyToClipboardLabel: "ReceiveScreen.copyClipboardBitcoin", + shareButtonLabel: "common.shareBitcoin", + ecl: "M" as const, + icon: "logo-bitcoin", }, - [TYPE_BITCOIN_ONCHAIN]: { - copyToClipboardLabel: "ReceiveWrapperScreen.copyClipboardBitcoin", + // TODO: Add them + [Invoice.PayCode]: { + copyToClipboardLabel: "ReceiveScreen.copyClipboardBitcoin", shareButtonLabel: "common.shareBitcoin", ecl: "M" as const, icon: "logo-bitcoin", @@ -45,12 +48,19 @@ const configByType = { } type Props = { - type: GetFullUriInput["type"] + type: InvoiceType getFullUri: GetFullUriFn | undefined loading: boolean completed: boolean err: string size?: number + style?: StyleProp + expired: boolean + regenerateInvoiceFn?: () => void + copyToClipboard?: () => void | undefined + isPayCode: boolean + canUsePayCode: boolean + toggleIsSetLightningAddressModalVisible: () => void } export const QRView: React.FC = ({ @@ -59,19 +69,54 @@ export const QRView: React.FC = ({ loading, completed, err, - size = 320, + size = 240, + style, + expired, + regenerateInvoiceFn, + copyToClipboard, + isPayCode, + canUsePayCode, + toggleIsSetLightningAddressModalVisible, }) => { const { theme: { colors }, } = useTheme() - const styles = useStyles() + const isPayCodeAndCanUsePayCode = isPayCode && canUsePayCode + + const isReady = (!isPayCodeAndCanUsePayCode || Boolean(getFullUri)) && !loading && !err + const displayingQR = + !completed && isReady && !expired && (!isPayCode || isPayCodeAndCanUsePayCode) + + const styles = useStyles(displayingQR) const { scale } = useWindowDimensions() - const isReady = getFullUri && !loading && !err + + const { LL } = useI18nContext() + + const scaleAnim = React.useRef(new Animated.Value(1)).current + + const breatheIn = () => { + Animated.timing(scaleAnim, { + toValue: 0.95, + duration: 200, + useNativeDriver: true, + easing: Easing.inOut(Easing.quad), + }).start() + } + + const breatheOut = () => { + if (!expired && copyToClipboard) copyToClipboard() + Animated.timing(scaleAnim, { + toValue: 1, + duration: 200, + useNativeDriver: true, + easing: Easing.inOut(Easing.quad), + }).start() + } const renderSuccessView = useMemo(() => { if (completed) { return ( - + @@ -83,27 +128,28 @@ export const QRView: React.FC = ({ const renderQRCode = useMemo(() => { const getQrLogo = () => { - if (type === TYPE_LIGHTNING_BTC) return Logo - if (type === TYPE_LIGHTNING_USD) return Logo - if (type === TYPE_BITCOIN_ONCHAIN) return Logo + if (type === Invoice.OnChain) return Logo + if (type === Invoice.Lightning) return Logo + if (type === Invoice.PayCode) return Logo return null } const getQrSize = () => { if (Platform.OS === "android") { if (scale > 3) { - return 260 + return 195 } } return size } - if (!completed && isReady) { + if (displayingQR && getFullUri) { + const uri = getFullUri({ uppercase: true }) return ( - + = ({ ) } return null - }, [completed, isReady, type, getFullUri, size, scale, styles]) + }, [displayingQR, type, getFullUri, size, scale, styles]) const renderStatusView = useMemo(() => { if (!completed && !isReady) { return ( - + {(err !== "" && ( @@ -129,24 +175,65 @@ export const QRView: React.FC = ({ ) + } else if (expired) { + return ( + + + {LL.ReceiveScreen.invoiceHasExpired()} + + + + ) + } else if (isPayCode && !canUsePayCode) { + return ( + + + {LL.ReceiveScreen.setUsernameToAcceptViaPaycode()} + + + + ) } return null - }, [err, isReady, completed, styles, colors]) + }, [ + err, + isReady, + completed, + styles, + colors, + expired, + loading, + isPayCode, + canUsePayCode, + ]) return ( - {renderSuccessView} - {renderQRCode} - {renderStatusView} + {}} + onPressOut={displayingQR ? breatheOut : () => {}} + > + + {renderSuccessView} + {renderQRCode} + {renderStatusView} + + ) } -const useStyles = makeStyles(({ colors }) => ({ +const useStyles = makeStyles(({ colors }, displayingQR: boolean) => ({ container: { justifyContent: "center", alignItems: "center", - backgroundColor: colors._white, + backgroundColor: displayingQR ? colors._white : colors.background, width: "100%", height: undefined, borderRadius: 10, @@ -165,6 +252,15 @@ const useStyles = makeStyles(({ colors }) => ({ qr: { alignItems: "center", }, + expiredInvoice: { + marginBottom: 10, + }, + cantUsePayCode: { + padding: "10%", + }, + cantUsePayCodeText: { + marginBottom: 10, + }, })) export default React.memo(QRView) diff --git a/app/screens/receive-bitcoin-screen/receive-btc.tsx b/app/screens/receive-bitcoin-screen/receive-btc.tsx deleted file mode 100644 index 5cd7a3565c..0000000000 --- a/app/screens/receive-bitcoin-screen/receive-btc.tsx +++ /dev/null @@ -1,556 +0,0 @@ -import React, { useEffect, useMemo, useState } from "react" -import { Alert, Pressable, Share, TextInput, View } from "react-native" -import Icon from "react-native-vector-icons/Ionicons" - -import { gql } from "@apollo/client" -import CalculatorIcon from "@app/assets/icons/calculator.svg" -import ChainIcon from "@app/assets/icons-redesign/bitcoin.svg" -import ChevronIcon from "@app/assets/icons/chevron.svg" -import PencilIcon from "@app/assets/icons-redesign/pencil.svg" -import { useReceiveBtcQuery, WalletCurrency } from "@app/graphql/generated" -import { useAppConfig, usePriceConversion } from "@app/hooks" -import { useI18nContext } from "@app/i18n/i18n-react" -import { testProps } from "@app/utils/testProps" -import { toastShow } from "@app/utils/toast" -import Clipboard from "@react-native-clipboard/clipboard" -import crashlytics from "@react-native-firebase/crashlytics" - -import { AmountInputModal } from "@app/components/amount-input" -import { useIsAuthed } from "@app/graphql/is-authed-context" -import { useDisplayCurrency } from "@app/hooks/use-display-currency" -import { RootStackParamList } from "@app/navigation/stack-param-lists" -import { - DisplayCurrency, - isNonZeroMoneyAmount, - MoneyAmount, - WalletOrDisplayCurrency, -} from "@app/types/amounts" -import { useNavigation } from "@react-navigation/native" -import { StackNavigationProp } from "@react-navigation/stack" -import { makeStyles, Text, useTheme } from "@rneui/themed" -import ReactNativeHapticFeedback from "react-native-haptic-feedback" -import { PaymentRequest } from "./payment-requests/index.types" -import QRView from "./qr-view" -import { useReceiveBitcoin } from "./use-payment-request" -import { PaymentRequestState } from "./use-payment-request.types" -import { useLevel } from "@app/graphql/level-context" -import { UpgradeAccountModal } from "@app/components/upgrade-account-modal" -import { GaloyPrimaryButton } from "@app/components/atomic/galoy-primary-button" -import { getDefaultMemo } from "./payment-requests" -import { getBtcWallet } from "@app/graphql/wallets-utils" - -gql` - query receiveBtc { - globals { - network - feesInformation { - deposit { - minBankFee - minBankFeeThreshold - } - } - } - me { - id - defaultAccount { - id - wallets { - id - balance - walletCurrency - } - } - } - } -` - -const ReceiveBtc = () => { - const { formatDisplayAndWalletAmount, zeroDisplayAmount } = useDisplayCurrency() - - const { - theme: { colors }, - } = useTheme() - const styles = useStyles() - const { - appConfig: { - galoyInstance: { name: bankName }, - }, - } = useAppConfig() - - const { isAtLeastLevelOne } = useLevel() - const [showUpgradeModal, setShowUpgradeModal] = useState(false) - const closeUpgradeModal = () => setShowUpgradeModal(false) - const openUpgradeModal = () => setShowUpgradeModal(true) - const [showMemoInput, setShowMemoInput] = useState(false) - const [showAmountInput, setShowAmountInput] = useState(false) - const { - state, - paymentRequestDetails, - createPaymentRequestDetailsParams, - setCreatePaymentRequestDetailsParams, - paymentRequest, - setAmount, - setMemo, - generatePaymentRequest, - setPaymentRequestType, - } = useReceiveBitcoin({}) - - const { data } = useReceiveBtcQuery({ - fetchPolicy: "cache-first", - skip: !useIsAuthed(), - }) - const network = data?.globals?.network - const minBankFee = data?.globals?.feesInformation?.deposit?.minBankFee - const minBankFeeThreshold = data?.globals?.feesInformation?.deposit?.minBankFeeThreshold - - const btcWallet = getBtcWallet(data?.me?.defaultAccount?.wallets) - - const btcWalletId = btcWallet?.id - const { convertMoneyAmount: _convertMoneyAmount } = usePriceConversion() - const { LL } = useI18nContext() - const navigation = - useNavigation>() - - // initialize useReceiveBitcoin hook - useEffect(() => { - if ( - !createPaymentRequestDetailsParams && - network && - btcWalletId && - zeroDisplayAmount && - // TODO: improve readability on when this function is available - _convertMoneyAmount - ) { - setCreatePaymentRequestDetailsParams({ - params: { - bitcoinNetwork: network, - receivingWalletDescriptor: { - currency: WalletCurrency.Btc, - id: btcWalletId, - }, - memo: getDefaultMemo(bankName), - unitOfAccountAmount: zeroDisplayAmount, - convertMoneyAmount: _convertMoneyAmount, - paymentRequestType: PaymentRequest.Lightning, - }, - generatePaymentRequestAfter: true, - }) - } - }, [ - createPaymentRequestDetailsParams, - setCreatePaymentRequestDetailsParams, - network, - btcWalletId, - _convertMoneyAmount, - zeroDisplayAmount, - bankName, - ]) - - const { copyToClipboard, share } = useMemo(() => { - if (!paymentRequest) { - return {} - } - - const paymentFullUri = paymentRequest.getFullUri({}) - - const copyToClipboard = () => { - Clipboard.setString(paymentFullUri) - - toastShow({ - message: (translations) => - paymentRequest.paymentRequestData?.paymentRequestType === - PaymentRequest.Lightning - ? translations.ReceiveWrapperScreen.copyClipboard() - : translations.ReceiveWrapperScreen.copyClipboardBitcoin(), - currentTranslation: LL, - type: "success", - }) - } - - const share = async () => { - try { - const result = await Share.share({ message: paymentFullUri }) - - if (result.action === Share.sharedAction) { - if (result.activityType) { - // shared with activity type of result.activityType - } else { - // shared - } - } else if (result.action === Share.dismissedAction) { - // dismissed - } - } catch (err) { - if (err instanceof Error) { - crashlytics().recordError(err) - Alert.alert(err.message) - } - } - } - - return { - copyToClipboard, - share, - } - }, [paymentRequest, LL]) - - useEffect(() => { - if (state === PaymentRequestState.Paid) { - ReactNativeHapticFeedback.trigger("notificationSuccess", { - ignoreAndroidSystemSettings: true, - }) - } else if (state === PaymentRequestState.Error) { - ReactNativeHapticFeedback.trigger("notificationError", { - ignoreAndroidSystemSettings: true, - }) - } - }, [state]) - - if (!paymentRequestDetails || !setAmount) { - return <> - } - - const togglePaymentRequestType = () => { - const newPaymentRequestType = - paymentRequestDetails.paymentRequestType === PaymentRequest.Lightning - ? PaymentRequest.OnChain - : PaymentRequest.Lightning - setPaymentRequestType({ - paymentRequestType: newPaymentRequestType, - generatePaymentRequestAfter: true, - }) - } - - const { - unitOfAccountAmount, - settlementAmount, - convertMoneyAmount, - memo, - paymentRequestType, - } = paymentRequestDetails - - const onSetAmount = (amount: MoneyAmount) => { - setAmount({ amount, generatePaymentRequestAfter: true }) - setShowAmountInput(false) - } - const closeAmountInput = () => { - setShowAmountInput(false) - } - - if (showAmountInput && unitOfAccountAmount) { - return ( - - ) - } - - if (showMemoInput) { - return ( - - - {LL.SendBitcoinScreen.note()} - - - - setMemo({ - memo, - }) - } - {...testProps(LL.SendBitcoinScreen.note())} - value={memo} - multiline={true} - numberOfLines={3} - autoFocus - /> - - - { - setShowMemoInput(false) - generatePaymentRequest && generatePaymentRequest() - }} - disabled={!memo} - /> - - ) - } - - const OnChainCharge = - minBankFee && - minBankFeeThreshold && - paymentRequestDetails.paymentRequestType === PaymentRequest.OnChain ? ( - - - {LL.ReceiveWrapperScreen.fees({ minBankFee, minBankFeeThreshold })} - - - ) : undefined - - const AmountInfo = () => { - if (isNonZeroMoneyAmount(settlementAmount) && unitOfAccountAmount) { - return ( - - {formatDisplayAndWalletAmount({ - displayAmount: convertMoneyAmount(unitOfAccountAmount, DisplayCurrency), - walletAmount: settlementAmount, - })} - - ) - } - return ( - - {LL.ReceiveWrapperScreen.flexibleAmountInvoice()} - - ) - } - - return ( - <> - - - - - - - {state === PaymentRequestState.Created ? ( - <> - - - - - - {paymentRequestType === PaymentRequest.Lightning - ? LL.ReceiveWrapperScreen.copyInvoice() - : LL.ReceiveWrapperScreen.copyAddress()} - - - - - - - - - {paymentRequestType === PaymentRequest.Lightning - ? LL.ReceiveWrapperScreen.shareInvoice() - : LL.ReceiveWrapperScreen.shareAddress()} - - - - - ) : state === PaymentRequestState.Loading ? ( - - {`${LL.ReceiveWrapperScreen.generatingInvoice()}...`} - - ) : null} - - - {state === PaymentRequestState.Created && ( - <> - {AmountInfo()} - - {!showAmountInput && ( - - { - setShowAmountInput(true) - }} - > - - - - - - - {LL.ReceiveWrapperScreen.addAmount()} - - - - - - - - - )} - - {!showMemoInput && ( - - setShowMemoInput(true)}> - - - - - - - {LL.ReceiveWrapperScreen.setANote()} - - - - - - - - - )} - - - - - - - - - - {paymentRequestType === PaymentRequest.Lightning - ? LL.ReceiveWrapperScreen.useABitcoinOnchainAddress() - : LL.ReceiveWrapperScreen.useALightningInvoice()} - - - - - - - - - - {OnChainCharge} - - )} - {state === PaymentRequestState.Paid && ( - - - - )} - - - ) -} - -export default ReceiveBtc - -const useStyles = makeStyles(({ colors }) => ({ - container: { - marginTop: 14, - marginLeft: 20, - marginRight: 20, - }, - field: { - padding: 10, - backgroundColor: colors.grey5, - borderRadius: 10, - marginBottom: 12, - }, - inputForm: { - marginVertical: 20, - }, - copyInvoiceContainer: { - flex: 2, - marginLeft: 10, - }, - shareInvoiceContainer: { - flex: 2, - alignItems: "flex-end", - marginRight: 10, - }, - noteInput: { - color: colors.black, - }, - textContainer: { - flexDirection: "row", - justifyContent: "center", - marginTop: 6, - }, - optionsContainer: { - marginTop: 20, - }, - fieldContainer: { - flexDirection: "row", - alignItems: "center", - }, - fieldIconContainer: { - justifyContent: "center", - marginRight: 10, - minWidth: 24, - }, - fieldTextContainer: { - flex: 4, - justifyContent: "center", - }, - fieldArrowContainer: { - flex: 1, - justifyContent: "center", - alignItems: "flex-end", - }, - fieldText: { - fontSize: 14, - }, - button: { - height: 60, - borderRadius: 10, - marginTop: 40, - }, - invoiceInfo: { - display: "flex", - flexDirection: "row", - justifyContent: "center", - marginTop: 14, - }, - feeInfoView: { - justifyContent: "center", - alignItems: "center", - marginVertical: 14, - }, - feeInfoText: { - textAlign: "center", - }, - fieldTitleText: { - marginBottom: 5, - }, - primaryAmount: { - fontWeight: "bold", - color: colors.black, - }, -})) diff --git a/app/screens/receive-bitcoin-screen/receive-wrapper.stories.tsx b/app/screens/receive-bitcoin-screen/receive-screen.stories.tsx similarity index 82% rename from app/screens/receive-bitcoin-screen/receive-wrapper.stories.tsx rename to app/screens/receive-bitcoin-screen/receive-screen.stories.tsx index 056f4ddea2..3d2e9c9760 100644 --- a/app/screens/receive-bitcoin-screen/receive-wrapper.stories.tsx +++ b/app/screens/receive-bitcoin-screen/receive-screen.stories.tsx @@ -5,11 +5,11 @@ import { StoryScreen } from "../../../.storybook/views" import { createCache } from "../../graphql/cache" import { IsAuthedContextProvider } from "../../graphql/is-authed-context" import mocks from "../../graphql/mocks" -import ReceiveWrapperScreen from "./receive-wrapper" +import ReceiveScreen from "./receive-screen" export default { title: "Receive", - component: ReceiveWrapperScreen, + component: ReceiveScreen, decorators: [ (Story) => ( @@ -17,11 +17,11 @@ export default { ), ], -} as Meta +} as Meta export const Default = () => ( - + ) diff --git a/app/screens/receive-bitcoin-screen/receive-screen.tsx b/app/screens/receive-bitcoin-screen/receive-screen.tsx new file mode 100644 index 0000000000..05e163c343 --- /dev/null +++ b/app/screens/receive-bitcoin-screen/receive-screen.tsx @@ -0,0 +1,286 @@ +import { Screen } from "@app/components/screen" +import { WalletCurrency } from "@app/graphql/generated" +import { useIsAuthed } from "@app/graphql/is-authed-context" +import { useI18nContext } from "@app/i18n/i18n-react" +import { requestNotificationPermission } from "@app/utils/notifications" +import { useIsFocused, useNavigation } from "@react-navigation/native" +import React, { useEffect } from "react" +import { TouchableOpacity, View } from "react-native" +import { testProps } from "../../utils/testProps" +import { withMyLnUpdateSub } from "./my-ln-updates-sub" +import { makeStyles, Text, useTheme } from "@rneui/themed" +import { ButtonGroup } from "@app/components/button-group" +import { useReceiveBitcoin } from "./use-receive-bitcoin" +import { Invoice, InvoiceType, PaymentRequestState } from "./payment/index.types" +import { QRView } from "./qr-view" +import { AmountInput } from "@app/components/amount-input" +import { NoteInput } from "@app/components/note-input" +import Icon from "react-native-vector-icons/Ionicons" +import { SetLightningAddressModal } from "@app/components/set-lightning-address-modal" +import { GaloyCurrencyBubble } from "@app/components/atomic/galoy-currency-bubble" + +const ReceiveScreen = () => { + const { + theme: { colors }, + } = useTheme() + const styles = useStyles() + const { LL } = useI18nContext() + const navigation = useNavigation() + + const isAuthed = useIsAuthed() + const isFocused = useIsFocused() + + const request = useReceiveBitcoin() + + // notification permission + useEffect(() => { + let timeout: NodeJS.Timeout + if (isAuthed && isFocused) { + const WAIT_TIME_TO_PROMPT_USER = 5000 + timeout = setTimeout( + requestNotificationPermission, // no op if already requested + WAIT_TIME_TO_PROMPT_USER, + ) + } + return () => timeout && clearTimeout(timeout) + }, [isAuthed, isFocused]) + + useEffect(() => { + switch (request?.type) { + case Invoice.OnChain: + navigation.setOptions({ title: LL.ReceiveScreen.receiveViaOnchain() }) + break + case Invoice.Lightning: + navigation.setOptions({ title: LL.ReceiveScreen.receiveViaInvoice() }) + break + case Invoice.PayCode: + navigation.setOptions({ title: LL.ReceiveScreen.receiveViaPaycode() }) + } + }, [request?.type]) + + useEffect(() => { + if (request?.state === PaymentRequestState.Paid) { + const id = setTimeout(() => navigation.goBack(), 5000) + return () => clearTimeout(id) + } + }, [request?.state]) + + if (!request) return <> + + const OnChainCharge = + request.feesInformation?.deposit.minBankFee && + request.feesInformation?.deposit.minBankFeeThreshold && + request.type === Invoice.OnChain ? ( + + + {LL.ReceiveScreen.fees({ + minBankFee: request.feesInformation?.deposit.minBankFee, + minBankFeeThreshold: request.feesInformation?.deposit.minBankFeeThreshold, + })} + + + ) : undefined + + return ( + <> + + , + }, + { + id: WalletCurrency.Usd, + text: LL.ReceiveScreen.stablesats(), + icon: , + }, + ]} + onPress={(id) => request.setReceivingWallet(id as WalletCurrency)} + style={styles.receivingWalletPicker} + disabled={!request.canSetReceivingWalletDescriptor} + /> + + + + + {request.state !== PaymentRequestState.Loading && ( + <> + + + + + + {LL.ReceiveScreen.copyInvoice()} + + + + + + {request.readablePaymentRequest} + + + + + + + + {LL.ReceiveScreen.shareInvoice()} + + + + + )} + + + + {request.extraDetails} + + + request.setType(id as InvoiceType)} + style={styles.invoiceTypePicker} + /> + + + + {OnChainCharge} + + + + + ) +} + +const useStyles = makeStyles(({ colors }) => ({ + screenStyle: { + paddingHorizontal: 16, + paddingBottom: 12, + flexGrow: 1, + }, + tabRow: { + flexDirection: "row", + flexWrap: "nowrap", + justifyContent: "center", + marginTop: 12, + }, + usdActive: { + backgroundColor: colors.green, + borderRadius: 7, + justifyContent: "center", + alignItems: "center", + width: 150, + height: 30, + margin: 5, + }, + btcActive: { + backgroundColor: colors.primary, + borderRadius: 7, + justifyContent: "center", + alignItems: "center", + width: 150, + height: 30, + margin: 5, + }, + inactiveTab: { + backgroundColor: colors.grey3, + borderRadius: 7, + justifyContent: "center", + alignItems: "center", + width: 150, + height: 30, + margin: 5, + }, + qrView: { + marginBottom: 10, + }, + receivingWalletPicker: { + marginBottom: 10, + }, + invoiceTypePicker: { + marginBottom: 10, + }, + note: { + marginTop: 10, + }, + extraDetails: { + flexDirection: "row", + justifyContent: "center", + alignItems: "center", + marginBottom: 15, + height: 16, + }, + invoiceActions: { + flexDirection: "row", + justifyContent: "center", + marginBottom: 10, + height: 16, + }, + copyInvoiceContainer: { + flex: 2, + marginLeft: 10, + }, + shareInvoiceContainer: { + flex: 2, + alignItems: "flex-end", + marginRight: 10, + }, +})) + +export default withMyLnUpdateSub(ReceiveScreen) diff --git a/app/screens/receive-bitcoin-screen/receive-usd.tsx b/app/screens/receive-bitcoin-screen/receive-usd.tsx deleted file mode 100644 index 7316e14f71..0000000000 --- a/app/screens/receive-bitcoin-screen/receive-usd.tsx +++ /dev/null @@ -1,567 +0,0 @@ -import React, { useEffect, useMemo, useState } from "react" -import { Alert, Pressable, Share, TextInput, View } from "react-native" -import Icon from "react-native-vector-icons/Ionicons" - -import { gql } from "@apollo/client" -import CalculatorIcon from "@app/assets/icons/calculator.svg" -import ChevronIcon from "@app/assets/icons/chevron.svg" -import PencilIcon from "@app/assets/icons-redesign/pencil.svg" -import { useReceiveUsdQuery, WalletCurrency } from "@app/graphql/generated" -import { useAppConfig, usePriceConversion } from "@app/hooks" -import { useI18nContext } from "@app/i18n/i18n-react" -import { - getDefaultMemo, - TYPE_LIGHTNING_USD, -} from "@app/screens/receive-bitcoin-screen/payment-requests/helpers" -import { testProps } from "@app/utils/testProps" -import { toastShow } from "@app/utils/toast" -import Clipboard from "@react-native-clipboard/clipboard" -import crashlytics from "@react-native-firebase/crashlytics" - -import { useIsAuthed } from "@app/graphql/is-authed-context" -import { useDisplayCurrency } from "@app/hooks/use-display-currency" -import { TranslationFunctions } from "@app/i18n/i18n-types" -import { RootStackParamList } from "@app/navigation/stack-param-lists" -import { - DisplayCurrency, - isNonZeroMoneyAmount, - MoneyAmount, - WalletOrDisplayCurrency, -} from "@app/types/amounts" -import { useNavigation } from "@react-navigation/native" -import { StackNavigationProp } from "@react-navigation/stack" -import { makeStyles, Text, useTheme } from "@rneui/themed" -import ReactNativeHapticFeedback from "react-native-haptic-feedback" -import { AmountInputModal } from "@app/components/amount-input" -import { PaymentRequest } from "./payment-requests/index.types" -import QRView from "./qr-view" -import { useReceiveBitcoin } from "./use-payment-request" -import { PaymentRequestState } from "./use-payment-request.types" -import { GaloyPrimaryButton } from "@app/components/atomic/galoy-primary-button" -import { getUsdWallet } from "@app/graphql/wallets-utils" - -gql` - query receiveUsd { - globals { - network - } - me { - id - defaultAccount { - id - wallets { - id - balance - walletCurrency - } - } - } - } -` - -const ReceiveUsd = () => { - const { formatDisplayAndWalletAmount, zeroDisplayAmount } = useDisplayCurrency() - - const { - theme: { colors }, - } = useTheme() - const styles = useStyles() - const { - appConfig: { - galoyInstance: { name: bankName }, - }, - } = useAppConfig() - - const [showMemoInput, setShowMemoInput] = useState(false) - const [showAmountInput, setShowAmountInput] = useState(false) - const { data } = useReceiveUsdQuery({ skip: !useIsAuthed() }) - - const usdWallet = getUsdWallet(data?.me?.defaultAccount?.wallets) - const usdWalletId = usdWallet?.id - - const network = data?.globals?.network - const { - state, - paymentRequestDetails, - createPaymentRequestDetailsParams, - setCreatePaymentRequestDetailsParams, - paymentRequest, - setAmount, - setMemo, - generatePaymentRequest, - checkExpiredAndGetRemainingSeconds, - } = useReceiveBitcoin({}) - - const { LL } = useI18nContext() - const { convertMoneyAmount: _convertMoneyAmount } = usePriceConversion() - const navigation = - useNavigation>() - - // initialize useReceiveBitcoin hook - useEffect(() => { - if ( - !createPaymentRequestDetailsParams && - network && - usdWalletId && - _convertMoneyAmount && - zeroDisplayAmount - ) { - setCreatePaymentRequestDetailsParams({ - params: { - bitcoinNetwork: network, - receivingWalletDescriptor: { - currency: WalletCurrency.Usd, - id: usdWalletId, - }, - memo: getDefaultMemo(bankName), - unitOfAccountAmount: zeroDisplayAmount, - convertMoneyAmount: _convertMoneyAmount, - paymentRequestType: PaymentRequest.Lightning, - }, - generatePaymentRequestAfter: true, - }) - } - }, [ - createPaymentRequestDetailsParams, - setCreatePaymentRequestDetailsParams, - network, - usdWalletId, - _convertMoneyAmount, - zeroDisplayAmount, - bankName, - ]) - - const { copyToClipboard, share } = useMemo(() => { - if (!paymentRequest) { - return {} - } - - const paymentFullUri = paymentRequest.getFullUri({}) - - const copyToClipboard = () => { - Clipboard.setString(paymentFullUri) - - toastShow({ - message: (translations) => translations.ReceiveWrapperScreen.copyClipboard(), - currentTranslation: LL, - type: "success", - }) - } - - const share = async () => { - try { - const result = await Share.share({ message: paymentFullUri }) - - if (result.action === Share.sharedAction) { - if (result.activityType) { - // shared with activity type of result.activityType - } else { - // shared - } - } else if (result.action === Share.dismissedAction) { - // dismissed - } - } catch (err) { - if (err instanceof Error) { - crashlytics().recordError(err) - Alert.alert(err.message) - } - } - } - - return { - copyToClipboard, - share, - } - }, [paymentRequest, LL]) - - useEffect(() => { - if (state === PaymentRequestState.Paid) { - ReactNativeHapticFeedback.trigger("notificationSuccess", { - ignoreAndroidSystemSettings: true, - }) - } else if (state === PaymentRequestState.Error) { - ReactNativeHapticFeedback.trigger("notificationError", { - ignoreAndroidSystemSettings: true, - }) - } - }, [state]) - - if (!paymentRequestDetails || !setAmount) { - return <> - } - - const { unitOfAccountAmount, memo, convertMoneyAmount, settlementAmount } = - paymentRequestDetails - - const onSetAmount = (amount: MoneyAmount) => { - setAmount({ amount, generatePaymentRequestAfter: true }) - setShowAmountInput(false) - } - const closeAmountInput = () => { - setShowAmountInput(false) - } - - if (showAmountInput && unitOfAccountAmount) { - return ( - - ) - } - - if (showMemoInput) { - return ( - - - - {LL.SendBitcoinScreen.note()} - - - - setMemo({ - memo, - }) - } - value={memo} - multiline={true} - numberOfLines={3} - autoFocus - /> - - - { - setShowMemoInput(false) - generatePaymentRequest && generatePaymentRequest() - }} - /> - - - ) - } - - const amountInfo = () => { - if (isNonZeroMoneyAmount(settlementAmount) && unitOfAccountAmount) { - return ( - <> - - {formatDisplayAndWalletAmount({ - displayAmount: convertMoneyAmount(unitOfAccountAmount, DisplayCurrency), - walletAmount: settlementAmount, - })} - - - ) - } - return ( - - {LL.ReceiveWrapperScreen.flexibleAmountInvoice()} - - ) - } - - let errorMessage = "" - if (state === PaymentRequestState.Expired) { - errorMessage = LL.ReceiveWrapperScreen.expired() - } else if (state === PaymentRequestState.Error) { - errorMessage = LL.ReceiveWrapperScreen.error() - } - - return ( - - {state !== PaymentRequestState.Expired && ( - <> - - - - - <> - - {state === PaymentRequestState.Loading || - !share || - (!copyToClipboard && ( - - {LL.ReceiveWrapperScreen.generatingInvoice()} - - ))} - {state === PaymentRequestState.Created && ( - <> - - - - - - {LL.ReceiveWrapperScreen.copyInvoice()} - - - - - - - - - {LL.ReceiveWrapperScreen.shareInvoice()} - - - - - )} - - - {state === PaymentRequestState.Created && ( - {amountInfo()} - )} - - - )} - - {state === PaymentRequestState.Expired ? ( - - - {LL.ReceiveWrapperScreen.expired()} - - { - generatePaymentRequest && generatePaymentRequest() - }} - /> - - ) : ( - <> - )} - - {state === PaymentRequestState.Created && ( - <> - - {!showAmountInput && ( - - { - setShowAmountInput(true) - }} - > - - - - - - - {LL.ReceiveWrapperScreen.addAmount()} - - - - - - - - - )} - - {!showMemoInput && ( - - setShowMemoInput(true)}> - - - - - - - {LL.ReceiveWrapperScreen.setANote()} - - - - - - - - - )} - - - - )} - {state === PaymentRequestState.Paid && ( - - - - )} - - ) -} - -type TimeInformationParams = { - checkExpiredAndGetRemainingSeconds: - | ((currentTime: Date) => number | undefined) - | undefined - LL: TranslationFunctions -} - -const TimeInformation = ({ - checkExpiredAndGetRemainingSeconds, - LL, -}: TimeInformationParams) => { - const styles = useStyles() - const [timeLeft, setTimeLeft] = useState(undefined) - - // update time left every second - useEffect(() => { - const updateTimeLeft = () => { - const newTimeLeft = - checkExpiredAndGetRemainingSeconds && - checkExpiredAndGetRemainingSeconds(new Date()) - if (newTimeLeft !== timeLeft) { - setTimeLeft(newTimeLeft) - } - } - const interval = setInterval(() => { - updateTimeLeft() - }, 1000) - updateTimeLeft() - return () => clearInterval(interval) - }, [checkExpiredAndGetRemainingSeconds, setTimeLeft, timeLeft]) - - const hourInSeconds = 60 * 60 - if (typeof timeLeft !== "number" || timeLeft > hourInSeconds) { - return <> - } - - const date = new Date(timeLeft * 1000) - const minutes = date.getUTCMinutes() - const secondsRaw = date.getUTCSeconds() - const seconds = secondsRaw < 10 ? `0${secondsRaw}` : secondsRaw - - return ( - - - {LL.ReceiveWrapperScreen.expiresIn()}:{" "} - - - {`${minutes}:${seconds}`} - - - ) -} - -export default ReceiveUsd - -const useStyles = makeStyles(({ colors }) => ({ - container: { - marginTop: 14, - marginLeft: 20, - marginRight: 20, - }, - field: { - padding: 10, - backgroundColor: colors.grey5, - borderRadius: 10, - marginBottom: 12, - }, - inputForm: { - marginVertical: 20, - }, - invoiceInfo: { - display: "flex", - flexDirection: "row", - justifyContent: "center", - marginTop: 14, - }, - invoiceExpired: { - marginTop: 25, - }, - noteInput: { - color: colors.black, - }, - invoiceExpiredMessage: { - color: colors.error, - fontSize: 20, - textAlign: "center", - }, - copyInvoiceContainer: { - flex: 2, - marginLeft: 10, - }, - shareInvoiceContainer: { - flex: 2, - alignItems: "flex-end", - marginRight: 10, - }, - textContainer: { - flexDirection: "row", - justifyContent: "center", - marginTop: 6, - }, - optionsContainer: { - marginTop: 20, - }, - fieldContainer: { - flexDirection: "row", - justifyContent: "center", - }, - fieldIconContainer: { - justifyContent: "center", - marginRight: 10, - minWidth: 24, - }, - fieldTextContainer: { - flex: 4, - justifyContent: "center", - }, - fieldArrowContainer: { - flex: 1, - justifyContent: "center", - alignItems: "flex-end", - }, - fieldText: { - fontSize: 14, - }, - primaryAmount: { - fontWeight: "bold", - color: colors.black, - }, - fieldTitleText: { - marginBottom: 5, - }, - lowTimer: { - color: colors.warning, - }, - countdownTimer: { - flexDirection: "row", - justifyContent: "center", - alignItems: "stretch", // - }, - timer: { - minWidth: 40, - }, -})) diff --git a/app/screens/receive-bitcoin-screen/receive-wrapper-screen.types.d.ts b/app/screens/receive-bitcoin-screen/receive-wrapper-screen.types.d.ts deleted file mode 100644 index a5ded2aaf1..0000000000 --- a/app/screens/receive-bitcoin-screen/receive-wrapper-screen.types.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -type GetFullUriInput = { - input: string - amount?: number - memo?: string - uppercase?: boolean - prefix?: boolean - type?: "BITCOIN_ONCHAIN" | "LIGHTNING_BTC" | "LIGHTNING_USD" -} diff --git a/app/screens/receive-bitcoin-screen/receive-wrapper.tsx b/app/screens/receive-bitcoin-screen/receive-wrapper.tsx deleted file mode 100644 index f04b6e720d..0000000000 --- a/app/screens/receive-bitcoin-screen/receive-wrapper.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import { gql } from "@apollo/client" -import { Screen } from "@app/components/screen" -import { - useRealtimePriceQuery, - useReceiveWrapperScreenQuery, - WalletCurrency, -} from "@app/graphql/generated" -import { useIsAuthed } from "@app/graphql/is-authed-context" -import { useI18nContext } from "@app/i18n/i18n-react" -import { requestNotificationPermission } from "@app/utils/notifications" -import { useIsFocused, useNavigation } from "@react-navigation/native" -import React, { useEffect, useState } from "react" -import { View } from "react-native" -import { TouchableWithoutFeedback } from "react-native-gesture-handler" -import { testProps } from "../../utils/testProps" -import { MyLnUpdateSub } from "./my-ln-updates-sub" -import ReceiveBtc from "./receive-btc" -import ReceiveUsd from "./receive-usd" -import { makeStyles, Text, useTheme } from "@rneui/themed" -import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view" -import { getDefaultWallet } from "@app/graphql/wallets-utils" - -const useStyles = makeStyles(({ colors }) => ({ - container: { - flexDirection: "column", - }, - tabRow: { - flexDirection: "row", - flexWrap: "nowrap", - justifyContent: "center", - marginTop: 12, - }, - usdActive: { - backgroundColor: colors.green, - borderRadius: 7, - justifyContent: "center", - alignItems: "center", - width: 150, - height: 30, - margin: 5, - }, - btcActive: { - backgroundColor: colors.primary, - borderRadius: 7, - justifyContent: "center", - alignItems: "center", - width: 150, - height: 30, - margin: 5, - }, - inactiveTab: { - backgroundColor: colors.grey3, - borderRadius: 7, - justifyContent: "center", - alignItems: "center", - width: 150, - height: 30, - margin: 5, - }, -})) - -gql` - query receiveWrapperScreen { - me { - id - defaultAccount { - id - wallets { - id - balance - walletCurrency - } - defaultWalletId - } - } - } -` - -const ReceiveWrapperScreen = () => { - const styles = useStyles() - const { - theme: { colors }, - } = useTheme() - - const navigation = useNavigation() - - const isAuthed = useIsAuthed() - - const { data } = useReceiveWrapperScreenQuery({ - fetchPolicy: "cache-first", - skip: !isAuthed, - }) - - // forcing price refresh - useRealtimePriceQuery({ - fetchPolicy: "network-only", - skip: !isAuthed, - }) - - const defaultCurrency = getDefaultWallet( - data?.me?.defaultAccount?.wallets, - data?.me?.defaultAccount?.defaultWalletId, - )?.walletCurrency - - const [receiveCurrency, setReceiveCurrency] = useState( - defaultCurrency || WalletCurrency.Btc, - ) - - const { LL } = useI18nContext() - const isFocused = useIsFocused() - - useEffect(() => { - let timeout: NodeJS.Timeout - if (isAuthed && isFocused) { - const WAIT_TIME_TO_PROMPT_USER = 5000 - timeout = setTimeout( - requestNotificationPermission, // no op if already requested - WAIT_TIME_TO_PROMPT_USER, - ) - } - - return () => timeout && clearTimeout(timeout) - }, [isAuthed, isFocused]) - - useEffect(() => { - if (receiveCurrency === WalletCurrency.Usd) { - navigation.setOptions({ title: LL.ReceiveWrapperScreen.usdTitle() }) - } - - if (receiveCurrency === WalletCurrency.Btc) { - navigation.setOptions({ title: LL.ReceiveWrapperScreen.title() }) - } - }, [receiveCurrency, navigation, LL]) - - return ( - - - - - setReceiveCurrency(WalletCurrency.Btc)} - > - - - BTC - - - - setReceiveCurrency(WalletCurrency.Usd)} - > - - - USD - - - - - {receiveCurrency === WalletCurrency.Usd && } - {receiveCurrency === WalletCurrency.Btc && } - - - - ) -} - -export default ReceiveWrapperScreen diff --git a/app/screens/receive-bitcoin-screen/use-payment-request.ts b/app/screens/receive-bitcoin-screen/use-payment-request.ts deleted file mode 100644 index 2d894b4e5b..0000000000 --- a/app/screens/receive-bitcoin-screen/use-payment-request.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { gql } from "@apollo/client" -import { - useLnInvoiceCreateMutation, - useLnNoAmountInvoiceCreateMutation, - useLnUsdInvoiceCreateMutation, - useOnChainAddressCurrentMutation, - WalletCurrency, -} from "@app/graphql/generated" -import { useLnUpdateHashPaid } from "@app/graphql/ln-update-context" -import { logGeneratePaymentRequest } from "@app/utils/analytics" -import { useCallback, useEffect, useMemo, useState } from "react" -import { - createPaymentRequestDetails, - CreatePaymentRequestDetailsParams, -} from "./payment-requests" -import { PaymentRequest } from "./payment-requests/index.types" -import { - ErrorType, - UsePaymentRequestState, - UsePaymentRequestParams, - UsePaymentRequestResult, - PaymentRequestState, - SetCreatePaymentRequestDetailsParamsParams, - SetAmountParams, - SetMemoParams, - SetReceivingWalletDescriptorParams, - SetPaymentRequestTypeParams, - SetConvertMoneyAmountParams, -} from "./use-payment-request.types" - -gql` - mutation lnNoAmountInvoiceCreate($input: LnNoAmountInvoiceCreateInput!) { - lnNoAmountInvoiceCreate(input: $input) { - errors { - message - } - invoice { - paymentHash - paymentRequest - paymentSecret - } - } - } - - mutation lnInvoiceCreate($input: LnInvoiceCreateInput!) { - lnInvoiceCreate(input: $input) { - errors { - message - } - invoice { - paymentHash - paymentRequest - paymentSecret - satoshis - } - } - } - - mutation onChainAddressCurrent($input: OnChainAddressCurrentInput!) { - onChainAddressCurrent(input: $input) { - errors { - message - } - address - } - } - - mutation lnUsdInvoiceCreate($input: LnUsdInvoiceCreateInput!) { - lnUsdInvoiceCreate(input: $input) { - errors { - message - } - invoice { - paymentHash - paymentRequest - paymentSecret - satoshis - } - } - } -` - -export const useReceiveBitcoin = ({ - initialCreatePaymentRequestDetailsParams, -}: UsePaymentRequestParams): UsePaymentRequestResult => { - const [lnNoAmountInvoiceCreate] = useLnNoAmountInvoiceCreateMutation() - const [lnUsdInvoiceCreate] = useLnUsdInvoiceCreateMutation() - const [lnInvoiceCreate] = useLnInvoiceCreateMutation() - const [onChainAddressCurrent] = useOnChainAddressCurrentMutation() - const lastHash = useLnUpdateHashPaid() - const [state, setState] = useState>({ - createPaymentRequestDetailsParams: initialCreatePaymentRequestDetailsParams, - paymentRequestDetails: - initialCreatePaymentRequestDetailsParams && - createPaymentRequestDetails(initialCreatePaymentRequestDetailsParams), - state: PaymentRequestState.Idle, - }) - - const generatePaymentRequestWithParams = useCallback( - async ( - createPaymentRequestDetailsParams: CreatePaymentRequestDetailsParams, - ) => { - const paymentRequestDetails = createPaymentRequestDetails( - createPaymentRequestDetailsParams, - ) - setState({ - paymentRequestDetails, - createPaymentRequestDetailsParams, - state: PaymentRequestState.Loading, - }) - - const { paymentRequest, gqlErrors, applicationErrors } = - await paymentRequestDetails.generatePaymentRequest({ - lnInvoiceCreate, - lnNoAmountInvoiceCreate, - lnUsdInvoiceCreate, - onChainAddressCurrent, - }) - logGeneratePaymentRequest({ - paymentType: createPaymentRequestDetailsParams.paymentRequestType, - hasAmount: Boolean(paymentRequestDetails.unitOfAccountAmount?.amount), - receivingWallet: paymentRequestDetails.receivingWalletDescriptor.currency, - }) - if (gqlErrors.length || applicationErrors.length || !paymentRequest) { - return setState({ - paymentRequestDetails, - createPaymentRequestDetailsParams, - state: PaymentRequestState.Error, - error: ErrorType.Generic, - }) - } - - return setState({ - paymentRequestDetails, - createPaymentRequestDetailsParams, - state: PaymentRequestState.Created, - paymentRequest, - }) - }, - [ - setState, - lnInvoiceCreate, - lnNoAmountInvoiceCreate, - lnUsdInvoiceCreate, - onChainAddressCurrent, - ], - ) - - const { createPaymentRequestDetailsParams, state: paymentRequestState } = state - - const setterMethods = useMemo(() => { - const setCreatePaymentRequestDetailsParams = ({ - params, - generatePaymentRequestAfter = false, - }: SetCreatePaymentRequestDetailsParamsParams) => { - if (generatePaymentRequestAfter) { - return generatePaymentRequestWithParams(params) - } - return setState({ - state: PaymentRequestState.Idle, - createPaymentRequestDetailsParams: params, - paymentRequestDetails: createPaymentRequestDetails(params), - }) - } - - if (!createPaymentRequestDetailsParams) { - return { - setCreatePaymentRequestDetailsParams, - } as const - } - - const createSetterMethod = < - T extends keyof CreatePaymentRequestDetailsParams, - >({ - field, - value, - generatePaymentRequestAfter = false, - }: { - field: T - value: CreatePaymentRequestDetailsParams[T] - generatePaymentRequestAfter: boolean - }) => { - const newParams: CreatePaymentRequestDetailsParams = { - ...createPaymentRequestDetailsParams, - [field]: value, - } - - setCreatePaymentRequestDetailsParams({ - params: newParams, - generatePaymentRequestAfter, - }) - } - - return { - setCreatePaymentRequestDetailsParams, - setAmount: ({ amount, generatePaymentRequestAfter = false }: SetAmountParams) => - createSetterMethod({ - field: "unitOfAccountAmount", - value: amount, - generatePaymentRequestAfter, - }), - - setMemo: ({ memo, generatePaymentRequestAfter = false }: SetMemoParams) => - createSetterMethod({ - field: "memo", - value: memo, - generatePaymentRequestAfter, - }), - - setReceivingWalletDescriptor: ({ - receivingWalletDescriptor, - generatePaymentRequestAfter = false, - }: SetReceivingWalletDescriptorParams) => - createSetterMethod({ - field: "receivingWalletDescriptor", - value: receivingWalletDescriptor, - generatePaymentRequestAfter, - }), - - setPaymentRequestType: ({ - paymentRequestType, - generatePaymentRequestAfter = false, - }: SetPaymentRequestTypeParams) => - createSetterMethod({ - field: "paymentRequestType", - value: paymentRequestType, - generatePaymentRequestAfter, - }), - - generatePaymentRequest: () => - generatePaymentRequestWithParams(createPaymentRequestDetailsParams), - - setConvertMoneyAmount: ({ - convertMoneyAmount, - generatePaymentRequestAfter = false, - }: SetConvertMoneyAmountParams) => - createSetterMethod({ - field: "convertMoneyAmount", - value: convertMoneyAmount, - generatePaymentRequestAfter, - }), - } as const - }, [createPaymentRequestDetailsParams, generatePaymentRequestWithParams]) - - const checkExpiredAndGetRemainingSeconds = - paymentRequestState === PaymentRequestState.Created - ? (currentTime: Date): number | undefined => { - const expiration = state.paymentRequest?.expiration - if (expiration) { - const secondsRemaining = Math.floor( - (expiration.getTime() - currentTime.getTime()) / 1000, - ) - if (secondsRemaining < 0) { - setState({ - ...state, - state: PaymentRequestState.Expired, - }) - } - return secondsRemaining - } - return undefined - } - : undefined - - // Check if paymentRequest has been paid - useEffect(() => { - const paymentRequestData = state.paymentRequest?.paymentRequestData - if ( - paymentRequestData && - paymentRequestData.paymentRequestType === PaymentRequest.Lightning && - state.state === PaymentRequestState.Created && - lastHash === paymentRequestData.paymentHash - ) { - setState({ - ...state, - state: PaymentRequestState.Paid, - }) - } - }, [state, lastHash]) - - return { - ...state, - checkExpiredAndGetRemainingSeconds, - ...setterMethods, - } -} diff --git a/app/screens/receive-bitcoin-screen/use-payment-request.types.ts b/app/screens/receive-bitcoin-screen/use-payment-request.types.ts deleted file mode 100644 index 708b895b00..0000000000 --- a/app/screens/receive-bitcoin-screen/use-payment-request.types.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { WalletCurrency } from "@app/graphql/generated" -import { MoneyAmount, WalletOrDisplayCurrency } from "@app/types/amounts" -import { WalletDescriptor } from "@app/types/wallets" -import { CreatePaymentRequestDetailsParams } from "./payment-requests" -import { - ConvertMoneyAmount, - PaymentRequestDetails, - PaymentRequest, - PaymentRequestType, -} from "./payment-requests/index.types" - -export type UsePaymentRequestParams = { - initialCreatePaymentRequestDetailsParams?: CreatePaymentRequestDetailsParams -} - -export const PaymentRequestState = { - Idle: "Idle", - Loading: "Loading", - Created: "Created", - Error: "Error", - Paid: "Paid", - Expired: "Expired", -} as const - -export const ErrorType = { - Generic: "Generic", -} - -export type ErrorType = (typeof ErrorType)[keyof typeof ErrorType] - -export type UsePaymentRequestState = - | { - state: typeof PaymentRequestState.Idle - createPaymentRequestDetailsParams: CreatePaymentRequestDetailsParams | undefined - paymentRequestDetails: PaymentRequestDetails | undefined - paymentRequest?: undefined - error?: undefined - } - | { - state: typeof PaymentRequestState.Loading - createPaymentRequestDetailsParams: CreatePaymentRequestDetailsParams - paymentRequestDetails: PaymentRequestDetails - paymentRequest?: undefined - error?: undefined - } - | { - state: typeof PaymentRequestState.Created - createPaymentRequestDetailsParams: CreatePaymentRequestDetailsParams - paymentRequestDetails: PaymentRequestDetails - paymentRequest: PaymentRequest - error?: undefined - } - | { - state: typeof PaymentRequestState.Error - createPaymentRequestDetailsParams: CreatePaymentRequestDetailsParams - paymentRequestDetails: PaymentRequestDetails - paymentRequest?: undefined - error: ErrorType - } - | { - state: typeof PaymentRequestState.Paid - createPaymentRequestDetailsParams: CreatePaymentRequestDetailsParams - paymentRequestDetails: PaymentRequestDetails - paymentRequest: PaymentRequest - } - | { - state: typeof PaymentRequestState.Expired - createPaymentRequestDetailsParams: CreatePaymentRequestDetailsParams - paymentRequestDetails: PaymentRequestDetails - paymentRequest: PaymentRequest - } - -export type PaymentRequestState = - (typeof PaymentRequestState)[keyof typeof PaymentRequestState] - -export type UsePaymentRequestResult = UsePaymentRequestState & { - setCreatePaymentRequestDetailsParams: ( - params: SetCreatePaymentRequestDetailsParamsParams, - ) => void - checkExpiredAndGetRemainingSeconds: - | ((currentTime: Date) => number | undefined) - | undefined -} & UsePaymentRequestSetterFns - -export type SetCreatePaymentRequestDetailsParamsParams = { - params: CreatePaymentRequestDetailsParams - generatePaymentRequestAfter: boolean -} - -export type SetAmountParams = { - amount: MoneyAmount - generatePaymentRequestAfter?: boolean -} - -export type SetMemoParams = { - memo: string - generatePaymentRequestAfter?: boolean -} - -export type SetReceivingWalletDescriptorParams = { - receivingWalletDescriptor: WalletDescriptor - generatePaymentRequestAfter?: boolean -} - -export type SetPaymentRequestTypeParams = { - paymentRequestType: PaymentRequestType - generatePaymentRequestAfter?: boolean -} - -export type SetConvertMoneyAmountParams = { - convertMoneyAmount: ConvertMoneyAmount - generatePaymentRequestAfter?: boolean -} - -type UsePaymentRequestSetterFns = - | { - setAmount: (params: SetAmountParams) => void - generatePaymentRequest: () => Promise - setMemo: (params: SetMemoParams) => void - setReceivingWalletDescriptor: (params: SetReceivingWalletDescriptorParams) => void - setPaymentRequestType: (params: SetPaymentRequestTypeParams) => void - setConvertMoneyAmount: (params: SetConvertMoneyAmountParams) => void - } - | { - generatePaymentRequest?: undefined - setAmount?: undefined - setMemo?: undefined - setReceivingWalletDescriptor?: undefined - setPaymentRequestType?: undefined - setConvertMoneyAmount?: undefined - } diff --git a/app/screens/receive-bitcoin-screen/use-receive-bitcoin.ts b/app/screens/receive-bitcoin-screen/use-receive-bitcoin.ts new file mode 100644 index 0000000000..cbd23f244e --- /dev/null +++ b/app/screens/receive-bitcoin-screen/use-receive-bitcoin.ts @@ -0,0 +1,457 @@ +import { useEffect, useMemo, useState } from "react" +import { + BaseCreatePaymentRequestCreationDataParams, + Invoice, + InvoiceType, + PaymentRequest, + PaymentRequestState, + PaymentRequestCreationData, +} from "./payment/index.types" +import { + WalletCurrency, + useLnInvoiceCreateMutation, + useLnNoAmountInvoiceCreateMutation, + useLnUsdInvoiceCreateMutation, + useOnChainAddressCurrentMutation, + usePaymentRequestQuery, + useRealtimePriceQuery, +} from "@app/graphql/generated" +import { createPaymentRequestCreationData } from "./payment/payment-request-creation-data" + +import { useAppConfig, usePriceConversion } from "@app/hooks" +import Clipboard from "@react-native-clipboard/clipboard" +import { gql } from "@apollo/client" +import { useIsAuthed } from "@app/graphql/is-authed-context" +import { getBtcWallet, getDefaultWallet, getUsdWallet } from "@app/graphql/wallets-utils" +import { createPaymentRequest } from "./payment/payment-request" +import { MoneyAmount, WalletOrDisplayCurrency } from "@app/types/amounts" +import { useLnUpdateHashPaid } from "@app/graphql/ln-update-context" +import { generateFutureLocalTime, secondsToH, secondsToHMS } from "./payment/helpers" +import { toastShow } from "@app/utils/toast" +import { useI18nContext } from "@app/i18n/i18n-react" + +import crashlytics from "@react-native-firebase/crashlytics" +import { Alert, Share } from "react-native" + +gql` + query paymentRequest { + globals { + network + feesInformation { + deposit { + minBankFee + minBankFeeThreshold + } + } + } + me { + id + username + defaultAccount { + id + wallets { + id + balance + walletCurrency + } + defaultWalletId + } + } + } + + mutation lnNoAmountInvoiceCreate($input: LnNoAmountInvoiceCreateInput!) { + lnNoAmountInvoiceCreate(input: $input) { + errors { + message + } + invoice { + paymentHash + paymentRequest + paymentSecret + } + } + } + + mutation lnInvoiceCreate($input: LnInvoiceCreateInput!) { + lnInvoiceCreate(input: $input) { + errors { + message + } + invoice { + paymentHash + paymentRequest + paymentSecret + satoshis + } + } + } + + mutation onChainAddressCurrent($input: OnChainAddressCurrentInput!) { + onChainAddressCurrent(input: $input) { + errors { + message + } + address + } + } + + mutation lnUsdInvoiceCreate($input: LnUsdInvoiceCreateInput!) { + lnUsdInvoiceCreate(input: $input) { + errors { + message + } + invoice { + paymentHash + paymentRequest + paymentSecret + satoshis + } + } + } +` + +export const useReceiveBitcoin = () => { + const [lnNoAmountInvoiceCreate] = useLnNoAmountInvoiceCreateMutation() + const [lnUsdInvoiceCreate] = useLnUsdInvoiceCreateMutation() + const [lnInvoiceCreate] = useLnInvoiceCreateMutation() + const [onChainAddressCurrent] = useOnChainAddressCurrentMutation() + + const mutations = { + lnNoAmountInvoiceCreate, + lnUsdInvoiceCreate, + lnInvoiceCreate, + onChainAddressCurrent, + } + + const [prcd, setPRCD] = useState | null>( + null, + ) + const [pr, setPR] = useState(null) + const [memoChangeText, setMemoChangeText] = useState(null) + + const [expiresInSeconds, setExpiresInSeconds] = useState(null) + const [ + isSetLightningAddressModalVisible, + setIsSetLightningAddressModalVisible, + ] = useState(false) + const toggleIsSetLightningAddressModalVisible = () => { + setIsSetLightningAddressModalVisible(!isSetLightningAddressModalVisible) + } + + const { LL } = useI18nContext() + const isAuthed = useIsAuthed() + + const { data } = usePaymentRequestQuery({ + fetchPolicy: "cache-first", + skip: !isAuthed, + }) + + // forcing price refresh + useRealtimePriceQuery({ + fetchPolicy: "network-only", + skip: !isAuthed, + }) + + const defaultWallet = getDefaultWallet( + data?.me?.defaultAccount?.wallets, + data?.me?.defaultAccount?.defaultWalletId, + ) + + const bitcoinWallet = getBtcWallet(data?.me?.defaultAccount?.wallets) + const usdWallet = getUsdWallet(data?.me?.defaultAccount?.wallets) + + const username = data?.me?.username + + const appConfig = useAppConfig().appConfig + const posUrl = appConfig.galoyInstance.posUrl + const lnAddressHostname = appConfig.galoyInstance.lnAddressHostname + + const { convertMoneyAmount: _convertMoneyAmount } = usePriceConversion() + + // Initialize Payment Request Creation Data + useEffect(() => { + if ( + prcd === null && + _convertMoneyAmount && + defaultWallet && + bitcoinWallet && + posUrl && + data?.globals?.network + ) { + const defaultWalletDescriptor = { + currency: defaultWallet.walletCurrency, + id: defaultWallet.id, + } + + const bitcoinWalletDescriptor = { + currency: WalletCurrency.Btc, + id: bitcoinWallet.id, + } + + const initialPRParams: BaseCreatePaymentRequestCreationDataParams = { + type: Invoice.Lightning, + defaultWalletDescriptor, + bitcoinWalletDescriptor, + convertMoneyAmount: _convertMoneyAmount, + username: username !== null ? username : undefined, + posUrl, + network: data.globals?.network, + } + setPRCD(createPaymentRequestCreationData(initialPRParams)) + } + }, [_convertMoneyAmount, defaultWallet, bitcoinWallet, username]) + + // Initialize Payment Request + useEffect(() => { + if (prcd) { + setPR( + createPaymentRequest({ + mutations, + creationData: prcd, + }), + ) + } + }, [ + prcd?.type, + prcd?.unitOfAccountAmount, + prcd?.memo, + prcd?.receivingWalletDescriptor, + prcd?.username, + setPR, + ]) + + // Generate Payment Request + useEffect(() => { + if (pr && pr.state === PaymentRequestState.Idle) { + setPR((pq) => pq && pq.setState(PaymentRequestState.Loading)) + pr.generateRequest().then((newPR) => + setPR((currentPR) => { + // don't override payment request if the request is from different request + if (currentPR?.creationData === newPR.creationData) return newPR + else return currentPR + }), + ) + } + }, [pr?.state]) + + // Setting it to idle would trigger last useEffect hook to regenerate invoice + const regenerateInvoice = () => { + if (expiresInSeconds === 0) setPR((pq) => pq && pq.setState(PaymentRequestState.Idle)) + } + + // For Detecting Paid + const lastHash = useLnUpdateHashPaid() + useEffect(() => { + if ( + pr?.state === PaymentRequestState.Created && + pr.info?.data?.invoiceType === "Lightning" && + lastHash === pr.info.data.paymentHash + ) { + setPR((pq) => pq && pq.setState(PaymentRequestState.Paid)) + } + }, [lastHash]) + + // For Expires In + useEffect(() => { + if (pr?.info?.data?.invoiceType === "Lightning" && pr.info?.data?.expiresAt) { + const setExpiresTime = () => { + const currentTime = new Date() + const expiresAt = + pr?.info?.data?.invoiceType === "Lightning" && pr.info?.data?.expiresAt + if (!expiresAt) return + + const remainingSeconds = Math.floor( + (expiresAt.getTime() - currentTime.getTime()) / 1000, + ) + + if (remainingSeconds >= 0) { + setExpiresInSeconds(remainingSeconds) + } else { + clearInterval(intervalId) + setExpiresInSeconds(0) + setPR((pq) => pq && pq.setState(PaymentRequestState.Expired)) + } + } + + setExpiresTime() + const intervalId = setInterval(setExpiresTime, 1000) + + return () => { + clearInterval(intervalId) + setExpiresInSeconds(null) + } + } + }, [pr?.info?.data, setExpiresInSeconds]) + + // Clean Memo + useEffect(() => { + if (memoChangeText === "") { + setPRCD((pr) => { + if (pr && pr.setMemo) { + return pr.setMemo("") + } + return pr + }) + } + }, [memoChangeText, setPRCD]) + + const { copyToClipboard, share } = useMemo(() => { + if (!pr) { + return {} + } + + const paymentFullUri = pr.info?.data?.getFullUriFn({}) + + const copyToClipboard = () => { + if (!paymentFullUri) return + + Clipboard.setString(paymentFullUri) + + toastShow({ + message: (translations) => translations.ReceiveScreen.copyClipboard(), + currentTranslation: LL, + type: "success", + }) + } + + const share = async () => { + if (!paymentFullUri) return + try { + const result = await Share.share({ message: paymentFullUri }) + + if (result.action === Share.sharedAction) { + if (result.activityType) { + // shared with activity type of result.activityType + } else { + // shared + } + } else if (result.action === Share.dismissedAction) { + // dismissed + } + } catch (err) { + if (err instanceof Error) { + crashlytics().recordError(err) + Alert.alert(err.message) + } + } + } + + return { + copyToClipboard, + share, + } + }, [pr?.info?.data, LL]) + + if (!prcd) return null + + const setType = (type: InvoiceType) => { + setPRCD((pr) => pr && pr.setType(type)) + setPRCD((pr) => { + if (pr && pr.setMemo) { + return pr.setMemo("") + } + return pr + }) + setMemoChangeText("") + } + + const setMemo = () => { + setPRCD((pr) => { + if (pr && memoChangeText && pr.setMemo) { + return pr.setMemo(memoChangeText) + } + return pr + }) + } + const setReceivingWallet = (walletCurrency: WalletCurrency) => { + setPRCD((pr) => { + if (pr && pr.setReceivingWalletDescriptor) { + if (walletCurrency === WalletCurrency.Btc && bitcoinWallet) { + return pr.setReceivingWalletDescriptor({ + id: bitcoinWallet.id, + currency: WalletCurrency.Btc, + }) + } else if (walletCurrency === WalletCurrency.Usd && usdWallet) { + return pr.setReceivingWalletDescriptor({ + id: usdWallet.id, + currency: WalletCurrency.Usd, + }) + } + } + return pr + }) + } + const setAmount = (amount: MoneyAmount) => { + setPRCD((pr) => { + if (pr && pr.setAmount) { + return pr.setAmount(amount) + } + return pr + }) + } + + let extraDetails = "" + if ( + prcd.type === "Lightning" && + expiresInSeconds !== null && + typeof expiresInSeconds === "number" && + pr?.state !== PaymentRequestState.Paid + ) { + if (expiresInSeconds > 60 * 60 * 23) + extraDetails = `${LL.ReceiveScreen.singleUse()} | ${LL.ReceiveScreen.invoiceValidity.validFor1Day()}` + else if (expiresInSeconds > 60 * 60 * 6) + extraDetails = `${LL.ReceiveScreen.singleUse()} | ${LL.ReceiveScreen.invoiceValidity.validForNext( + { duration: secondsToH(expiresInSeconds) }, + )}` + else if (expiresInSeconds > 60 * 2) + extraDetails = `${LL.ReceiveScreen.singleUse()} | ${LL.ReceiveScreen.invoiceValidity.validBefore( + { + time: generateFutureLocalTime(expiresInSeconds), + }, + )}` + else if (expiresInSeconds > 0) + extraDetails = `${LL.ReceiveScreen.singleUse()} | ${LL.ReceiveScreen.invoiceValidity.expiresIn( + { + duration: secondsToHMS(expiresInSeconds), + }, + )}` + else if (pr?.state === PaymentRequestState.Expired) + extraDetails = LL.ReceiveScreen.invoiceExpired() + else + extraDetails = `${LL.ReceiveScreen.singleUse()} | ${LL.ReceiveScreen.invoiceValidity.expiresNow()}` + } else if (prcd.type === "Lightning" && pr?.state === PaymentRequestState.Paid) { + extraDetails = LL.ReceiveScreen.invoiceHasBeenPaid() + } else if (prcd.type === "OnChain" && pr?.info?.data?.invoiceType === "OnChain") { + extraDetails = LL.ReceiveScreen.yourBitcoinOnChainAddress() + } else if (prcd.type === "PayCode" && pr?.info?.data?.invoiceType === "PayCode") { + extraDetails = `${pr.info.data.username}@${lnAddressHostname}` + } + + let readablePaymentRequest = "" + if (pr?.info?.data?.invoiceType === Invoice.Lightning) { + const uri = pr.info?.data?.getFullUriFn({}) + readablePaymentRequest = `${uri.slice(0, 6)}..${uri.slice(-6)}` + } else if (pr?.info?.data?.invoiceType === Invoice.OnChain) { + const address = pr.info?.data?.address || "" + readablePaymentRequest = `${address.slice(0, 6)}..${address.slice(-6)}` + } + + return { + ...prcd, + setType, + ...pr, + extraDetails, + regenerateInvoice, + setMemo, + setReceivingWallet, + setAmount, + feesInformation: data?.globals?.feesInformation, + memoChangeText, + setMemoChangeText, + copyToClipboard, + share, + isSetLightningAddressModalVisible, + toggleIsSetLightningAddressModalVisible, + readablePaymentRequest, + } +} diff --git a/app/screens/redeem-lnurl-withdrawal-screen/redeem-bitcoin-result-screen.tsx b/app/screens/redeem-lnurl-withdrawal-screen/redeem-bitcoin-result-screen.tsx index c8b615f493..7a11160c41 100644 --- a/app/screens/redeem-lnurl-withdrawal-screen/redeem-bitcoin-result-screen.tsx +++ b/app/screens/redeem-lnurl-withdrawal-screen/redeem-bitcoin-result-screen.tsx @@ -2,7 +2,7 @@ import { RootStackParamList } from "@app/navigation/stack-param-lists" import { StackNavigationProp } from "@react-navigation/stack" import React, { useCallback, useEffect, useMemo, useState } from "react" import { ActivityIndicator, View } from "react-native" -import { PaymentRequest } from "../receive-bitcoin-screen/payment-requests/index.types" +// import { PaymentRequest } from "../receive-bitcoin-screen/payment-requests/index.types" import { HomeAuthedDocument, @@ -81,11 +81,11 @@ const RedeemBitcoinResultScreen: React.FC = ({ route }) => { async (satAmount: number, memo: string) => { setInvoice(null) try { - logGeneratePaymentRequest({ - paymentType: PaymentRequest.Lightning, - hasAmount: true, - receivingWallet: WalletCurrency.Btc, - }) + // logGeneratePaymentRequest({ + // paymentType: PaymentRequest.Lightning, + // hasAmount: true, + // receivingWallet: WalletCurrency.Btc, + // }) const { data } = await lnInvoiceCreate({ variables: { input: { walletId: receivingWalletDescriptor.id, amount: satAmount, memo }, diff --git a/app/screens/send-bitcoin-screen/send-bitcoin-details-screen.tsx b/app/screens/send-bitcoin-screen/send-bitcoin-details-screen.tsx index 113ad4b8eb..5523ee2e31 100644 --- a/app/screens/send-bitcoin-screen/send-bitcoin-details-screen.tsx +++ b/app/screens/send-bitcoin-screen/send-bitcoin-details-screen.tsx @@ -1,5 +1,4 @@ import { gql } from "@apollo/client" -import NoteIcon from "@app/assets/icons/note.svg" import { AmountInput } from "@app/components/amount-input/amount-input" import { GaloyPrimaryButton } from "@app/components/atomic/galoy-primary-button" import { Screen } from "@app/components/screen" @@ -28,7 +27,7 @@ import crashlytics from "@react-native-firebase/crashlytics" import { NavigationProp, RouteProp, useNavigation } from "@react-navigation/native" import { makeStyles, Text, useTheme } from "@rneui/themed" import React, { useEffect, useState } from "react" -import { TextInput, TouchableWithoutFeedback, View } from "react-native" +import { TouchableWithoutFeedback, View } from "react-native" import ReactNativeModal from "react-native-modal" import Icon from "react-native-vector-icons/Ionicons" import { testProps } from "../../utils/testProps" @@ -38,6 +37,7 @@ import { SendBitcoinDetailsExtraInfo } from "./send-bitcoin-details-extra-info" import { requestInvoice, utils } from "lnurl-pay" import { GaloyTertiaryButton } from "@app/components/atomic/galoy-tertiary-button" import { getBtcWallet, getDefaultWallet, getUsdWallet } from "@app/graphql/wallets-utils" +import { NoteInput } from "@app/components/note-input" gql` query sendBitcoinDetailsScreen { @@ -499,25 +499,13 @@ const SendBitcoinDetailsScreen: React.FC = ({ route }) => { {LL.SendBitcoinScreen.note()} - - - - - - - paymentDetail.setMemo && setPaymentDetail(paymentDetail.setMemo(text)) - } - value={paymentDetail.memo || ""} - editable={paymentDetail.canSetMemo} - selectTextOnFocus - maxLength={500} - /> - - + + paymentDetail.setMemo && setPaymentDetail(paymentDetail.setMemo(text)) + } + value={paymentDetail.memo || ""} + editable={paymentDetail.canSetMemo} + /> ({ justifyContent: "center", alignItems: "center", }, - noteContainer: { - flex: 1, - flexDirection: "row", - }, - noteIconContainer: { - marginRight: 12, - justifyContent: "center", - alignItems: "flex-start", - }, - noteIcon: { - justifyContent: "center", - alignItems: "center", - }, - noteInput: { - flex: 1, - color: colors.black, - }, buttonContainer: { flex: 1, justifyContent: "flex-end", diff --git a/app/types/amounts.ts b/app/types/amounts.ts index 8d3172c3d1..dec66cf2bf 100644 --- a/app/types/amounts.ts +++ b/app/types/amounts.ts @@ -109,11 +109,11 @@ export const toDisplayAmount = ({ } } -export const createToDisplayAmount = - (currencyCode: string) => - (amount: number | undefined): DisplayAmount => { - return toDisplayAmount({ amount, currencyCode }) - } +export const createToDisplayAmount = (currencyCode: string) => ( + amount: number | undefined, +): DisplayAmount => { + return toDisplayAmount({ amount, currencyCode }) +} export const lessThanOrEqualTo = ({ value,