Skip to content

Commit c790903

Browse files
Fix(mobile): Create a new fiat formatter and implement its usage (#5205)
* fix(mobile): use polyfills for Intl.formatNumber (#5362) We rely on `notation: „compact“`, but it is not available in Hermes yet. We can use some polyfills from formatjs to do the trick for us. formatjs/formatjs#4463 (comment) --------- Co-authored-by: Daniel Dimitrov <[email protected]>
1 parent 4957a0a commit c790903

File tree

10 files changed

+181
-33
lines changed

10 files changed

+181
-33
lines changed

apps/mobile/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@
4242
"@ethersproject/shims": "^5.7.0",
4343
"@expo/config-plugins": "^9.0.10",
4444
"@expo/vector-icons": "^14.0.2",
45+
"@formatjs/intl-getcanonicallocales": "^2.5.4",
46+
"@formatjs/intl-locale": "^4.2.10",
47+
"@formatjs/intl-numberformat": "^8.15.3",
48+
"@formatjs/intl-pluralrules": "^5.4.3",
4549
"@notifee/react-native": "^9.1.8",
4650
"@react-native-clipboard/clipboard": "^1.15.0",
4751
"@react-native-community/blur": "^4.4.1",

apps/mobile/src/app/_layout.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import '@/src/config/polyfills'
12
import { Stack } from 'expo-router'
23
import 'react-native-reanimated'
34
import { SafeThemeProvider } from '@/src/theme/provider/safeTheme'

apps/mobile/src/components/Fiat/Fiat.test.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import { render } from '@/src/tests/test-utils'
22
import { Fiat } from '.'
33

44
describe('Fiat', () => {
5-
it('should render the default markup', () => {
6-
const { getByText } = render(<Fiat baseAmount="215,531.65" />)
5+
it('should render the formatted value correctly', () => {
6+
const container = render(<Fiat value="215531.65" currency="usd" />)
7+
const fiatBalanceDisplay = container.getByTestId('fiat-balance-display')
78

8-
expect(getByText('$')).toBeTruthy()
9-
expect(getByText('215,531')).toBeTruthy()
10-
expect(getByText('.65')).toBeTruthy()
9+
expect(fiatBalanceDisplay).toBeVisible()
10+
expect(fiatBalanceDisplay).toHaveTextContent('$ 215.53K')
1111
})
1212
})

apps/mobile/src/components/Fiat/Fiat.tsx

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,46 @@
1-
import React from 'react'
2-
import { H1, H3, View } from 'tamagui'
1+
import React, { useMemo } from 'react'
2+
import { H1, View } from 'tamagui'
3+
import { formatCurrency, formatCurrencyPrecise } from '@safe-global/utils/formatNumber'
34

45
interface FiatProps {
5-
baseAmount: string
6+
value: string
7+
currency: string
8+
maxLength?: number
9+
precise?: boolean
610
}
711

8-
export const Fiat = ({ baseAmount }: FiatProps) => {
9-
const amount = baseAmount.split('.')
12+
export const Fiat = ({ value, currency, maxLength, precise }: FiatProps) => {
13+
const fiat = useMemo(() => {
14+
return formatCurrency(value, currency, maxLength)
15+
}, [value, currency, maxLength])
1016

11-
return (
12-
<View flexDirection="row" alignItems="center">
13-
<H3 fontWeight="600">$</H3>
14-
<H1 fontWeight="600">{amount[0]}</H1>
17+
const preciseFiat = useMemo(() => {
18+
return formatCurrencyPrecise(value, currency)
19+
}, [value, currency])
20+
21+
const [whole, decimals, endCurrency] = useMemo(() => {
22+
const match = (preciseFiat ?? '').match(/(.+)(\D\d+)(\D+)?$/)
23+
return match ? match.slice(1) : ['', preciseFiat, '', '']
24+
}, [preciseFiat])
1525

16-
{amount[1] && (
17-
<H1 fontWeight={600} color="$textSecondaryDark">
18-
.{amount[1].slice(0, 2)}
26+
if (fiat == null) {
27+
return <H1 fontWeight="600">--</H1>
28+
}
29+
30+
return (
31+
<View flexDirection="row" alignItems="center" testID={'fiat-balance-display'}>
32+
{precise ? (
33+
<H1 fontWeight="600">
34+
{whole}
35+
{decimals && (
36+
<H1 fontWeight={600} color="$textSecondaryDark">
37+
{decimals}
38+
</H1>
39+
)}
40+
{endCurrency}
1941
</H1>
42+
) : (
43+
<H1 fontWeight="600">{fiat}</H1>
2044
)}
2145
</View>
2246
)

apps/mobile/src/config/polyfills.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Hermes's implementation of Intl is not up to date and is missing some features. We rely mainly on `notation: "compact"`
3+
* to properly format currency values, but it is not supported in Hermes.
4+
* https://github.com/facebook/hermes/blob/5911e8180796d3ccb2669237ca441da717ad00b2/doc/IntlAPIs.md#limited-ios-property-support
5+
*
6+
* We need to polyfill some Intl APIs to make it work, and it seems that formatjs is doing the trick for us
7+
*/
8+
9+
// Don't remove -force from these because detection is VERY slow on low-end Android.
10+
// https://github.com/formatjs/formatjs/issues/4463#issuecomment-2176070577
11+
12+
// https://github.com/formatjs/formatjs/blob/main/packages/intl-getcanonicallocales/polyfill-force.ts
13+
import '@formatjs/intl-getcanonicallocales/polyfill-force'
14+
// https://github.com/formatjs/formatjs/blob/main/packages/intl-locale/polyfill-force.ts
15+
import '@formatjs/intl-locale/polyfill-force'
16+
// https://github.com/formatjs/formatjs/blob/main/packages/intl-pluralrules/polyfill-force.ts
17+
import '@formatjs/intl-pluralrules/polyfill-force'
18+
// https://github.com/formatjs/formatjs/blob/main/packages/intl-numberformat/polyfill-force.ts
19+
import '@formatjs/intl-numberformat/polyfill-force'
20+
21+
import '@formatjs/intl-pluralrules/locale-data/en'
22+
import '@formatjs/intl-numberformat/locale-data/en'

apps/mobile/src/features/Assets/components/Balance/Balance.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export function Balance({ activeChainId, chains, isLoading, balanceAmount, chain
3838
{isLoading ? (
3939
<Spinner />
4040
) : balanceAmount ? (
41-
<Fiat baseAmount={balanceAmount} />
41+
<Fiat value={balanceAmount} currency="usd" precise={balanceAmount.length < 6} />
4242
) : (
4343
<Alert type="error" message="error while getting the balance of your wallet" />
4444
)}

apps/mobile/src/features/Assets/components/Tokens/Tokens.container.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ describe('TokensContainer', () => {
4848
// Then check for content
4949
const ethText = await screen.findByText('Ethereum')
5050
const ethAmount = await screen.findByText('1 ETH')
51-
const ethValue = await screen.findByText('$2000')
51+
const ethValue = await screen.findByText('$ 2,000')
5252

5353
expect(ethText).toBeTruthy()
5454
expect(ethAmount).toBeTruthy()

apps/mobile/src/features/Assets/components/Tokens/Tokens.container.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ import { AssetsCard } from '@/src/components/transactions-list/Card/AssetsCard'
88
import { POLLING_INTERVAL } from '@/src/config/constants'
99
import { selectActiveSafe } from '@/src/store/activeSafeSlice'
1010
import { Balance, useBalancesGetBalancesV1Query } from '@safe-global/store/gateway/AUTO_GENERATED/balances'
11-
import { formatValue } from '@/src/utils/formatters'
12-
1311
import { Fallback } from '../Fallback'
1412
import { skipToken } from '@reduxjs/toolkit/query'
13+
import { formatCurrency } from '@safe-global/utils/formatNumber'
14+
import { formatVisualAmount } from '@safe-global/utils/formatters'
1515

1616
export function TokensContainer() {
1717
const activeSafe = useSelector(selectActiveSafe)
1818

19-
const { data, isFetching, error } = useBalancesGetBalancesV1Query(
19+
const { data, isFetching, error, isLoading } = useBalancesGetBalancesV1Query(
2020
!activeSafe
2121
? skipToken
2222
: {
@@ -36,17 +36,17 @@ export function TokensContainer() {
3636
<AssetsCard
3737
name={item.tokenInfo.name}
3838
logoUri={item.tokenInfo.logoUri}
39-
description={`${formatValue(item.balance, item.tokenInfo.decimals as number)} ${item.tokenInfo.symbol}`}
39+
description={`${formatVisualAmount(item.balance, item.tokenInfo.decimals as number)} ${item.tokenInfo.symbol}`}
4040
rightNode={
4141
<Text fontSize="$4" fontWeight={400} color="$color">
42-
${item.fiatBalance}
42+
{formatCurrency(item.fiatBalance, 'usd')}
4343
</Text>
4444
}
4545
/>
4646
)
4747
}, [])
4848

49-
if (isFetching || !data?.items.length || error) {
49+
if (isLoading || !data?.items.length || error) {
5050
return <Fallback loading={isFetching} hasError={!!error} />
5151
}
5252

apps/mobile/src/features/NetworksSheet/NetworksSheet.container.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { SafeOverviewResult } from '@safe-global/store/gateway/types'
1010
import { makeSafeId } from '@/src/utils/formatters'
1111
import { POLLING_INTERVAL } from '@/src/config/constants'
1212
import { useDefinedActiveSafe } from '@/src/store/hooks/activeSafe'
13+
import { formatCurrency } from '@safe-global/utils/formatNumber'
1314

1415
export const NetworksSheetContainer = () => {
1516
const dispatch = useAppDispatch()
@@ -45,7 +46,7 @@ export const NetworksSheetContainer = () => {
4546
onClose()
4647
}}
4748
activeChain={activeChain}
48-
fiatTotal={item.fiatTotal}
49+
fiatTotal={formatCurrency(item.fiatTotal, 'usd')}
4950
chains={chains}
5051
chainId={item.chainId}
5152
key={item.chainId}

yarn.lock

Lines changed: 103 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5060,6 +5060,18 @@ __metadata:
50605060
languageName: node
50615061
linkType: hard
50625062

5063+
"@formatjs/ecma402-abstract@npm:2.3.3":
5064+
version: 2.3.3
5065+
resolution: "@formatjs/ecma402-abstract@npm:2.3.3"
5066+
dependencies:
5067+
"@formatjs/fast-memoize": "npm:2.2.6"
5068+
"@formatjs/intl-localematcher": "npm:0.6.0"
5069+
decimal.js: "npm:10"
5070+
tslib: "npm:2"
5071+
checksum: 10/c73a704d5cba3b929a9a303a04b4e8a708bb2dea000ee41808c55b22941a70da01b065697cbc5a25898b2007405abd71f414ab8c327fe953f63ae4120c2cc98d
5072+
languageName: node
5073+
linkType: hard
5074+
50635075
"@formatjs/fast-memoize@npm:1.2.1":
50645076
version: 1.2.1
50655077
resolution: "@formatjs/fast-memoize@npm:1.2.1"
@@ -5069,6 +5081,15 @@ __metadata:
50695081
languageName: node
50705082
linkType: hard
50715083

5084+
"@formatjs/fast-memoize@npm:2.2.6":
5085+
version: 2.2.6
5086+
resolution: "@formatjs/fast-memoize@npm:2.2.6"
5087+
dependencies:
5088+
tslib: "npm:2"
5089+
checksum: 10/efa5601dddbd94412ee567d5d067dfd206afa2d08553435f6938e69acba3309b83b9b15021cd30550d5fb93817a53b7691098a11a73f621c2d9318efad49fd76
5090+
languageName: node
5091+
linkType: hard
5092+
50725093
"@formatjs/icu-messageformat-parser@npm:2.1.0":
50735094
version: 2.1.0
50745095
resolution: "@formatjs/icu-messageformat-parser@npm:2.1.0"
@@ -5090,6 +5111,37 @@ __metadata:
50905111
languageName: node
50915112
linkType: hard
50925113

5114+
"@formatjs/intl-enumerator@npm:1.8.9":
5115+
version: 1.8.9
5116+
resolution: "@formatjs/intl-enumerator@npm:1.8.9"
5117+
dependencies:
5118+
"@formatjs/ecma402-abstract": "npm:2.3.3"
5119+
tslib: "npm:2"
5120+
checksum: 10/cc7290147ce6331b8803307af70a68583392fb5df4b74e2441cbda766528c6104ec96f31eaa5a41478236e04b579bea65a1a819f7c7292c891fe063701f157f6
5121+
languageName: node
5122+
linkType: hard
5123+
5124+
"@formatjs/intl-getcanonicallocales@npm:2.5.4, @formatjs/intl-getcanonicallocales@npm:^2.5.4":
5125+
version: 2.5.4
5126+
resolution: "@formatjs/intl-getcanonicallocales@npm:2.5.4"
5127+
dependencies:
5128+
tslib: "npm:2"
5129+
checksum: 10/69c2cf0b0fc9f85cd7845b27733b5c9796a935f30968ab446f6d32a8563edeba9ee1abd680763e66e7543d17244729d3f474e06582b217ef63023237490d73d9
5130+
languageName: node
5131+
linkType: hard
5132+
5133+
"@formatjs/intl-locale@npm:^4.2.10":
5134+
version: 4.2.10
5135+
resolution: "@formatjs/intl-locale@npm:4.2.10"
5136+
dependencies:
5137+
"@formatjs/ecma402-abstract": "npm:2.3.3"
5138+
"@formatjs/intl-enumerator": "npm:1.8.9"
5139+
"@formatjs/intl-getcanonicallocales": "npm:2.5.4"
5140+
tslib: "npm:2"
5141+
checksum: 10/f6e5508ade093135df82da070436898027184d61a62f804e775d28bbfb0bd7512c593823362630cb3a2d5c86ebde5fe8a3c07891a7a94a6d03016a045006832b
5142+
languageName: node
5143+
linkType: hard
5144+
50935145
"@formatjs/intl-localematcher@npm:0.2.25":
50945146
version: 0.2.25
50955147
resolution: "@formatjs/intl-localematcher@npm:0.2.25"
@@ -5099,6 +5151,39 @@ __metadata:
50995151
languageName: node
51005152
linkType: hard
51015153

5154+
"@formatjs/intl-localematcher@npm:0.6.0":
5155+
version: 0.6.0
5156+
resolution: "@formatjs/intl-localematcher@npm:0.6.0"
5157+
dependencies:
5158+
tslib: "npm:2"
5159+
checksum: 10/d8fd984c14121949d0ba60732a096aed6dccb2ab93770c4bffaea1170c85f5639d2a71fe6e2c68ab62bf6f3583a9c4b1fcd11bd5fb8837bfb2582228c33398c1
5160+
languageName: node
5161+
linkType: hard
5162+
5163+
"@formatjs/intl-numberformat@npm:^8.15.3":
5164+
version: 8.15.3
5165+
resolution: "@formatjs/intl-numberformat@npm:8.15.3"
5166+
dependencies:
5167+
"@formatjs/ecma402-abstract": "npm:2.3.3"
5168+
"@formatjs/intl-localematcher": "npm:0.6.0"
5169+
decimal.js: "npm:10"
5170+
tslib: "npm:2"
5171+
checksum: 10/5688885dadcb6d56b552893a99024fe56e89f2323507af229780e1477dceb6dc21bc74f3460ce8016364ebbfe03d9b3f4e3026c56de7a475735a1f37147cf528
5172+
languageName: node
5173+
linkType: hard
5174+
5175+
"@formatjs/intl-pluralrules@npm:^5.4.3":
5176+
version: 5.4.3
5177+
resolution: "@formatjs/intl-pluralrules@npm:5.4.3"
5178+
dependencies:
5179+
"@formatjs/ecma402-abstract": "npm:2.3.3"
5180+
"@formatjs/intl-localematcher": "npm:0.6.0"
5181+
decimal.js: "npm:10"
5182+
tslib: "npm:2"
5183+
checksum: 10/99192aef736bf2b3642f4e5eaea68fc6443823222a6c48edd6a111d8e1e4e969a3c5e24acb33f2bc375d55e84b829fb387a4eee7e2612f8cf7565fbd43c3f1dd
5184+
languageName: node
5185+
linkType: hard
5186+
51025187
"@gnosis.pm/mock-contract@npm:^4.0.0":
51035188
version: 4.0.0
51045189
resolution: "@gnosis.pm/mock-contract@npm:4.0.0"
@@ -7699,6 +7784,10 @@ __metadata:
76997784
"@ethersproject/shims": "npm:^5.7.0"
77007785
"@expo/config-plugins": "npm:^9.0.10"
77017786
"@expo/vector-icons": "npm:^14.0.2"
7787+
"@formatjs/intl-getcanonicallocales": "npm:^2.5.4"
7788+
"@formatjs/intl-locale": "npm:^4.2.10"
7789+
"@formatjs/intl-numberformat": "npm:^8.15.3"
7790+
"@formatjs/intl-pluralrules": "npm:^5.4.3"
77027791
"@gorhom/bottom-sheet": "npm:^5.1.1"
77037792
"@notifee/react-native": "npm:^9.1.8"
77047793
"@react-native-async-storage/async-storage": "npm:1.23.1"
@@ -16260,6 +16349,13 @@ __metadata:
1626016349
languageName: node
1626116350
linkType: hard
1626216351

16352+
"decimal.js@npm:10":
16353+
version: 10.5.0
16354+
resolution: "decimal.js@npm:10.5.0"
16355+
checksum: 10/714d49cf2f2207b268221795ede330e51452b7c451a0c02a770837d2d4faed47d603a729c2aa1d952eb6c4102d999e91c9b952c1aa016db3c5cba9fc8bf4cda2
16356+
languageName: node
16357+
linkType: hard
16358+
1626316359
"decimal.js@npm:^10.4.2":
1626416360
version: 10.4.3
1626516361
resolution: "decimal.js@npm:10.4.3"
@@ -31124,20 +31220,20 @@ __metadata:
3112431220
languageName: node
3112531221
linkType: hard
3112631222

31223+
"tslib@npm:2, tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.6.2, tslib@npm:^2.7.0, tslib@npm:^2.8.0, tslib@npm:^2.8.1":
31224+
version: 2.8.1
31225+
resolution: "tslib@npm:2.8.1"
31226+
checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7
31227+
languageName: node
31228+
linkType: hard
31229+
3112731230
"tslib@npm:2.7.0":
3112831231
version: 2.7.0
3112931232
resolution: "tslib@npm:2.7.0"
3113031233
checksum: 10/9a5b47ddac65874fa011c20ff76db69f97cf90c78cff5934799ab8894a5342db2d17b4e7613a087046bc1d133d21547ddff87ac558abeec31ffa929c88b7fce6
3113131234
languageName: node
3113231235
linkType: hard
3113331236

31134-
"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.6.2, tslib@npm:^2.7.0, tslib@npm:^2.8.0, tslib@npm:^2.8.1":
31135-
version: 2.8.1
31136-
resolution: "tslib@npm:2.8.1"
31137-
checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7
31138-
languageName: node
31139-
linkType: hard
31140-
3114131237
"tty-browserify@npm:^0.0.1":
3114231238
version: 0.0.1
3114331239
resolution: "tty-browserify@npm:0.0.1"

0 commit comments

Comments
 (0)