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 @@
"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": {
......
......@@ -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": {
......
......@@ -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">
......
......@@ -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;
......
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