Skip to content

Region settings #1197

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Feb 3, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tall-foxes-eat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@alephium/mobile-wallet": patch
---

Format amounts based on user's locale
5 changes: 5 additions & 0 deletions .changeset/wild-jobs-train.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@alephium/mobile-wallet": patch
---

Tap to reveal hidden amounts
2 changes: 1 addition & 1 deletion apps/desktop-wallet/src/App.tsx
Original file line number Diff line number Diff line change
@@ -14,12 +14,12 @@ import useTrackUserSettings from '@/features/analytics/useTrackUserSettings'
import AutoUpdateSnackbar from '@/features/autoUpdate/AutoUpdateSnackbar'
import { languageOptions } from '@/features/localization/languages'
import { systemLanguageMatchFailed, systemLanguageMatchSucceeded } from '@/features/localization/localizationActions'
import useRegionOptions from '@/features/settings/regionSettings/useRegionOptions'
import {
localStorageGeneralSettingsMigrated,
systemRegionMatchFailed,
systemRegionMatchSucceeded
} from '@/features/settings/settingsActions'
import useRegionOptions from '@/features/settings/useRegionOptions'
import { darkTheme, lightTheme } from '@/features/theme/themes'
import { WalletConnectContextProvider } from '@/features/walletConnect/walletConnectContext'
import { useAppDispatch, useAppSelector } from '@/hooks/redux'
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@ const useTrackUserSettings = () => {
const passwordRequirement = useAppSelector(selectEffectivePasswordRequirement)
const fiatCurrency = useAppSelector((s) => s.settings.fiatCurrency)
const network = useAppSelector((s) => s.network.name)
const region = useAppSelector((s) => s.settings.region)

useEffect(() => {
if (posthog.__loaded)
@@ -27,7 +28,8 @@ const useTrackUserSettings = () => {
language,
passwordRequirement,
fiatCurrency,
network
network,
region
})
}, [
devTools,
@@ -38,6 +40,7 @@ const useTrackUserSettings = () => {
passwordRequirement,
posthog.__loaded,
posthog.people,
region,
theme,
walletLockTimeInMinutes
])
Original file line number Diff line number Diff line change
@@ -3,8 +3,8 @@ import { useTranslation } from 'react-i18next'
import KeyValueInput from '@/components/Inputs/InlineLabelValueInput'
import Select from '@/components/Inputs/Select'
import useAnalytics from '@/features/analytics/useAnalytics'
import useRegionOptions from '@/features/settings/regionSettings/useRegionOptions'
import { numberFormatRegionChanged } from '@/features/settings/settingsActions'
import useRegionOptions from '@/features/settings/useRegionOptions'
import { useAppDispatch, useAppSelector } from '@/hooks/redux'

const RegionSettings = () => {
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { upperFirst } from 'lodash'
import { useMemo } from 'react'

import { Language } from '@/features/localization/languages'
import { useAppSelector } from '@/hooks/redux'

import regionsLocales from './regions.json'
@@ -14,6 +13,11 @@ const useRegionOptions = () => {

export default useRegionOptions

const getRegionsOptions = (languageLocale: string) =>
regionsLocales
.map((regionLocale) => getRegionOption(regionLocale, languageLocale))
.sort((a, b) => a.label.localeCompare(b.label))

// Inspired by https://github.com/LedgerHQ/ledger-live/blob/065dda3/apps/ledger-live-desktop/src/renderer/screens/settings/sections/General/RegionSelect.tsx
const getRegionOption = (regionLocale: string, languageLocale: string | Intl.Locale) => {
const [language, region = ''] = regionLocale.split('-')
@@ -32,8 +36,3 @@ const getRegionOption = (regionLocale: string, languageLocale: string | Intl.Loc
label
}
}

const getRegionsOptions = (languageLocale: Language) =>
regionsLocales
.map((regionLocale) => getRegionOption(regionLocale, languageLocale))
.sort((a, b) => a.label.localeCompare(b.label))
2 changes: 2 additions & 0 deletions apps/mobile-wallet/locales/en-US/translation.json
Original file line number Diff line number Diff line change
@@ -456,5 +456,7 @@
"Splitting your funds into multiple addresses can help you stay organized and increase privacy.": "Splitting your funds into multiple addresses can help you stay organized and increase privacy.",
"No dApps match your search: \"{{ searchText }}\"": "No dApps match your search: \"{{ searchText }}\"",
"Visit dApp": "Visit dApp",
"Region": "Region",
"Choose your region to update formats of dates, time, and currencies.": "Choose your region to update formats of dates, time, and currencies.",
"More details on Alph.land": "More details on Alph.land"
}
2 changes: 2 additions & 0 deletions apps/mobile-wallet/src/App.tsx
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ import { DefaultTheme, ThemeProvider } from 'styled-components/native'
import ToastAnchor from '~/components/toasts/ToastAnchor'
import LoadingManager from '~/features/loader/LoadingManager'
import { useLocalization } from '~/features/localization/useLocalization'
import { useSystemRegion } from '~/features/settings/regionSettings/useSystemRegion'
import useLoadStoredSettings from '~/features/settings/useLoadStoredSettings'
import { useAppDispatch, useAppSelector } from '~/hooks/redux'
import { useAsyncData } from '~/hooks/useAsyncData'
@@ -129,6 +130,7 @@ const Main = ({ children, ...props }: ViewProps) => {
useLoadStoredSettings()
useInitializeClient()
useLocalization()
useSystemRegion()

useEffect(() => {
if (walletMetadata) dispatch(appLaunchedWithLastUsedWallet(walletMetadata))
62 changes: 36 additions & 26 deletions apps/mobile-wallet/src/components/Amount.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { convertToPositive, formatAmountForDisplay, formatFiatAmountForDisplay } from '@alephium/shared'
import { convertToPositive, formatAmountForDisplay } from '@alephium/shared'
import { useState } from 'react'
import { StyleProp, TextStyle } from 'react-native'

import { useAppSelector } from '~/hooks/redux'
@@ -9,7 +10,6 @@ export interface AmountProps extends AppTextProps {
value?: bigint | number
decimals?: number
isFiat?: boolean
fadeDecimals?: boolean
fullPrecision?: boolean
nbOfDecimalsToShow?: number
suffix?: string
@@ -24,7 +24,6 @@ export interface AmountProps extends AppTextProps {

const Amount = ({
value,
fadeDecimals,
fullPrecision = false,
suffix = '',
showOnDiscreetMode = false,
@@ -40,59 +39,70 @@ const Amount = ({
...props
}: AmountProps) => {
const discreetMode = useAppSelector((state) => state.settings.discreetMode)
const region = useAppSelector((state) => state.settings.region)
const fiatCurrency = useAppSelector((state) => state.settings.currency)

const [tappedToDisableDiscreetMode, setTappedToDisableDiscreetMode] = useState(false)

const hideAmount = discreetMode && !showOnDiscreetMode && !tappedToDisableDiscreetMode

const handleTappedToDisableDiscreetMode = () => setTappedToDisableDiscreetMode(!tappedToDisableDiscreetMode)

let quantitySymbol = ''
let amount = ''
let tinyAmount = ''
let isNegative = false
const color = props.color ?? (highlight && value !== undefined ? (value < 0 ? 'send' : 'receive') : 'primary')

if (value !== undefined) {
isNegative = value < 0

if (isFiat && typeof value === 'number') {
amount = formatFiatAmountForDisplay(isNegative ? value * -1 : value)
amount = new Intl.NumberFormat(region, { style: 'currency', currency: fiatCurrency }).format(value)

return (
<AppText {...props} {...{ color, style }} onPress={handleTappedToDisableDiscreetMode}>
{hideAmount ? '•••' : amount}
</AppText>
)
} else if (isUnknownToken) {
amount = convertToPositive(value as bigint).toString()
} else {
amount = formatAmountForDisplay({
amount: convertToPositive(value as bigint),
amountDecimals: decimals,
displayDecimals: nbOfDecimalsToShow,
fullPrecision
fullPrecision,
region
})
}

if (fadeDecimals && ['K', 'M', 'B', 'T'].some((char) => amount.endsWith(char))) {
quantitySymbol = amount.slice(-1)
amount = amount.slice(0, -1)
}
}

let [integralPart, fractionalPart] = amount.split('.')
const amountIsTooSmall = formatAmountForDisplay({
amount: convertToPositive(value as bigint),
amountDecimals: decimals,
displayDecimals: nbOfDecimalsToShow,
fullPrecision
}).startsWith('0.0000')

if (useTinyAmountShorthand && amount.startsWith('0.0000')) {
integralPart = '< 0'
fractionalPart = '0001'
tinyAmount =
useTinyAmountShorthand && amountIsTooSmall
? formatAmountForDisplay({ amount: BigInt(1), amountDecimals: 4, region })
: ''
}
}

const color = props.color ?? (highlight && value !== undefined ? (value < 0 ? 'send' : 'receive') : 'primary')
const fadedColor = fadeDecimals ? 'secondary' : color

return (
<AppText {...props} {...{ color, style }}>
{discreetMode && !showOnDiscreetMode ? (
<AppText {...props} {...{ color, style }} onPress={handleTappedToDisableDiscreetMode}>
{hideAmount ? (
'•••'
) : integralPart ? (
) : amount ? (
<>
{showPlusMinus && (
<AppText {...props} color={color}>
{isNegative ? '-' : '+'}
</AppText>
)}
<AppText {...props} color={color}>
{integralPart}
{tinyAmount ? `< ${tinyAmount}` : amount}
</AppText>
{fractionalPart && <AppText {...props} color={fadedColor}>{`.${fractionalPart}`}</AppText>}
{quantitySymbol && <AppText {...props} color={fadedColor}>{` ${quantitySymbol} `}</AppText>}
{!isUnknownToken && (
<AppText {...props} color={fadeSuffix ? 'secondary' : color}>{` ${suffix || 'ALPH'}`}</AppText>
)}
2 changes: 1 addition & 1 deletion apps/mobile-wallet/src/components/ConsolidationModal.tsx
Original file line number Diff line number Diff line change
@@ -42,7 +42,7 @@ const ConsolidationModal = withModal<ConsolidationModalProps>(({ id, onConsolida
</AppText>
<Fee>
<AppText>{t('Fee')}:</AppText>
<Amount value={fees} fullPrecision fadeDecimals bold />
<Amount value={fees} fullPrecision bold />
</Fee>
</View>
</ScreenSection>
3 changes: 3 additions & 0 deletions apps/mobile-wallet/src/features/modals/AppModals.tsx
Original file line number Diff line number Diff line change
@@ -31,6 +31,7 @@ import TokenAmountModal from '~/features/send/modals/TokenAmountModal'
import CurrencySelectModal from '~/features/settings/CurrencySelectModal'
import EditWalletNameModal from '~/features/settings/EditWalletNameModal'
import MnemonicModal from '~/features/settings/MnemonicModal'
import RegionSelectModal from '~/features/settings/regionSettings/RegionSelectModal'
import SafePlaceWarningModal from '~/features/settings/SafePlaceWarningModal'
import WalletDeleteModal from '~/features/settings/WalletDeleteModal'
import TransactionModal from '~/features/transactionsDisplay/TransactionModal'
@@ -130,6 +131,8 @@ const AppModals = () => {
return <DAppQuickActionsModal key={id} id={id} {...params.props} />
case 'DAppDetailsModal':
return <DAppDetailsModal key={id} id={id} {...params.props} />
case 'RegionSelectModal':
return <RegionSelectModal key={id} id={id} />
default:
return null
}
4 changes: 3 additions & 1 deletion apps/mobile-wallet/src/features/modals/modalTypes.ts
Original file line number Diff line number Diff line change
@@ -29,6 +29,7 @@ import TokenAmountModal from '~/features/send/modals/TokenAmountModal'
import CurrencySelectModal from '~/features/settings/CurrencySelectModal'
import EditWalletNameModal from '~/features/settings/EditWalletNameModal'
import MnemonicModal from '~/features/settings/MnemonicModal'
import RegionSelectModal from '~/features/settings/regionSettings/RegionSelectModal'
import SafePlaceWarningModal from '~/features/settings/SafePlaceWarningModal'
import WalletDeleteModal from '~/features/settings/WalletDeleteModal'
import TransactionModal from '~/features/transactionsDisplay/TransactionModal'
@@ -76,7 +77,8 @@ export const ModalComponents = {
AddressQRCodeScanActionsModal,
AddressPickerQuickActionsModal,
DAppQuickActionsModal,
DAppDetailsModal
DAppDetailsModal,
RegionSelectModal
}

export type ModalName = keyof typeof ModalComponents
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { FlashList } from '@shopify/flash-list'
import { useTranslation } from 'react-i18next'

import { sendAnalytics } from '~/analytics'
import RadioButtonRow from '~/components/RadioButtonRow'
import BottomModalFlashList from '~/features/modals/BottomModalFlashList'
import { closeModal } from '~/features/modals/modalActions'
import withModal from '~/features/modals/withModal'
import { numberFormatRegionChanged } from '~/features/settings/regionSettings/regionSettingsActions'
import { regionOptions } from '~/features/settings/regionSettings/regionsUtils'
import { useAppDispatch, useAppSelector } from '~/hooks/redux'

const RegionSelectModal = withModal(({ id }) => {
const dispatch = useAppDispatch()
const { t } = useTranslation()
const currentRegion = useAppSelector((s) => s.settings.region)

const handleRegionChange = (region: string) => {
dispatch(numberFormatRegionChanged(region))
dispatch(closeModal({ id }))
sendAnalytics({ event: 'Region changed', props: { region } })
}

return (
<BottomModalFlashList
modalId={id}
title={t('Region')}
flashListRender={(props) => (
<FlashList
data={regionOptions}
estimatedItemSize={65}
renderItem={({ item: regionOption, index }) => (
<RadioButtonRow
key={regionOption.label}
title={regionOption.label}
onPress={() => handleRegionChange(regionOption.value)}
isActive={currentRegion === regionOption.value}
isLast={index === regionOptions.length - 1}
/>
)}
{...props}
/>
)}
/>
)
})

export default RegionSelectModal
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useTranslation } from 'react-i18next'

import AppText from '~/components/AppText'
import Row from '~/components/Row'
import { openModal } from '~/features/modals/modalActions'
import { regionOptions } from '~/features/settings/regionSettings/regionsUtils'
import { useAppDispatch, useAppSelector } from '~/hooks/redux'

const RegionSettingsRow = () => {
const currentRegion = useAppSelector((s) => s.settings.region)
const { t } = useTranslation()
const dispatch = useAppDispatch()

const openRegionSelectModal = () => dispatch(openModal({ name: 'RegionSelectModal' }))

return (
<Row onPress={openRegionSelectModal} title={t('Region')}>
<AppText bold style={{ textAlign: 'right' }}>
{regionOptions.find((region) => region.value === currentRegion)?.label}
</AppText>
</Row>
)
}

export default RegionSettingsRow
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createAction } from '@reduxjs/toolkit'

import { Settings } from '~/types/settings'

export const numberFormatRegionChanged = createAction<Settings['general']['region']>(
'settings/numberFormatRegionChanged'
)

export const systemRegionMatchSucceeded = createAction<string>('settings/systemRegionMatchSucceeded')

export const systemRegionMatchFailed = createAction('settings/systemRegionMatchFailed')
1,578 changes: 1,578 additions & 0 deletions apps/mobile-wallet/src/features/settings/regionSettings/regions.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { upperFirst } from 'lodash'

import regionByKeys from './regions.json'

// See https://github.com/LedgerHQ/ledger-live/blob/3308c61ab3749b293167bb7485360a66187ae9eb/apps/ledger-live-mobile/src/screens/Settings/General/Region.ts
export const regionOptions = Object.keys(regionByKeys)
.map((key) => {
const { languageDisplayName, regionDisplayName } = regionByKeys[key as keyof typeof regionByKeys]
const label = `${upperFirst(regionDisplayName)} (${upperFirst(languageDisplayName)})`

return {
value: key,
label
}
})
.sort((a, b) => a.label.localeCompare(b.label))
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useEffect } from 'react'
import { getLocales } from 'react-native-localize'

import {
systemRegionMatchFailed,
systemRegionMatchSucceeded
} from '~/features/settings/regionSettings/regionSettingsActions'
import { regionOptions } from '~/features/settings/regionSettings/regionsUtils'
import { useAppDispatch, useAppSelector } from '~/hooks/redux'

export const useSystemRegion = () => {
const region = useAppSelector((s) => s.settings.region)
const settingsLoadedFromStorage = useAppSelector((s) => s.settings.loadedFromStorage)
const dispatch = useAppDispatch()

useEffect(() => {
if (!settingsLoadedFromStorage || region !== undefined) return

const locales = getLocales()

if (!locales.length) {
dispatch(systemRegionMatchFailed())
} else {
const systemLanguage = locales[0].languageTag

if (systemLanguage && regionOptions.find((option) => option.value === systemLanguage)) {
dispatch(systemRegionMatchSucceeded(systemLanguage))
} else {
dispatch(systemRegionMatchFailed())
}
}
}, [dispatch, region, settingsLoadedFromStorage])
}
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@ export const defaultGeneralSettings: GeneralSettings = {
walletConnect: false,
usesBiometrics: false,
language: undefined,
region: undefined,
autoLockSeconds: 0
}

Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ import Toggle from '~/components/Toggle'
import { useWalletConnectContext } from '~/contexts/walletConnect/WalletConnectContext'
import { languageOptions } from '~/features/localization/languages'
import { openModal } from '~/features/modals/modalActions'
import RegionSettingsRow from '~/features/settings/regionSettings/RegionSettingsRow'
import SettingsAssetsSection from '~/features/settings/settingsScreen/SettingsAssetsSection'
import SettingsSecuritySection from '~/features/settings/settingsScreen/SettingsSecuritySection'
import {
@@ -121,6 +122,9 @@ const SettingsScreen = ({ navigation, ...props }: ScreenProps) => {
<Row onPress={openLanguageSelectModal} title="Language">
<AppText bold>{languageOptions.find((l) => l.value === language)?.label}</AppText>
</Row>

<RegionSettingsRow />

<Row onPress={openCurrencySelectModal} title={t('Currency')}>
<AppText bold>{currentCurrency}</AppText>
</Row>
13 changes: 13 additions & 0 deletions apps/mobile-wallet/src/features/settings/settingsSlice.ts
Original file line number Diff line number Diff line change
@@ -6,6 +6,11 @@ import {
systemLanguageMatchFailed,
systemLanguageMatchSucceeded
} from '~/features/localization/localizationActions'
import {
numberFormatRegionChanged,
systemRegionMatchFailed,
systemRegionMatchSucceeded
} from '~/features/settings/regionSettings/regionSettingsActions'
import { allBiometricsEnabled, analyticsIdGenerated } from '~/features/settings/settingsActions'
import { defaultGeneralSettings, persistSettings } from '~/features/settings/settingsPersistentStorage'
import { RootState } from '~/store/store'
@@ -72,6 +77,12 @@ const settingsSlice = createSlice({
.addCase(languageChanged, (state, action) => {
state.language = action.payload
})
.addCase(systemRegionMatchFailed, (state) => {
state.region = 'en-US'
})
.addMatcher(isAnyOf(numberFormatRegionChanged, systemRegionMatchSucceeded), (state, action) => {
state.region = action.payload
})
}
})

@@ -102,6 +113,8 @@ settingsListenerMiddleware.startListening({
autoLockSecondsChanged,
allBiometricsEnabled,
languageChanged,
systemRegionMatchSucceeded,
systemRegionMatchFailed,
appReset
),
effect: async (_, { getState }) => {
Original file line number Diff line number Diff line change
@@ -79,7 +79,7 @@ const TransactionModal = withModal<TransactionModalProps>(({ id, tx }) => {
<Badge color={status.color}>{status.text}</Badge>
</Row>
<Row title={t('Fee')} transparent>
<Amount value={BigInt(tx.gasPrice) * BigInt(tx.gasAmount)} fadeDecimals fullPrecision bold showOnDiscreetMode />
<Amount value={BigInt(tx.gasPrice) * BigInt(tx.gasAmount)} fullPrecision bold showOnDiscreetMode />
</Row>
{isMoved && (
<Row title={t('Moved')} transparent>
4 changes: 2 additions & 2 deletions apps/mobile-wallet/src/screens/AddressDiscoveryScreen.tsx
Original file line number Diff line number Diff line change
@@ -141,7 +141,7 @@ const AddressDiscoveryScreen = ({ navigation, route: { params }, ...props }: Scr
truncate
isLast={index === addresses.length - 1}
>
<Amount value={BigInt(address.balance)} fadeDecimals bold />
<Amount value={BigInt(address.balance)} bold />
</Row>
))}
</Surface>
@@ -171,7 +171,7 @@ const AddressDiscoveryScreen = ({ navigation, route: { params }, ...props }: Scr
isLast={index === discoveredAddresses.length - 1}
>
<AmountContent>
<AmountStyled value={BigInt(balance)} color="secondary" fadeDecimals />
<AmountStyled value={BigInt(balance)} color="secondary" />
<Checkbox
value={addressSelections[hash]}
disabled={loading}
1 change: 1 addition & 0 deletions apps/mobile-wallet/src/types/settings.ts
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ export interface GeneralSettings {
usesBiometrics: boolean
autoLockSeconds: number
language?: Language
region: string | undefined
}

export interface Settings {