mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-10-05 19:28:26 +00:00
Enter amount in foreign currency, auto-calculate SAT equivalent (#164)
Co-authored-by: minibits-cash <138401554+minibits-cash@users.noreply.github.com>
This commit is contained in:
co-authored by
minibits-cash
parent
c83e90a36e
commit
34efa8cb9a
+12
@@ -91,5 +91,17 @@ mise.yaml
|
||||
# temp dev scripts 2025-07-14 and beyond. will remove afterwards
|
||||
key-mappings.json
|
||||
replace-translation-keys.js
|
||||
|
||||
|
||||
# for AI agents' one-off jobs, it's useful to use this workflow: tell them:
|
||||
|
||||
# explore the codebase and create a detailed, step by step plan how you're going to implement this feature.
|
||||
# don't do any implementations yet
|
||||
# write this plan down in tasks.md as a list of markdown tasks - [ ], in root of the respoitory.
|
||||
# then, start working on it, constantly re-evaluating how much his done and if it's done, then check off that particular task.
|
||||
# once they are all checked off, review the task list and double-check everything
|
||||
tasks.md
|
||||
|
||||
# hot-updater
|
||||
.hot-updater/output
|
||||
|
||||
|
||||
Vendored
+2
-6
@@ -1,10 +1,6 @@
|
||||
{
|
||||
"[javascriptreact][typescriptreact]": {
|
||||
"[javascriptreact][typescriptreact][javascript][typescript]": {
|
||||
"editor.insertSpaces": true,
|
||||
"editor.tabSize": 2
|
||||
},
|
||||
"[javascript][typescript]": {
|
||||
"editor.insertSpaces": true,
|
||||
"editor.tabSize": 2
|
||||
"editor.tabSize": 4
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,11 @@ interface AmountInputProps {
|
||||
value: string
|
||||
onChangeText: (text: string) => void
|
||||
unit?: MintUnit
|
||||
/**
|
||||
* Override default formatting options (thousandSeparated, mantissa, etc.) for onEndEditing formatting.
|
||||
* Useful for fiat currencies that need custom decimal precision or thousand separator behavior.
|
||||
*/
|
||||
formatOptions?: Partial<numbro.Format>
|
||||
onEndEditing?: () => void
|
||||
editable?: boolean
|
||||
selectTextOnFocus?: boolean
|
||||
@@ -23,6 +28,7 @@ export const AmountInput = forwardRef<TextInput, AmountInputProps>(
|
||||
value,
|
||||
onChangeText,
|
||||
unit = 'sat',
|
||||
formatOptions,
|
||||
onEndEditing,
|
||||
editable = true,
|
||||
selectTextOnFocus,
|
||||
@@ -47,18 +53,18 @@ export const AmountInput = forwardRef<TextInput, AmountInputProps>(
|
||||
}
|
||||
|
||||
const handleEndEditing = () => {
|
||||
if (onEndEditing) {
|
||||
onEndEditing()
|
||||
}
|
||||
// Default formatting behavior
|
||||
const formattedValue = typeof value === "undefined"
|
||||
? ""
|
||||
: numbro(value).format({
|
||||
thousandSeparated: true,
|
||||
mantissa: getCurrency(unit).mantissa
|
||||
})
|
||||
onChangeText(formattedValue)
|
||||
if (onEndEditing) onEndEditing();
|
||||
if (!value) value = "0";
|
||||
// ^^ needed as previous '=== undefined' check didn't trigger on value = "",
|
||||
// which would still crash numbro's formatter
|
||||
|
||||
// Merge default format options with overrides
|
||||
const formatOptionsMerged = Object.assign(
|
||||
{ thousandSeparated: true, mantissa: getCurrency(unit).mantissa },
|
||||
formatOptions
|
||||
);
|
||||
const formattedValue = numbro(value).format(formatOptionsMerged);
|
||||
onChangeText(formattedValue)
|
||||
}
|
||||
|
||||
const defaultStyle: TextStyle = {
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
import { verticalScale } from '@gocodingnow/rn-size-matters'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { TextInput, TextStyle, TouchableOpacity, View, ViewStyle } from 'react-native'
|
||||
import { useStores } from '../models'
|
||||
import type { Mint } from '../models/Mint'
|
||||
import { TransactionStatus } from '../models/Transaction'
|
||||
import { MintHeader } from '../screens/Mints/MintHeader'
|
||||
import { CurrencyAmount } from '../screens/Wallet/CurrencyAmount'
|
||||
import {
|
||||
availableExchangeCurrencies,
|
||||
convertToFromSats,
|
||||
CurrencyCode,
|
||||
formatCurrency,
|
||||
getCurrency,
|
||||
getCurrencyByCode,
|
||||
MintUnit
|
||||
} from '../services/wallet/currency'
|
||||
import { spacing, useThemeColor } from '../theme'
|
||||
import { round, toNumber } from '../utils/number'
|
||||
import { AmountInput, Icon, Text } from './index'
|
||||
|
||||
interface IAmountInputHeaderProps {
|
||||
amountInputRef: React.RefObject<TextInput>
|
||||
amountToSend: string
|
||||
setAmountToSend: (amount: string) => void
|
||||
unit: MintUnit
|
||||
onAmountEndEditing?: () => void
|
||||
transactionStatus?: TransactionStatus
|
||||
isCashuPrWithAmount?: boolean
|
||||
lockedPubkey?: string
|
||||
unitRef: React.RefObject<MintUnit>
|
||||
mintHeaderMint: Mint
|
||||
}
|
||||
|
||||
const $amountContainer: ViewStyle = {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}
|
||||
|
||||
const $headerContainer: TextStyle = {
|
||||
alignItems: 'center',
|
||||
padding: spacing.extraSmall,
|
||||
paddingTop: 0,
|
||||
height: spacing.screenHeight * 0.31,
|
||||
}
|
||||
|
||||
const $pubKey: ViewStyle = {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginTop: -spacing.extraSmall
|
||||
}
|
||||
|
||||
export function AmountInputHeader(props: IAmountInputHeaderProps) {
|
||||
const { userSettingsStore, walletStore } = useStores()
|
||||
|
||||
/** displayed converted amount for <CurrencyAmount /> */
|
||||
const [currencyAmount, setCurrencyAmount] = useState(0);
|
||||
const [isFiatMode, setIsFiatMode] = useState(false)
|
||||
const [amountFiat, setAmountFiat] = useState("0");
|
||||
const [shouldTriggerSubmit, setShouldTriggerSubmit] = useState(false);
|
||||
|
||||
const amountInputColor = useThemeColor('amountInput');
|
||||
const convertedAmountColor = useThemeColor('headerSubTitle');
|
||||
const headerBg = useThemeColor('header')
|
||||
const buttonIconColor = useThemeColor('buttonIcon');
|
||||
|
||||
const {
|
||||
amountInputRef,
|
||||
amountToSend,
|
||||
setAmountToSend,
|
||||
unit,
|
||||
onAmountEndEditing,
|
||||
transactionStatus,
|
||||
isCashuPrWithAmount,
|
||||
lockedPubkey,
|
||||
unitRef,
|
||||
mintHeaderMint
|
||||
} = props
|
||||
|
||||
const fiatCurrency = userSettingsStore.exchangeCurrency
|
||||
const isFiatSupported = availableExchangeCurrencies.includes(fiatCurrency as typeof availableExchangeCurrencies[number]);
|
||||
const canUseFiatMode = isFiatSupported && walletStore.exchangeRate && unit === 'sat'
|
||||
|
||||
const getFiatUnit = () => {
|
||||
const currencyData = getCurrencyByCode(fiatCurrency)
|
||||
return currencyData?.mintUnit || 'sat'
|
||||
}
|
||||
|
||||
const roundToSatPrecision = (value: number) => round(value, getCurrency(unitRef.current).mantissa)
|
||||
const roundToFiatPrecision = (value: number) => round(value, getCurrencyByCode(fiatCurrency)?.mantissa || 2)
|
||||
|
||||
/** @returns converted number or 0 if conversion fails for any reason */
|
||||
const FIATtoSATS = (inputAmount: string) => {
|
||||
if (!walletStore.exchangeRate || !inputAmount || inputAmount.trim() === '') return 0;
|
||||
|
||||
const fiatCurrencyData = getCurrencyByCode(fiatCurrency)
|
||||
if (!fiatCurrencyData) return 0;
|
||||
|
||||
const precision = fiatCurrencyData.precision
|
||||
const converted = convertToFromSats(
|
||||
round(toNumber(inputAmount) * precision, 0) || 0,
|
||||
fiatCurrency,
|
||||
walletStore.exchangeRate
|
||||
)
|
||||
return converted ? roundToSatPrecision(converted) : 0;
|
||||
}
|
||||
|
||||
/** @returns converted number or 0 if conversion fails for any reason */
|
||||
const SATStoFIAT = (inputAmount: string) => {
|
||||
if (!walletStore.exchangeRate || !inputAmount || inputAmount.trim() === '') return 0;
|
||||
|
||||
const precision = getCurrency(unitRef.current).precision
|
||||
const converted = convertToFromSats(
|
||||
round(toNumber(inputAmount) * precision, 0) || 0,
|
||||
getCurrency(unitRef.current).code,
|
||||
walletStore.exchangeRate
|
||||
)
|
||||
return converted ? roundToFiatPrecision(converted) : 0;
|
||||
}
|
||||
|
||||
const isConvertedAmountVisible = () => {
|
||||
return canUseFiatMode && walletStore.exchangeRate
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!canUseFiatMode) return;
|
||||
|
||||
// the conversions between the two *looks* asymmetric, but it works:
|
||||
// the reason it's like this is because <CurrencyAmount /> formats each currency differently,
|
||||
// so setting currencyAmount to 9.97 often produces stuff like 0.1 EUR
|
||||
|
||||
// there is possibly a better way to solve this:
|
||||
// if CurrencyAmount would just display raw values, we could just call FIATtoSATS and SATStoFIAT respectively
|
||||
// feel free to refactor/change this; i spent way too long trying to get it to work
|
||||
|
||||
if (isFiatMode) {
|
||||
// SATS -> FIAT mode
|
||||
// for 1067 sats, even though currencyAmount is 106.26, after formatting it's the desired 1.06
|
||||
// we can simply do the same formatting as <CurrencyAmount /> to get 1.06 for our input
|
||||
// since <CurrencyAmount /> formats SATS normally, we can pass in the raw value (e.g. 1067)
|
||||
const newVal = formatCurrency(currencyAmount, fiatCurrency, false);
|
||||
setAmountFiat(newVal);
|
||||
setCurrencyAmount(toNumber(amountToSend.trim() || "0"));
|
||||
} else {
|
||||
// FIAT -> SATS mode
|
||||
// we convert our FIAT "result" (e.g. 1.06) using FIATtoSATS
|
||||
// since editing the SATS input always correctly calculates the FIAT currency on editd, we simply call handleAmountChange
|
||||
const newVal = FIATtoSATS(amountFiat).toString();
|
||||
setAmountToSend(newVal);
|
||||
handleAmountChange(newVal)
|
||||
if (shouldTriggerSubmit && onAmountEndEditing) onAmountEndEditing();
|
||||
}
|
||||
|
||||
}, [isFiatMode])
|
||||
|
||||
const handleAmountChange = (amount: string) => {
|
||||
setShouldTriggerSubmit(false);
|
||||
setAmountToSend(amount)
|
||||
if (canUseFiatMode) {
|
||||
const convertedAmount = SATStoFIAT(amount)
|
||||
setCurrencyAmount(convertedAmount)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFiatAmountChange = (amount: string) => {
|
||||
setAmountFiat(amount)
|
||||
setCurrencyAmount(FIATtoSATS(amount))
|
||||
}
|
||||
|
||||
const handleAmountEndEditing = () => {
|
||||
if (canUseFiatMode) {
|
||||
const convertedAmount = SATStoFIAT(amountToSend)
|
||||
setCurrencyAmount(convertedAmount)
|
||||
}
|
||||
if (onAmountEndEditing) onAmountEndEditing();
|
||||
}
|
||||
|
||||
/** when FIAT input is submitted, it just recalculates to SATS and submits that amount */
|
||||
const onFiatAmountEndEditing = () => {
|
||||
const convertedAmount = FIATtoSATS(amountFiat)
|
||||
setCurrencyAmount(convertedAmount)
|
||||
setAmountToSend(convertedAmount.toString())
|
||||
|
||||
if (amountFiat.trim()) {
|
||||
setShouldTriggerSubmit(true);
|
||||
setIsFiatMode(false);
|
||||
}
|
||||
}
|
||||
|
||||
return <View style={[$headerContainer, { backgroundColor: headerBg }]}>
|
||||
<MintHeader
|
||||
mint={mintHeaderMint}
|
||||
unit={canUseFiatMode && isFiatMode ? getFiatUnit() : unitRef.current}
|
||||
displayCurrency={canUseFiatMode && isFiatMode ? fiatCurrency : undefined}
|
||||
/>
|
||||
<View style={$amountContainer}>
|
||||
{isFiatMode && canUseFiatMode ? (
|
||||
<AmountInput
|
||||
value={amountFiat}
|
||||
onChangeText={handleFiatAmountChange}
|
||||
unit={getFiatUnit()}
|
||||
formatOptions={{ // Override formatting for FIAT
|
||||
thousandSeparated: false,
|
||||
mantissa: getCurrencyByCode(fiatCurrency)?.mantissa || 2
|
||||
}}
|
||||
onEndEditing={transactionStatus !== TransactionStatus.PENDING ? onFiatAmountEndEditing : undefined}
|
||||
editable={!(transactionStatus === TransactionStatus.PENDING || isCashuPrWithAmount)}
|
||||
style={{ color: amountInputColor }}
|
||||
/>
|
||||
) : (
|
||||
<AmountInput
|
||||
value={amountToSend}
|
||||
onChangeText={handleAmountChange}
|
||||
unit={unit}
|
||||
onEndEditing={transactionStatus !== TransactionStatus.PENDING ? handleAmountEndEditing : undefined}
|
||||
editable={!(transactionStatus === TransactionStatus.PENDING || isCashuPrWithAmount)}
|
||||
style={{ color: amountInputColor }}
|
||||
ref={amountInputRef}
|
||||
/>
|
||||
)}
|
||||
{isConvertedAmountVisible() && (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (!canUseFiatMode) return;
|
||||
setIsFiatMode(!isFiatMode);
|
||||
}}
|
||||
style={{ position: 'relative', flexDirection: 'row', alignItems: "center" }}
|
||||
>
|
||||
<CurrencyAmount
|
||||
amount={currencyAmount}
|
||||
currencyCode={isFiatMode ? CurrencyCode.SAT : fiatCurrency}
|
||||
symbolStyle={{ color: convertedAmountColor, marginTop: spacing.tiny, fontSize: verticalScale(10) }}
|
||||
amountStyle={{ color: convertedAmountColor, lineHeight: spacing.medium }}
|
||||
size='medium'
|
||||
containerStyle={{ justifyContent: 'center', paddingRight: 0 }}
|
||||
/>
|
||||
<Icon
|
||||
icon="faArrowRightArrowLeft"
|
||||
size={16}
|
||||
color={buttonIconColor}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{lockedPubkey ? (
|
||||
<View style={$pubKey}>
|
||||
<Icon
|
||||
icon="faLock"
|
||||
size={spacing.small}
|
||||
color={amountInputColor}
|
||||
/>
|
||||
<Text
|
||||
size='xs'
|
||||
tx="sendLocked"
|
||||
style={{ color: amountInputColor, marginLeft: spacing.tiny }}
|
||||
/>
|
||||
|
||||
</View>
|
||||
) : (
|
||||
<Text
|
||||
size='xs'
|
||||
tx='amountSend'
|
||||
style={{
|
||||
color: amountInputColor,
|
||||
textAlign: 'center',
|
||||
marginTop: isConvertedAmountVisible() ? -spacing.extraSmall : undefined
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const CadIcon = `<svg width="256px" height="256px" viewBox="2 2 20.00 20.00" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0" transform="translate(2.16,2.16), scale(0.82)"><rect x="0" y="0" width="24.00" height="24.00" rx="12" fill="#ffffff" strokewidth="0"></rect></g><g id="SVGRepo_iconCarrier"> <path fill-rule="evenodd" clip-rule="evenodd" d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22ZM12.75 6C12.75 5.58579 12.4142 5.25 12 5.25C11.5858 5.25 11.25 5.58579 11.25 6V6.31673C9.61957 6.60867 8.25 7.83361 8.25 9.5C8.25 11.4172 10.0628 12.75 12 12.75C13.3765 12.75 14.25 13.6557 14.25 14.5C14.25 15.3443 13.3765 16.25 12 16.25C10.6235 16.25 9.75 15.3443 9.75 14.5C9.75 14.0858 9.41421 13.75 9 13.75C8.58579 13.75 8.25 14.0858 8.25 14.5C8.25 16.1664 9.61957 17.3913 11.25 17.6833V18C11.25 18.4142 11.5858 18.75 12 18.75C12.4142 18.75 12.75 18.4142 12.75 18V17.6833C14.3804 17.3913 15.75 16.1664 15.75 14.5C15.75 12.5828 13.9372 11.25 12 11.25C10.6235 11.25 9.75 10.3443 9.75 9.5C9.75 8.65573 10.6235 7.75 12 7.75C13.3765 7.75 14.25 8.65573 14.25 9.5C14.25 9.91421 14.5858 10.25 15 10.25C15.4142 10.25 15.75 9.91421 15.75 9.5C15.75 7.83361 14.3804 6.60867 12.75 6.31673V6Z" fill="#D62718"></path> </g></svg>`
|
||||
@@ -98,13 +98,14 @@ import { faArrowRotateLeft } from "@fortawesome/free-solid-svg-icons/faArrowRota
|
||||
import { faHeartPulse } from "@fortawesome/free-solid-svg-icons/faHeartPulse"
|
||||
import { faSeedling } from "@fortawesome/free-solid-svg-icons/faSeedling"
|
||||
import { faChevronLeft } from '@fortawesome/free-solid-svg-icons/faChevronLeft'
|
||||
import { faArrowRightArrowLeft } from "@fortawesome/free-solid-svg-icons"
|
||||
|
||||
|
||||
export type IconTypes = keyof typeof iconRegistry
|
||||
|
||||
// TODO remove need for manual iconregistry?
|
||||
// would be best to just import all of them, i guess, or figure out something smart
|
||||
export const iconRegistry = { faAddressCard, faAddressBook, faWallet, faQrcode, faClipboard, faSliders, faCoins, faEllipsisVertical, faEllipsis, faArrowUp, faArrowDown, faArrowLeft, faXmark, faInfoCircle, faBug, faCheckCircle, faArrowTurnUp, faArrowTurnDown, faPencil, faTags, faShareFromSquare, faRotate, faCode, faBan, faCircle, faPaperPlane, faBolt, faArrowUpFromBracket, faArrowRightToBracket, faPlus, faShieldHalved, faCloudArrowUp, faPaintbrush, faCopy, faBurst, faUserShield, faLock, faLockOpen, faTriangleExclamation, faDownload, faUpload, faRecycle, faListUl, faExpand, faFingerprint, faWandMagicSparkles, faCircleUser, faComment, faKey, faCircleNodes, faBullseye, faEyeSlash, faUpRightFromSquare, faShareNodes, faPaste, faKeyboard, faMoneyBill1, faGears, faTag, faBank, faChevronDown, faChevronUp, faCircleExclamation, faCircleQuestion, faEnvelope, faTwitter, faTelegramPlane, faDiscord, faGithub, faReddit, faCircleArrowUp, faCircleArrowDown, faGlobe, faCubes, faClock, faArrowRotateLeft, faHeartPulse, faSeedling, faChevronLeft}
|
||||
export const iconRegistry = { faAddressCard, faAddressBook, faWallet, faQrcode, faClipboard, faSliders, faCoins, faEllipsisVertical, faEllipsis, faArrowUp, faArrowDown, faArrowLeft, faXmark, faInfoCircle, faBug, faCheckCircle, faArrowTurnUp, faArrowTurnDown, faPencil, faTags, faShareFromSquare, faRotate, faCode, faBan, faCircle, faPaperPlane, faBolt, faArrowUpFromBracket, faArrowRightToBracket, faPlus, faShieldHalved, faCloudArrowUp, faPaintbrush, faCopy, faBurst, faUserShield, faLock, faLockOpen, faTriangleExclamation, faDownload, faUpload, faRecycle, faListUl, faExpand, faFingerprint, faWandMagicSparkles, faCircleUser, faComment, faKey, faCircleNodes, faBullseye, faEyeSlash, faUpRightFromSquare, faShareNodes, faPaste, faKeyboard, faMoneyBill1, faGears, faTag, faBank, faChevronDown, faChevronUp, faCircleExclamation, faCircleQuestion, faEnvelope, faTwitter, faTelegramPlane, faDiscord, faGithub, faReddit, faCircleArrowUp, faCircleArrowDown, faGlobe, faCubes, faClock, faArrowRotateLeft, faHeartPulse, faSeedling, faChevronLeft, faArrowRightArrowLeft }
|
||||
|
||||
|
||||
interface IconProps extends TouchableOpacityProps {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export * from "./AmountInput"
|
||||
export * from "./AmountInputHeader"
|
||||
export * from "./AutoImage"
|
||||
export * from "./Button"
|
||||
export * from "./CadIcon"
|
||||
export * from "./Card"
|
||||
export * from "./Header"
|
||||
export * from "./Icon"
|
||||
|
||||
@@ -2,11 +2,10 @@ import React from "react"
|
||||
import { Header, Text } from "../../components"
|
||||
import { spacing, useThemeColor } from "../../theme"
|
||||
import { Mint } from "../../models/Mint"
|
||||
import { MintUnit } from "../../services/wallet/currency"
|
||||
import { CurrencyCode, MintUnit } from "../../services/wallet/currency"
|
||||
import { CurrencySign } from "../Wallet/CurrencySign"
|
||||
import { CurrencyAmount } from "../Wallet/CurrencyAmount"
|
||||
import { observer } from "mobx-react-lite"
|
||||
import { StackNavigationProp } from "@react-navigation/stack"
|
||||
import { moderateScale } from "@gocodingnow/rn-size-matters"
|
||||
import { useNavigation } from "@react-navigation/native"
|
||||
|
||||
@@ -15,10 +14,11 @@ export const MintHeader = observer(function(props: {
|
||||
mint?: Mint
|
||||
hideBalance?: boolean
|
||||
onBackPress?: () => void
|
||||
displayCurrency?: CurrencyCode
|
||||
}
|
||||
) {
|
||||
const navigation = useNavigation()
|
||||
const {mint, unit, hideBalance, onBackPress} = props
|
||||
const {mint, unit, hideBalance, onBackPress, displayCurrency} = props
|
||||
|
||||
const getActiveUnitColor = () => {
|
||||
/* switch (props.unit) {
|
||||
@@ -47,7 +47,8 @@ export const MintHeader = observer(function(props: {
|
||||
size='xxs'
|
||||
/>)}
|
||||
<CurrencySign
|
||||
mintUnit={unit}
|
||||
mintUnit={displayCurrency ? undefined : unit}
|
||||
currencyCode={displayCurrency}
|
||||
textStyle={{color: 'white'}}
|
||||
containerStyle={{
|
||||
borderBottomWidth: 2,
|
||||
@@ -64,8 +65,9 @@ export const MintHeader = observer(function(props: {
|
||||
}}
|
||||
RightActionComponent={mint && unit && !hideBalance ? (
|
||||
<CurrencyAmount
|
||||
mintUnit={unit}
|
||||
amount={mint?.balances?.balances[unit as MintUnit] || 0}
|
||||
mintUnit={displayCurrency ? 'sat' : unit}
|
||||
// always display wallet balance in SAT if using displayCurrency
|
||||
amount={mint?.balances?.balances[displayCurrency ? 'sat' : unit as MintUnit] || 0}
|
||||
amountStyle={{color: 'white'}}
|
||||
symbolStyle={{color: 'white'}}
|
||||
containerStyle={{marginRight: spacing.medium}}
|
||||
|
||||
+11
-105
@@ -28,7 +28,7 @@ import {
|
||||
ErrorModal,
|
||||
BottomModal,
|
||||
Text,
|
||||
AmountInput,
|
||||
AmountInputHeader
|
||||
} from '../components'
|
||||
import {TransactionStatus, Transaction} from '../models/Transaction'
|
||||
import {useStores} from '../models'
|
||||
@@ -1000,8 +1000,6 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
|
||||
setError(e)
|
||||
}
|
||||
|
||||
const headerBg = useThemeColor('header')
|
||||
const amountInputColor = useThemeColor('amountInput')
|
||||
const hintColor = useThemeColor('textDim')
|
||||
const inputText = useThemeColor('text')
|
||||
const inputBg = useThemeColor('background')
|
||||
@@ -1028,94 +1026,23 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
|
||||
})
|
||||
}
|
||||
|
||||
const convertedAmountColor = useThemeColor('headerSubTitle')
|
||||
|
||||
const getConvertedAmount = function () {
|
||||
if (!walletStore.exchangeRate) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const precision = getCurrency(unitRef.current).precision
|
||||
return convertToFromSats(
|
||||
round(toNumber(amountToSend) * precision, 0) || 0,
|
||||
getCurrency(unitRef.current).code,
|
||||
walletStore.exchangeRate
|
||||
)
|
||||
}
|
||||
|
||||
const isConvertedAmountVisible = function () {
|
||||
return (
|
||||
walletStore.exchangeRate &&
|
||||
(userSettingsStore.exchangeCurrency === getCurrency(unitRef.current).code ||
|
||||
unitRef.current === 'sat') &&
|
||||
getConvertedAmount() !== undefined
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Screen preset="fixed" contentContainerStyle={$screen}>
|
||||
<MintHeader
|
||||
mint={mintBalanceToSendFrom ? mintsStore.findByUrl(mintBalanceToSendFrom?.mintUrl) : undefined}
|
||||
<AmountInputHeader
|
||||
amountInputRef={amountInputRef}
|
||||
amountToSend={amountToSend}
|
||||
setAmountToSend={setAmountToSend}
|
||||
unit={unitRef.current}
|
||||
onAmountEndEditing={onAmountEndEditing}
|
||||
transactionStatus={transactionStatus}
|
||||
isCashuPrWithAmount={isCashuPrWithAmount}
|
||||
lockedPubkey={lockedPubkey}
|
||||
unitRef={unitRef}
|
||||
mintHeaderMint={mintBalanceToSendFrom ? mintsStore.findByUrl(mintBalanceToSendFrom?.mintUrl) : undefined}
|
||||
/>
|
||||
<View style={[$headerContainer, {backgroundColor: headerBg}]}>
|
||||
<View style={$amountContainer}>
|
||||
<AmountInput
|
||||
ref={amountInputRef}
|
||||
value={amountToSend}
|
||||
onChangeText={amount => setAmountToSend(amount)}
|
||||
unit={unitRef.current}
|
||||
onEndEditing={transactionStatus !== TransactionStatus.PENDING ? onAmountEndEditing : undefined}
|
||||
editable={(transactionStatus === TransactionStatus.PENDING || isCashuPrWithAmount)
|
||||
? false
|
||||
: true
|
||||
}
|
||||
style={{color: amountInputColor}}
|
||||
/>
|
||||
{isConvertedAmountVisible() && (
|
||||
<CurrencyAmount
|
||||
amount={getConvertedAmount() ?? 0}
|
||||
currencyCode={unitRef.current === 'sat' ? userSettingsStore.exchangeCurrency : CurrencyCode.SAT}
|
||||
symbolStyle={{color: convertedAmountColor, marginTop: spacing.tiny, fontSize: verticalScale(10)}}
|
||||
amountStyle={{color: convertedAmountColor, lineHeight: spacing.small}}
|
||||
size='small'
|
||||
containerStyle={{justifyContent: 'center'}}
|
||||
/>
|
||||
)}
|
||||
{lockedPubkey ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginTop: isConvertedAmountVisible() ? -spacing.extraSmall : undefined
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
icon="faLock"
|
||||
size={spacing.small}
|
||||
color={amountInputColor}
|
||||
/>
|
||||
<Text
|
||||
size='xs'
|
||||
tx="sendLocked"
|
||||
style={{color: amountInputColor, marginLeft: spacing.tiny}}
|
||||
/>
|
||||
|
||||
</View>
|
||||
) : (
|
||||
<Text
|
||||
size='xs'
|
||||
tx='amountSend'
|
||||
style={{
|
||||
color: amountInputColor,
|
||||
textAlign: 'center',
|
||||
marginTop: isConvertedAmountVisible() ? -spacing.extraSmall : undefined
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
<View style={$contentContainer}>
|
||||
{!encodedTokenToSend && (
|
||||
<MemoInputCard
|
||||
@@ -1833,14 +1760,6 @@ const $screen: ViewStyle = {
|
||||
flex: 1,
|
||||
}
|
||||
|
||||
const $headerContainer: TextStyle = {
|
||||
alignItems: 'center',
|
||||
padding: spacing.extraSmall,
|
||||
paddingTop: 0,
|
||||
height: spacing.screenHeight * 0.20,
|
||||
|
||||
}
|
||||
|
||||
const $pubkeyInput: TextStyle = {
|
||||
flex: 1,
|
||||
// borderRadius: 0,
|
||||
@@ -1852,19 +1771,6 @@ const $pubkeyInput: TextStyle = {
|
||||
// borderWidth: 1,
|
||||
}
|
||||
|
||||
const $amountContainer: ViewStyle = {
|
||||
}
|
||||
|
||||
const $amountInput: TextStyle = {
|
||||
borderRadius: spacing.small,
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
fontSize: verticalScale(48),
|
||||
fontFamily: typography.primary?.medium,
|
||||
textAlign: 'center',
|
||||
color: 'white',
|
||||
}
|
||||
|
||||
const $contentContainer: TextStyle = {
|
||||
flex: 1,
|
||||
padding: spacing.extraSmall,
|
||||
|
||||
@@ -15,7 +15,10 @@ import {useHeader} from '../utils/useHeader'
|
||||
import {useStores} from '../models'
|
||||
import {translate} from '../i18n'
|
||||
import { log } from '../services'
|
||||
import { Currencies, CurrencyCode } from '../services/wallet/currency'
|
||||
|
||||
import {Env} from '../utils/envtypes'
|
||||
import { round } from '../utils/number'
|
||||
import { Currencies, CurrencyCode, availableExchangeCurrencies } from '../services/wallet/currency'
|
||||
import { NotificationService } from '../services/notificationService'
|
||||
import { SvgXml } from 'react-native-svg'
|
||||
import { CurrencySign } from './Wallet/CurrencySign'
|
||||
@@ -531,7 +534,7 @@ export const SettingsScreen = observer(function SettingsScreen({ route }: Props)
|
||||
style={{alignItems: 'stretch'}}
|
||||
ContentComponent={
|
||||
<>
|
||||
{[CurrencyCode.USD, CurrencyCode.EUR].map(code =>
|
||||
{availableExchangeCurrencies.map(code =>
|
||||
<ListItem
|
||||
key={code}
|
||||
LeftComponent={
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from "react"
|
||||
import { TextStyle, View, ViewStyle } from "react-native"
|
||||
import { SvgXml } from "react-native-svg"
|
||||
import { Text } from "../../components"
|
||||
import { Spacing, colors, spacing, typography, useThemeColor } from "../../theme"
|
||||
import { Spacing, spacing, typography, useThemeColor } from "../../theme"
|
||||
import { CurrencyCode, Currencies, MintUnit, getCurrency } from "../../services/wallet/currency"
|
||||
|
||||
|
||||
@@ -17,12 +17,11 @@ export const CurrencySign = function(props: {
|
||||
const {currencyCode, mintUnit, containerStyle, textStyle, size} = props
|
||||
let code = currencyCode || CurrencyCode.SAT
|
||||
|
||||
if(!!mintUnit) {
|
||||
if(!!mintUnit && !currencyCode) {
|
||||
code = getCurrency(mintUnit).code
|
||||
}
|
||||
|
||||
const textColor = useThemeColor('amount')
|
||||
const bgColor = colors.palette.primary200
|
||||
|
||||
return (
|
||||
<View
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
import numbro from 'numbro'
|
||||
import { BtcIcon, EurIcon, UsdIcon } from '../../components'
|
||||
import { BtcIcon, EurIcon, UsdIcon, CadIcon } from '../../components'
|
||||
import AppError, { Err } from '../../utils/AppError'
|
||||
import { log } from '../logService'
|
||||
import { ExchangeRate } from '../../models/WalletStore'
|
||||
|
||||
|
||||
export type MintUnit = typeof MintUnits[number]
|
||||
export const MintUnits = ['btc', 'sat', 'msat', 'usd', 'eur'] as const
|
||||
|
||||
|
||||
export enum CurrencyCode {
|
||||
BTC = 'BTC', SAT = 'SAT', MSAT = 'MSAT', EUR = 'EUR', GBP = 'GBP',
|
||||
CZK = 'CZK', USD = 'USD', PLN = 'PLN', HUF = 'HUF', RON = 'RON',
|
||||
CAD = 'CAD'
|
||||
}
|
||||
|
||||
export type MintUnitCurrencyPair = {
|
||||
[key in MintUnit]: CurrencyCode
|
||||
}
|
||||
|
||||
/** add any currency codes you want to allow the user to select as "reference currency" in the app settings, here: */
|
||||
export const availableExchangeCurrencies = [CurrencyCode.USD, CurrencyCode.EUR, CurrencyCode.CAD] as const;
|
||||
|
||||
export const MintUnitCurrencyPairs: MintUnitCurrencyPair = {
|
||||
btc:CurrencyCode.BTC, sat:CurrencyCode.SAT, msat:CurrencyCode.MSAT, eur:CurrencyCode.EUR, usd:CurrencyCode.USD,
|
||||
}
|
||||
@@ -114,16 +118,24 @@ export const Currencies: CurrencyList = {
|
||||
precision: 100,
|
||||
mantissa: 2,
|
||||
},
|
||||
CAD: {
|
||||
symbol: 'CAD',
|
||||
title: 'Canadian Dollar',
|
||||
code: CurrencyCode.CAD,
|
||||
icon: CadIcon,
|
||||
precision: 100,
|
||||
mantissa: 2,
|
||||
},
|
||||
} as const
|
||||
|
||||
|
||||
export const formatCurrency = (amount: number, code: CurrencyCode) => {
|
||||
export const formatCurrency = (amount: number, code: CurrencyCode, thousandSeparated: boolean = true) => {
|
||||
const c = Currencies[code]
|
||||
if(!c || !c.precision) {
|
||||
throw new AppError(Err.VALIDATION_ERROR, `Currency code ${code} is not yet supported by Minibits. Submit request to add it on our Github.`)
|
||||
}
|
||||
|
||||
return numbro(amount / c.precision).format({ mantissa: c.mantissa, thousandSeparated: true })
|
||||
return numbro(amount / c.precision).format({ mantissa: c.mantissa, thousandSeparated })
|
||||
}
|
||||
|
||||
export const getCurrency = (unit: MintUnit) => {
|
||||
@@ -146,6 +158,15 @@ export const getCurrency = (unit: MintUnit) => {
|
||||
return currencyData as CurrencyData
|
||||
}
|
||||
|
||||
export const getCurrencyByCode = (code: CurrencyCode): CurrencyData | undefined => {
|
||||
for (const [currencyCode, currencyData] of Object.entries(Currencies)) {
|
||||
if (currencyCode === code && currencyData) {
|
||||
return currencyData
|
||||
}
|
||||
}
|
||||
return void 0;
|
||||
}
|
||||
|
||||
export const convertToFromSats = (amount: number, currencyFrom: CurrencyCode, satExchangeRate: ExchangeRate) => {
|
||||
// exchangeRate is always 1 fiat precision unit (cent) in SAT {currency: 'EUR', rate: 15.69}
|
||||
|
||||
@@ -154,5 +175,4 @@ export const convertToFromSats = (amount: number, currencyFrom: CurrencyCode, sa
|
||||
}
|
||||
|
||||
return amount * satExchangeRate.rate
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user