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:
KraXen72
2025-08-18 21:54:51 +02:00
committed by GitHub
co-authored by minibits-cash
parent c83e90a36e
commit 34efa8cb9a
12 changed files with 364 additions and 144 deletions
+12
View File
@@ -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
+2 -6
View File
@@ -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
}
}
+17 -11
View File
@@ -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 = {
+272
View File
@@ -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>
}
+1
View File
@@ -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>`
+2 -1
View File
@@ -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 {
+2
View File
@@ -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"
+8 -6
View File
@@ -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
View File
@@ -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,
+5 -2
View File
@@ -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 -3
View File
@@ -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
+25 -5
View File
@@ -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
}