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
c8497a48
You need to sign in or sign up before continuing.
Commit
c8497a48
authored
Sep 14, 2026
by
ThinhNC
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(notifications): improve Zalo Bot linking flow with 2-step guide and dynamic URL support
parent
9a70ebeb
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
115 additions
and
52 deletions
+115
-52
en.json
src/i18n/locales/en.json
+17
-5
vi.json
src/i18n/locales/vi.json
+17
-5
NotificationSettings.tsx
src/pages/notifications/components/NotificationSettings.tsx
+80
-42
zalo-bot.service.ts
src/services/zalo-bot.service.ts
+1
-0
No files found.
src/i18n/locales/en.json
View file @
c8497a48
...
@@ -1297,17 +1297,29 @@
...
@@ -1297,17 +1297,29 @@
"linked"
:
"Zalo Bot linked"
,
"linked"
:
"Zalo Bot linked"
,
"unlink"
:
"Unlink"
,
"unlink"
:
"Unlink"
,
"saveChat"
:
"Save Chat ID"
,
"saveChat"
:
"Save Chat ID"
,
"oneClickBtn"
:
"
One-Click
Link via Zalo Bot"
,
"oneClickBtn"
:
"Link via Zalo Bot"
,
"oneClickTitle"
:
"Link Zalo Bot
(One-Click)
"
,
"oneClickTitle"
:
"Link Zalo Bot"
,
"oneClickDesc"
:
"Send this code to Bot Finwise on Zalo to automatically connect:"
,
"oneClickDesc"
:
"Send this code to Bot Finwise on Zalo to automatically connect:"
,
"openZalo"
:
"Open Zalo Chat"
,
"openZalo"
:
"Open Zalo Chat"
,
"copyCode"
:
"Copy Code"
,
"copyCode"
:
"Copy"
,
"copied"
:
"Code copied!"
,
"copyCommand"
:
"Copy"
,
"copied"
:
"Copied!"
,
"copiedCommand"
:
"Link command copied!"
,
"autoCopiedToast"
:
"Command copied! Opening Zalo..."
,
"waiting"
:
"Waiting for message on Zalo..."
,
"waiting"
:
"Waiting for message on Zalo..."
,
"successLinked"
:
"Zalo Bot linked successfully!"
,
"successLinked"
:
"Zalo Bot linked successfully!"
,
"codeExpires"
:
"Code expires in"
,
"codeExpires"
:
"Code expires in"
,
"manualToggle"
:
"Or enter Chat ID manually"
,
"manualToggle"
:
"Or enter Chat ID manually"
,
"hideManual"
:
"Hide manual input"
"hideManual"
:
"Hide manual input"
,
"guideTitle"
:
"How to link with Zalo Bot:"
,
"guideStep1"
:
"Click
\"
Open Zalo Chat
\"
or
\"
Copy Code
\"
(the link command is copied to your clipboard)."
,
"guideStep2"
:
"Open Zalo app → search for
\"
{{botName}}
\"
(or ID {{botId}}) → paste and send."
,
"step1Title"
:
"Copy the link command below:"
,
"step2Title"
:
"Copy link and open in Zalo:"
,
"step2Desc"
:
"Paste the link into Zalo's browser address bar → Open bot page → Send the command."
,
"copyUrl"
:
"Copy"
,
"copiedUrl"
:
"Copied!"
,
"openBot"
:
"Open Bot Finwise in Zalo"
}
}
},
},
"reminder"
:
{
"reminder"
:
{
...
...
src/i18n/locales/vi.json
View file @
c8497a48
...
@@ -1361,17 +1361,29 @@
...
@@ -1361,17 +1361,29 @@
"linked"
:
"Đã liên kết Zalo Bot"
,
"linked"
:
"Đã liên kết Zalo Bot"
,
"unlink"
:
"Hủy liên kết"
,
"unlink"
:
"Hủy liên kết"
,
"saveChat"
:
"Lưu Chat ID"
,
"saveChat"
:
"Lưu Chat ID"
,
"oneClickBtn"
:
"Kết nối
1 chạm
với Zalo Bot"
,
"oneClickBtn"
:
"Kết nối với Zalo Bot"
,
"oneClickTitle"
:
"Kết Nối Zalo Bot
(1 Chạm)
"
,
"oneClickTitle"
:
"Kết Nối Zalo Bot"
,
"oneClickDesc"
:
"Nhắn mã bên dưới cho Bot Finwise để hoàn tất kết nối tự động:"
,
"oneClickDesc"
:
"Nhắn mã bên dưới cho Bot Finwise để hoàn tất kết nối tự động:"
,
"openZalo"
:
"Mở khung chat Zalo Bot"
,
"openZalo"
:
"Mở khung chat Zalo Bot"
,
"copyCode"
:
"Sao chép mã"
,
"copyCode"
:
"Sao chép"
,
"copied"
:
"Đã sao chép mã!"
,
"copyCommand"
:
"Sao chép"
,
"copied"
:
"Đã sao chép!"
,
"copiedCommand"
:
"Đã sao chép lệnh liên kết!"
,
"autoCopiedToast"
:
"Đã sao chép lệnh! Đang chuyển sang Zalo..."
,
"waiting"
:
"Đang chờ nhận tin nhắn từ Zalo..."
,
"waiting"
:
"Đang chờ nhận tin nhắn từ Zalo..."
,
"successLinked"
:
"Đã liên kết Zalo Bot thành công!"
,
"successLinked"
:
"Đã liên kết Zalo Bot thành công!"
,
"codeExpires"
:
"Mã hết hạn sau"
,
"codeExpires"
:
"Mã hết hạn sau"
,
"manualToggle"
:
"Hoặc nhập Chat ID thủ công"
,
"manualToggle"
:
"Hoặc nhập Chat ID thủ công"
,
"hideManual"
:
"Ẩn nhập thủ công"
"hideManual"
:
"Ẩn nhập thủ công"
,
"guideTitle"
:
"Hướng dẫn gửi mã liên kết:"
,
"guideStep1"
:
"Bấm
\"
Mở khung chat
\"
hoặc
\"
Sao chép mã
\"
(lệnh sẽ tự động được lưu vào bộ nhớ tạm)."
,
"guideStep2"
:
"Mở ứng dụng Zalo → tìm kiếm
\"
{{botName}}
\"
(hoặc ID {{botId}}) → dán và gửi tin nhắn."
,
"step1Title"
:
"Sao chép lệnh liên kết bên dưới:"
,
"step2Title"
:
"Sao chép link và mở trong Zalo:"
,
"step2Desc"
:
"Dán link vào thanh địa chỉ trình duyệt Zalo → Mở trang bot → Nhắn lệnh."
,
"copyUrl"
:
"Sao chép"
,
"copiedUrl"
:
"Đã sao chép!"
,
"openBot"
:
"Mở Bot Finwise trong Zalo"
}
}
},
},
"reminder"
:
{
"reminder"
:
{
...
...
src/pages/notifications/components/NotificationSettings.tsx
View file @
c8497a48
...
@@ -23,6 +23,8 @@ export const NotificationSettings: React.FC<NotificationSettingsProps> = ({ sett
...
@@ -23,6 +23,8 @@ export const NotificationSettings: React.FC<NotificationSettingsProps> = ({ sett
const
[
linkCodeData
,
setLinkCodeData
]
=
useState
<
ZaloLinkCodeData
|
null
>
(
null
);
const
[
linkCodeData
,
setLinkCodeData
]
=
useState
<
ZaloLinkCodeData
|
null
>
(
null
);
const
[
timeLeft
,
setTimeLeft
]
=
useState
<
number
>
(
0
);
const
[
timeLeft
,
setTimeLeft
]
=
useState
<
number
>
(
0
);
const
[
isCopied
,
setIsCopied
]
=
useState
(
false
);
const
[
isCopied
,
setIsCopied
]
=
useState
(
false
);
const
[
isCopiedUrl
,
setIsCopiedUrl
]
=
useState
(
false
);
const
[
showManualInput
,
setShowManualInput
]
=
useState
(
false
);
const
[
showManualInput
,
setShowManualInput
]
=
useState
(
false
);
const
pollTimerRef
=
useRef
<
ReturnType
<
typeof
setInterval
>
|
null
>
(
null
);
const
pollTimerRef
=
useRef
<
ReturnType
<
typeof
setInterval
>
|
null
>
(
null
);
...
@@ -99,17 +101,16 @@ export const NotificationSettings: React.FC<NotificationSettingsProps> = ({ sett
...
@@ -99,17 +101,16 @@ export const NotificationSettings: React.FC<NotificationSettingsProps> = ({ sett
}
}
};
};
const
handleCopyCode
=
async
(
)
=>
{
const
handleCopyCode
=
(
isFullCommand
=
false
)
=>
{
if
(
!
linkCodeData
)
return
;
if
(
!
linkCodeData
)
return
;
try
{
const
textToCopy
=
isFullCommand
?
`/link
${
linkCodeData
.
linkCode
}
`
:
linkCodeData
.
linkCode
;
await
navigator
.
clipboard
.
writeText
(
linkCodeData
.
linkCode
);
// Fire-and-forget clipboard, không block hiệu ứng nút
setIsCopied
(
true
);
if
(
navigator
?.
clipboard
)
navigator
.
clipboard
.
writeText
(
textToCopy
).
catch
(()
=>
{});
setTimeout
(()
=>
setIsCopied
(
false
),
2000
);
setIsCopied
(
true
);
}
catch
{
setTimeout
(()
=>
setIsCopied
(
false
),
2000
);
// Fallback nếu clipboard API bị chặn
}
};
};
const
handleUnlink
=
async
()
=>
{
const
handleUnlink
=
async
()
=>
{
try
{
try
{
await
zaloBotService
.
unlink
();
await
zaloBotService
.
unlink
();
...
@@ -174,7 +175,6 @@ export const NotificationSettings: React.FC<NotificationSettingsProps> = ({ sett
...
@@ -174,7 +175,6 @@ export const NotificationSettings: React.FC<NotificationSettingsProps> = ({ sett
{
isZaloEnabled
&&
(
{
isZaloEnabled
&&
(
<
Card
className=
"p-5"
>
<
Card
className=
"p-5"
>
<
div
className=
"flex items-start gap-3"
>
<
div
className=
"flex items-start gap-3"
>
<
span
className=
"text-2xl"
>
💬
</
span
>
<
div
className=
"flex-1"
>
<
div
className=
"flex-1"
>
<
h2
className=
"clay-title-h3"
>
{
t
(
"notification.zaloBotLink.title"
)
}
</
h2
>
<
h2
className=
"clay-title-h3"
>
{
t
(
"notification.zaloBotLink.title"
)
}
</
h2
>
{
isZaloLinked
?
(
{
isZaloLinked
?
(
...
@@ -220,52 +220,90 @@ export const NotificationSettings: React.FC<NotificationSettingsProps> = ({ sett
...
@@ -220,52 +220,90 @@ export const NotificationSettings: React.FC<NotificationSettingsProps> = ({ sett
</
span
>
</
span
>
</
div
>
</
div
>
<
p
className=
"clay-caption text-xs text-blue-800"
>
{
/* Bước 1: Copy lệnh */
}
{
t
(
"notification.zaloBotLink.oneClickDesc"
)
}
<
div
className=
"flex flex-col gap-1.5"
>
</
p
>
<
div
className=
"flex items-center gap-2"
>
<
span
className=
"flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-blue-600 font-nunito text-[10px] font-bold text-white"
>
1
</
span
>
<
span
className=
"font-nunito text-xs font-bold text-blue-900"
>
{
t
(
"notification.zaloBotLink.step1Title"
)
}
</
span
>
</
div
>
<
div
className=
"flex items-center gap-2 rounded-clay-sm bg-white p-3 border border-blue-200 shadow-clay-pressed"
>
<
code
className=
"flex-1 font-mono text-sm font-extrabold tracking-widest text-blue-900 select-all"
>
/link
{
linkCodeData
.
linkCode
}
</
code
>
<
button
type=
"button"
onClick=
{
()
=>
handleCopyCode
(
true
)
}
className=
{
`shrink-0 rounded-clay-sm px-3 py-1.5 text-xs font-bold shadow-sm transition-colors flex items-center gap-1.5 ${isCopied ? "bg-green-600 text-white" : "bg-blue-600 text-white hover:bg-blue-700"}`
}
>
{
isCopied
?
(
<>
<
svg
className=
"w-3.5 h-3.5"
viewBox=
"0 0 24 24"
fill=
"none"
stroke=
"currentColor"
strokeWidth=
"3"
><
polyline
points=
"20 6 9 17 4 12"
></
polyline
></
svg
>
<
span
>
{
t
(
"notification.zaloBotLink.copied"
)
}
</
span
>
</>
)
:
(
<>
<
svg
className=
"w-3.5 h-3.5"
viewBox=
"0 0 24 24"
fill=
"none"
stroke=
"currentColor"
strokeWidth=
"2"
><
rect
x=
"9"
y=
"9"
width=
"13"
height=
"13"
rx=
"2"
ry=
"2"
></
rect
><
path
d=
"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
></
path
></
svg
>
<
span
>
{
t
(
"notification.zaloBotLink.copyCommand"
)
}
</
span
>
</>
)
}
</
button
>
</
div
>
</
div
>
{
/* Box hiển thị mã lớn */
}
{
/* Bước 2: Copy link bot */
}
<
div
className=
"flex items-center justify-between rounded-clay-sm bg-white p-3 border border-blue-200 shadow-clay-pressed"
>
<
div
className=
"flex flex-col gap-1.5"
>
<
span
className=
"font-mono text-xl font-extrabold tracking-widest text-blue-900"
>
<
div
className=
"flex items-center gap-2"
>
{
linkCodeData
.
linkCode
}
<
span
className=
"flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-blue-600 font-nunito text-[10px] font-bold text-white"
>
2
</
span
>
</
span
>
<
span
className=
"font-nunito text-xs font-bold text-blue-900"
>
{
t
(
"notification.zaloBotLink.step2Title"
)
}
</
span
>
<
button
</
div
>
type=
"button"
<
div
className=
"flex items-center gap-2 rounded-clay-sm bg-white p-3 border border-blue-200 shadow-clay-pressed"
>
onClick=
{
handleCopyCode
}
<
span
className=
"flex-1 font-mono text-xs font-bold text-blue-900 break-all select-all"
>
className=
"rounded-clay-sm bg-blue-100 px-2.5 py-1 text-xs font-bold text-blue-700 hover:bg-blue-200 transition-colors"
{
linkCodeData
.
deepLinkUrl
||
`https://bot.zaloplatforms.com/bots/${linkCodeData.botId || "3517263789471244097"}`
}
>
</
span
>
{
isCopied
?
t
(
"notification.zaloBotLink.copied"
)
:
t
(
"notification.zaloBotLink.copyCode"
)
}
<
button
</
button
>
type=
"button"
onClick=
{
()
=>
{
const
url
=
linkCodeData
.
deepLinkUrl
||
`https://bot.zaloplatforms.com/bots/${linkCodeData.botId || "3517263789471244097"}`
;
if
(
navigator
?.
clipboard
)
navigator
.
clipboard
.
writeText
(
url
).
catch
(()
=>
{});
setIsCopiedUrl
(
true
);
setTimeout
(()
=>
setIsCopiedUrl
(
false
),
2000
);
}
}
className=
{
`shrink-0 rounded-clay-sm px-3 py-1.5 text-xs font-bold shadow-sm transition-colors flex items-center gap-1.5 ${isCopiedUrl ? "bg-green-600 text-white" : "bg-blue-600 text-white hover:bg-blue-700"}`
}
>
{
isCopiedUrl
?
(
<>
<
svg
className=
"w-3.5 h-3.5"
viewBox=
"0 0 24 24"
fill=
"none"
stroke=
"currentColor"
strokeWidth=
"3"
><
polyline
points=
"20 6 9 17 4 12"
></
polyline
></
svg
>
<
span
>
{
t
(
"notification.zaloBotLink.copied"
)
}
</
span
>
</>
)
:
(
<>
<
svg
className=
"w-3.5 h-3.5"
viewBox=
"0 0 24 24"
fill=
"none"
stroke=
"currentColor"
strokeWidth=
"2"
><
rect
x=
"9"
y=
"9"
width=
"13"
height=
"13"
rx=
"2"
ry=
"2"
></
rect
><
path
d=
"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
></
path
></
svg
>
<
span
>
{
t
(
"notification.zaloBotLink.copyUrl"
)
}
</
span
>
</>
)
}
</
button
>
</
div
>
</
div
>
</
div
>
{
/* Nút mở thẳng sang Zalo */
}
<
div
className=
"flex items-center justify-between pt-1"
>
<
div
className=
"flex gap-2"
>
<
div
className=
"flex items-center gap-2"
>
<
a
<
span
className=
"h-2 w-2 rounded-full bg-blue-600 animate-ping"
></
span
>
href=
{
linkCodeData
.
deepLinkUrl
}
<
span
className=
"clay-caption text-xs text-blue-700 font-medium"
>
target=
"_blank"
{
t
(
"notification.zaloBotLink.waiting"
)
}
rel=
"noreferrer"
</
span
>
className=
"flex-1 rounded-clay-sm bg-blue-600 px-3 py-2 text-center font-nunito text-xs font-bold text-white shadow-clay-raised hover:bg-blue-700 transition-colors"
</
div
>
>
{
t
(
"notification.zaloBotLink.openZalo"
)
}
</
a
>
<
button
<
button
type=
"button"
type=
"button"
onClick=
{
()
=>
setLinkCodeData
(
null
)
}
onClick=
{
()
=>
setLinkCodeData
(
null
)
}
className=
"rounded-clay-sm border border-gray-300 bg-white px-3 py-
2
font-nunito text-xs font-semibold text-gray-600 hover:bg-gray-50"
className=
"rounded-clay-sm border border-gray-300 bg-white px-3 py-
1.5
font-nunito text-xs font-semibold text-gray-600 hover:bg-gray-50"
>
>
{
t
(
"common.cancel"
)
}
{
t
(
"common.cancel"
)
}
</
button
>
</
button
>
</
div
>
</
div
>
<
div
className=
"flex items-center justify-center gap-2 pt-1"
>
<
span
className=
"h-2 w-2 rounded-full bg-blue-600 animate-ping"
></
span
>
<
span
className=
"clay-caption text-xs text-blue-700 font-medium"
>
{
t
(
"notification.zaloBotLink.waiting"
)
}
</
span
>
</
div
>
</
div
>
</
div
>
)
}
)
}
{
/* TH3: Đã liên kết -> Hiện nút Hủy liên kết */
}
{
/* TH3: Đã liên kết -> Hiện nút Hủy liên kết */
}
{
isZaloLinked
&&
(
{
isZaloLinked
&&
(
<
div
className=
"flex items-center justify-between gap-3 pt-1"
>
<
div
className=
"flex items-center justify-between gap-3 pt-1"
>
...
...
src/services/zalo-bot.service.ts
View file @
c8497a48
...
@@ -3,6 +3,7 @@ import { apiClient } from "@/lib/api-client";
...
@@ -3,6 +3,7 @@ import { apiClient } from "@/lib/api-client";
export
interface
ZaloLinkCodeData
{
export
interface
ZaloLinkCodeData
{
linkCode
:
string
;
linkCode
:
string
;
expiresInSeconds
:
number
;
expiresInSeconds
:
number
;
botId
?:
string
;
botUsername
:
string
;
botUsername
:
string
;
botDisplayName
:
string
;
botDisplayName
:
string
;
deepLinkUrl
:
string
;
deepLinkUrl
:
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