Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
F
finwise-miniapp-fe
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ThinhNC
finwise-miniapp-fe
Commits
27d86b4d
Commit
27d86b4d
authored
Sep 12, 2026
by
ThinhNC
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(calculator): integrate real-time market exchange rates with live badge and offline fallback
parent
c4c60432
Changes
6
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
298 additions
and
39 deletions
+298
-39
CalculatorModal.tsx
src/components/ui/CalculatorModal.tsx
+131
-22
en.json
src/i18n/locales/en.json
+7
-1
vi.json
src/i18n/locales/vi.json
+7
-1
ai-assistant.service.ts
src/services/ai-assistant.service.ts
+18
-0
currency.service.ts
src/services/currency.service.ts
+119
-15
ai.ts
src/types/ai.ts
+16
-0
No files found.
src/components/ui/CalculatorModal.tsx
View file @
27d86b4d
This diff is collapsed.
Click to expand it.
src/i18n/locales/en.json
View file @
27d86b4d
...
...
@@ -2210,6 +2210,12 @@
"thb"
:
"Thai Baht"
,
"sgd"
:
"Singapore Dollar"
,
"aud"
:
"Australian Dollar"
,
"cad"
:
"Canadian Dollar"
"cad"
:
"Canadian Dollar"
,
"updatingRate"
:
"Updating market rate..."
,
"liveBadge"
:
"Live Market"
,
"estimated"
:
"Baseline"
,
"refreshRate"
:
"Refresh market rate"
,
"sameCurrencyNote"
:
"Exchange rate for identical currencies"
}
}
src/i18n/locales/vi.json
View file @
27d86b4d
...
...
@@ -2311,6 +2311,12 @@
"thb"
:
"Baht Thái"
,
"sgd"
:
"Đô la Singapore"
,
"aud"
:
"Đô la Úc"
,
"cad"
:
"Đô la Canada"
"cad"
:
"Đô la Canada"
,
"updatingRate"
:
"Đang cập nhật tỷ giá..."
,
"liveBadge"
:
"Thị trường"
,
"estimated"
:
"Cơ sở"
,
"refreshRate"
:
"Cập nhật tỷ giá thị trường"
,
"sameCurrencyNote"
:
"Tỷ giá giữa cùng một loại tiền tệ"
}
}
src/services/ai-assistant.service.ts
View file @
27d86b4d
...
...
@@ -10,6 +10,8 @@ import {
CategorizeTransactionData
,
ExtractReceiptInput
,
ExtractReceiptData
,
AIExchangeRateInput
,
AIExchangeRateData
,
AIServiceResponse
,
}
from
"@/types/ai"
;
...
...
@@ -142,4 +144,20 @@ export const aiAssistantService = {
return
handleAIError
(
error
);
}
},
async
getExchangeRate
(
input
:
AIExchangeRateInput
):
Promise
<
AIServiceResponse
<
AIExchangeRateData
>>
{
try
{
const
response
=
await
apiClient
.
post
<
AIServiceResponse
<
AIExchangeRateData
>>
(
"/ai-assistant/exchange-rate"
,
input
,
{
timeout
:
30000
}
);
return
response
.
data
;
}
catch
(
error
)
{
return
handleAIError
(
error
);
}
},
};
src/services/currency.service.ts
View file @
27d86b4d
...
...
@@ -26,16 +26,16 @@ export const POPULAR_CURRENCIES: CurrencyItem[] = [
*/
export
const
DEFAULT_VND_RATES
:
ExchangeRateMap
=
{
VND
:
1
,
USD
:
25
450
,
EUR
:
27600
,
JPY
:
16
5
,
KRW
:
19
,
CNY
:
3
550
,
GBP
:
3
2800
,
THB
:
7
40
,
SGD
:
19400
,
AUD
:
1
6800
,
CAD
:
18
500
,
USD
:
25
922.52
,
EUR
:
30067.05
,
JPY
:
16
8.3
,
KRW
:
19
.3
,
CNY
:
3
864.5
,
GBP
:
3
5022.8
,
THB
:
7
84.2
,
SGD
:
20448.9
,
AUD
:
1
8591.4
,
CAD
:
18
710.2
,
};
const
CACHE_KEY
=
"finwise.currency_rates"
;
...
...
@@ -132,7 +132,39 @@ export async function fetchLatestRates(): Promise<ExchangeRateMap> {
}
}
// Try fetching USD-based rates from free open API
// 1. Try FloatRates (interbank feed)
try
{
const
response
=
await
fetch
(
"https://www.floatrates.com/daily/usd.json"
);
if
(
response
.
ok
)
{
const
data
=
await
response
.
json
();
if
(
data
&&
data
.
vnd
?.
rate
)
{
const
usdVnd
=
parseFloat
(
String
(
data
.
vnd
.
rate
));
if
(
!
isNaN
(
usdVnd
)
&&
usdVnd
>
0
)
{
const
newVndRates
:
ExchangeRateMap
=
{
VND
:
1
,
USD
:
usdVnd
};
for
(
const
[
codeLower
,
info
]
of
Object
.
entries
(
data
))
{
const
code
=
codeLower
.
toUpperCase
();
const
item
=
info
as
{
rate
?:
string
|
number
;
inverseRate
?:
string
|
number
};
if
(
item
?.
rate
)
{
const
foreignRateAgainstUsd
=
parseFloat
(
String
(
item
.
rate
));
if
(
!
isNaN
(
foreignRateAgainstUsd
)
&&
foreignRateAgainstUsd
>
0
)
{
newVndRates
[
code
]
=
Math
.
round
((
usdVnd
/
foreignRateAgainstUsd
)
*
100
)
/
100
;
}
}
}
const
merged
=
{
...
DEFAULT_VND_RATES
,
...
newVndRates
};
try
{
localStorage
.
setItem
(
CACHE_KEY
,
JSON
.
stringify
(
merged
));
localStorage
.
setItem
(
CACHE_TIME_KEY
,
String
(
now
));
}
catch
{}
return
merged
;
}
}
}
}
catch
{
// Continue to open.er-api fallback
}
// 2. Try fetching USD-based rates from open exchange API
const
response
=
await
fetch
(
"https://open.er-api.com/v6/latest/USD"
);
if
(
!
response
.
ok
)
{
return
getCachedRates
();
...
...
@@ -148,7 +180,6 @@ export async function fetchLatestRates(): Promise<ExchangeRateMap> {
for
(
const
[
code
,
rateAgainstUsd
]
of
Object
.
entries
(
usdRates
))
{
const
rateNum
=
Number
(
rateAgainstUsd
);
if
(
rateNum
>
0
)
{
// 1 USD = rateNum [CODE] => 1 [CODE] = (usdVnd / rateNum) VND
newVndRates
[
code
]
=
Math
.
round
((
usdVnd
/
rateNum
)
*
100
)
/
100
;
}
}
...
...
@@ -157,9 +188,7 @@ export async function fetchLatestRates(): Promise<ExchangeRateMap> {
try
{
localStorage
.
setItem
(
CACHE_KEY
,
JSON
.
stringify
(
merged
));
localStorage
.
setItem
(
CACHE_TIME_KEY
,
String
(
now
));
}
catch
{
// Storage might be full
}
}
catch
{}
return
merged
;
}
}
catch
{
...
...
@@ -168,3 +197,78 @@ export async function fetchLatestRates(): Promise<ExchangeRateMap> {
return
getCachedRates
();
}
/**
* Fetch real-time live exchange rate for a specific pair from interbank feed (FloatRates / Open Exchange Rates)
* with robust offline fallback to cached rates. No AI tokens consumed.
*/
export
async
function
fetchLiveExchangeRate
(
from
:
string
,
to
:
string
):
Promise
<
{
rate
:
number
;
isLive
:
boolean
;
note
?:
string
}
>
{
const
fromCode
=
from
.
toUpperCase
().
trim
();
const
toCode
=
to
.
toUpperCase
().
trim
();
if
(
fromCode
===
toCode
)
{
return
{
rate
:
1
,
isLive
:
true
,
note
:
"Tỷ giá giữa cùng một loại tiền tệ"
};
}
// 1. Try FloatRates directly for real-time interbank market rate
try
{
const
controller
=
new
AbortController
();
const
timeout
=
setTimeout
(()
=>
controller
.
abort
(),
3500
);
const
res
=
await
fetch
(
`https://www.floatrates.com/daily/
${
fromCode
.
toLowerCase
()}
.json`
,
{
signal
:
controller
.
signal
,
});
clearTimeout
(
timeout
);
if
(
res
.
ok
)
{
const
data
=
await
res
.
json
();
const
target
=
data
[
toCode
.
toLowerCase
()];
if
(
target
?.
rate
)
{
const
rate
=
parseFloat
(
String
(
target
.
rate
));
if
(
!
isNaN
(
rate
)
&&
rate
>
0
)
{
return
{
rate
,
isLive
:
true
,
note
:
"Tỷ giá thị trường liên ngân hàng (FloatRates)"
,
};
}
}
}
}
catch
{
// Ignore network error and continue
}
// 2. Try Open Exchange Rates API
try
{
const
controller
=
new
AbortController
();
const
timeout
=
setTimeout
(()
=>
controller
.
abort
(),
3500
);
const
res
=
await
fetch
(
`https://open.er-api.com/v6/latest/
${
fromCode
}
`
,
{
signal
:
controller
.
signal
,
});
clearTimeout
(
timeout
);
if
(
res
.
ok
)
{
const
data
=
await
res
.
json
();
const
targetRate
=
data
.
rates
?.[
toCode
];
if
(
typeof
targetRate
===
"number"
&&
targetRate
>
0
)
{
return
{
rate
:
targetRate
,
isLive
:
true
,
note
:
"Tỷ giá thị trường mở (Open Exchange Rates)"
,
};
}
}
}
catch
{
// Ignore network error
}
// 3. Fallback to cached or DEFAULT_VND_RATES
const
cachedRates
=
getCachedRates
();
const
fallbackRate
=
getExchangeRate
(
fromCode
,
toCode
,
cachedRates
);
return
{
rate
:
fallbackRate
,
isLive
:
false
,
note
:
"Tỷ giá thị trường cơ sở (ngoại tuyến)"
,
};
}
src/types/ai.ts
View file @
27d86b4d
...
...
@@ -160,3 +160,19 @@ export interface AIRateLimitError {
code
:
string
;
retryAfterSeconds
:
number
;
}
export
interface
AIExchangeRateInput
{
from
:
string
;
to
:
string
;
amount
?:
number
;
}
export
interface
AIExchangeRateData
{
from
:
string
;
to
:
string
;
rate
:
number
;
amount
:
number
;
convertedAmount
:
number
;
formattedRate
?:
string
;
note
?:
string
;
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment