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
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
Show 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 @@
"linked"
:
"Zalo Bot linked"
,
"unlink"
:
"Unlink"
,
"saveChat"
:
"Save Chat ID"
,
"oneClickBtn"
:
"
One-Click
Link via Zalo Bot"
,
"oneClickTitle"
:
"Link Zalo Bot
(One-Click)
"
,
"oneClickBtn"
:
"Link via Zalo Bot"
,
"oneClickTitle"
:
"Link Zalo Bot"
,
"oneClickDesc"
:
"Send this code to Bot Finwise on Zalo to automatically connect:"
,
"openZalo"
:
"Open Zalo Chat"
,
"copyCode"
:
"Copy Code"
,
"copied"
:
"Code copied!"
,
"copyCode"
:
"Copy"
,
"copyCommand"
:
"Copy"
,
"copied"
:
"Copied!"
,
"copiedCommand"
:
"Link command copied!"
,
"autoCopiedToast"
:
"Command copied! Opening Zalo..."
,
"waiting"
:
"Waiting for message on Zalo..."
,
"successLinked"
:
"Zalo Bot linked successfully!"
,
"codeExpires"
:
"Code expires in"
,
"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"
:
{
...
...
src/i18n/locales/vi.json
View file @
c8497a48
...
...
@@ -1361,17 +1361,29 @@
"linked"
:
"Đã liên kết Zalo Bot"
,
"unlink"
:
"Hủy liên kết"
,
"saveChat"
:
"Lưu Chat ID"
,
"oneClickBtn"
:
"Kết nối
1 chạm
với Zalo Bot"
,
"oneClickTitle"
:
"Kết Nối Zalo Bot
(1 Chạm)
"
,
"oneClickBtn"
:
"Kết nối với Zalo Bot"
,
"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:"
,
"openZalo"
:
"Mở khung chat Zalo Bot"
,
"copyCode"
:
"Sao chép mã"
,
"copied"
:
"Đã sao chép mã!"
,
"copyCode"
:
"Sao chép"
,
"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..."
,
"successLinked"
:
"Đã liên kết Zalo Bot thành công!"
,
"codeExpires"
:
"Mã hết hạn sau"
,
"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"
:
{
...
...
src/pages/notifications/components/NotificationSettings.tsx
View file @
c8497a48
...
...
@@ -23,6 +23,8 @@ export const NotificationSettings: React.FC<NotificationSettingsProps> = ({ sett
const
[
linkCodeData
,
setLinkCodeData
]
=
useState
<
ZaloLinkCodeData
|
null
>
(
null
);
const
[
timeLeft
,
setTimeLeft
]
=
useState
<
number
>
(
0
);
const
[
isCopied
,
setIsCopied
]
=
useState
(
false
);
const
[
isCopiedUrl
,
setIsCopiedUrl
]
=
useState
(
false
);
const
[
showManualInput
,
setShowManualInput
]
=
useState
(
false
);
const
pollTimerRef
=
useRef
<
ReturnType
<
typeof
setInterval
>
|
null
>
(
null
);
...
...
@@ -99,17 +101,16 @@ export const NotificationSettings: React.FC<NotificationSettingsProps> = ({ sett
}
};
const
handleCopyCode
=
async
(
)
=>
{
const
handleCopyCode
=
(
isFullCommand
=
false
)
=>
{
if
(
!
linkCodeData
)
return
;
try
{
await
navigator
.
clipboard
.
writeText
(
linkCodeData
.
linkCode
);
const
textToCopy
=
isFullCommand
?
`/link
${
linkCodeData
.
linkCode
}
`
:
linkCodeData
.
linkCode
;
// Fire-and-forget clipboard, không block hiệu ứng nút
if
(
navigator
?.
clipboard
)
navigator
.
clipboard
.
writeText
(
textToCopy
).
catch
(()
=>
{});
setIsCopied
(
true
);
setTimeout
(()
=>
setIsCopied
(
false
),
2000
);
}
catch
{
// Fallback nếu clipboard API bị chặn
}
};
const
handleUnlink
=
async
()
=>
{
try
{
await
zaloBotService
.
unlink
();
...
...
@@ -174,7 +175,6 @@ export const NotificationSettings: React.FC<NotificationSettingsProps> = ({ sett
{
isZaloEnabled
&&
(
<
Card
className=
"p-5"
>
<
div
className=
"flex items-start gap-3"
>
<
span
className=
"text-2xl"
>
💬
</
span
>
<
div
className=
"flex-1"
>
<
h2
className=
"clay-title-h3"
>
{
t
(
"notification.zaloBotLink.title"
)
}
</
h2
>
{
isZaloLinked
?
(
...
...
@@ -220,52 +220,90 @@ export const NotificationSettings: React.FC<NotificationSettingsProps> = ({ sett
</
span
>
</
div
>
<
p
className=
"clay-caption text-xs text-blue-800"
>
{
t
(
"notification.zaloBotLink.oneClickDesc"
)
}
</
p
>
{
/* Box hiển thị mã lớn */
}
<
div
className=
"flex items-center justify-between rounded-clay-sm bg-white p-3 border border-blue-200 shadow-clay-pressed"
>
<
span
className=
"font-mono text-xl font-extrabold tracking-widest text-blue-900"
>
{
linkCodeData
.
linkCode
}
</
span
>
{
/* Bước 1: Copy lệnh */
}
<
div
className=
"flex flex-col gap-1.5"
>
<
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
}
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"
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
?
t
(
"notification.zaloBotLink.copied"
)
:
t
(
"notification.zaloBotLink.copyCode"
)
}
{
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
>
{
/*
Nút mở thẳng sang Zalo
*/
}
<
div
className=
"flex
gap-2
"
>
<
a
href=
{
linkCodeData
.
deepLinkUrl
}
target=
"_blank"
rel=
"noreferrer"
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"
>
{
t
(
"notification.zaloBotLink.openZalo"
)
}
</
a
>
{
/*
Bước 2: Copy link bot
*/
}
<
div
className=
"flex
flex-col gap-1.5
"
>
<
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"
>
2
</
span
>
<
span
className=
"font-nunito text-xs font-bold text-blue-900"
>
{
t
(
"notification.zaloBotLink.step2Title"
)
}
</
span
>
</
div
>
<
div
className=
"flex items-center gap-2 rounded-clay-sm bg-white p-3 border border-blue-200 shadow-clay-pressed"
>
<
span
className=
"flex-1 font-mono text-xs font-bold text-blue-900 break-all select-all"
>
{
linkCodeData
.
deepLinkUrl
||
`https://bot.zaloplatforms.com/bots/${linkCodeData.botId || "3517263789471244097"}`
}
</
span
>
<
button
type=
"button"
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"
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"}`
}
>
{
t
(
"common.cancel"
)
}
{
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
className=
"flex items-center justify-center gap-2 pt-1"
>
<
div
className=
"flex items-center justify-between pt-1"
>
<
div
className=
"flex items-center gap-2"
>
<
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
>
<
button
type=
"button"
onClick=
{
()
=>
setLinkCodeData
(
null
)
}
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"
)
}
</
button
>
</
div
>
</
div
>
)
}
{
/* TH3: Đã liên kết -> Hiện nút Hủy liên kết */
}
{
isZaloLinked
&&
(
<
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";
export
interface
ZaloLinkCodeData
{
linkCode
:
string
;
expiresInSeconds
:
number
;
botId
?:
string
;
botUsername
:
string
;
botDisplayName
:
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