Commit c8497a48 authored by ThinhNC's avatar ThinhNC

feat(notifications): improve Zalo Bot linking flow with 2-step guide and dynamic URL support

parent 9a70ebeb
...@@ -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": {
......
...@@ -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": {
......
...@@ -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">
......
...@@ -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;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment