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
if (navigator?.clipboard) navigator.clipboard.writeText(textToCopy).catch(() => {});
setIsCopied(true); setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000); setTimeout(() => setIsCopied(false), 2000);
} catch {
// 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>
{/* Box hiển thị mã lớn */} <span className="font-nunito text-xs font-bold text-blue-900">{t("notification.zaloBotLink.step1Title")}</span>
<div className="flex items-center justify-between rounded-clay-sm bg-white p-3 border border-blue-200 shadow-clay-pressed"> </div>
<span className="font-mono text-xl font-extrabold tracking-widest text-blue-900"> <div className="flex items-center gap-2 rounded-clay-sm bg-white p-3 border border-blue-200 shadow-clay-pressed">
{linkCodeData.linkCode} <code className="flex-1 font-mono text-sm font-extrabold tracking-widest text-blue-900 select-all">
</span> /link {linkCodeData.linkCode}
</code>
<button <button
type="button" type="button"
onClick={handleCopyCode} onClick={() => handleCopyCode(true)}
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" 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> </button>
</div> </div>
</div>
{/* Nút mở thẳng sang Zalo */} {/* Bước 2: Copy link bot */}
<div className="flex gap-2"> <div className="flex flex-col gap-1.5">
<a <div className="flex items-center gap-2">
href={linkCodeData.deepLinkUrl} <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>
target="_blank" <span className="font-nunito text-xs font-bold text-blue-900">{t("notification.zaloBotLink.step2Title")}</span>
rel="noreferrer" </div>
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 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">
{t("notification.zaloBotLink.openZalo")} {linkCodeData.deepLinkUrl || `https://bot.zaloplatforms.com/bots/${linkCodeData.botId || "3517263789471244097"}`}
</a> </span>
<button <button
type="button" type="button"
onClick={() => setLinkCodeData(null)} onClick={() => {
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" 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> </button>
</div> </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="h-2 w-2 rounded-full bg-blue-600 animate-ping"></span>
<span className="clay-caption text-xs text-blue-700 font-medium"> <span className="clay-caption text-xs text-blue-700 font-medium">
{t("notification.zaloBotLink.waiting")} {t("notification.zaloBotLink.waiting")}
</span> </span>
</div> </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> </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