138 lines
4.9 KiB
TypeScript
138 lines
4.9 KiB
TypeScript
|
|
// components/account/ConnectKofi.tsx
|
|||
|
|
"use client";
|
|||
|
|
|
|||
|
|
import { useCallback, useMemo, useState } from "react";
|
|||
|
|
|
|||
|
|
export default function ConnectKofi({
|
|||
|
|
email,
|
|||
|
|
userId,
|
|||
|
|
}: {
|
|||
|
|
email?: string | null;
|
|||
|
|
userId?: string | null;
|
|||
|
|
}) {
|
|||
|
|
const [value, setValue] = useState(email || "");
|
|||
|
|
const [busy, setBusy] = useState(false);
|
|||
|
|
const [msg, setMsg] = useState<string | null>(null);
|
|||
|
|
const [err, setErr] = useState<string | null>(null);
|
|||
|
|
const canSubmit = useMemo(
|
|||
|
|
() => !!value && /\S+@\S+\.\S+/.test(value),
|
|||
|
|
[value]
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const startClaim = useCallback(async () => {
|
|||
|
|
setErr(null);
|
|||
|
|
setMsg(null);
|
|||
|
|
setBusy(true);
|
|||
|
|
try {
|
|||
|
|
const res = await fetch("/api/support/kofi/claim/start", {
|
|||
|
|
method: "POST",
|
|||
|
|
headers: {
|
|||
|
|
"Content-Type": "application/json",
|
|||
|
|
// if your auth middleware expects anything, set it here; otherwise cookies suffice
|
|||
|
|
"x-user-id": userId ?? "",
|
|||
|
|
"x-user-email": email ?? "",
|
|||
|
|
},
|
|||
|
|
credentials: "include",
|
|||
|
|
body: JSON.stringify({ email: value }),
|
|||
|
|
});
|
|||
|
|
const j = await res.json().catch(() => ({} as any));
|
|||
|
|
if (!res.ok) {
|
|||
|
|
// Show specific errors where helpful
|
|||
|
|
const detail = j?.detail || j?.error || res.statusText;
|
|||
|
|
throw new Error(
|
|||
|
|
j?.error === "not_found"
|
|||
|
|
? "We don’t have any Ko-fi records for that email yet. If you’re sure it’s correct, try again after your next Ko-fi payment or after we run the backfill."
|
|||
|
|
: String(detail || "Failed to start verification")
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
if (j?.alreadyLinked) {
|
|||
|
|
setMsg("This Ko-fi email is already linked to your account.");
|
|||
|
|
} else {
|
|||
|
|
setMsg(
|
|||
|
|
"Verification email sent! Check your inbox and click the link to finish linking Ko-fi."
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
} catch (e: any) {
|
|||
|
|
setErr(e?.message || "Something went wrong.");
|
|||
|
|
} finally {
|
|||
|
|
setBusy(false);
|
|||
|
|
}
|
|||
|
|
}, [value, userId, email]);
|
|||
|
|
|
|||
|
|
const unlink = useCallback(async () => {
|
|||
|
|
setErr(null);
|
|||
|
|
setMsg(null);
|
|||
|
|
setBusy(true);
|
|||
|
|
try {
|
|||
|
|
const res = await fetch("/api/support/kofi/unlink", {
|
|||
|
|
method: "POST",
|
|||
|
|
headers: {
|
|||
|
|
"Content-Type": "application/json",
|
|||
|
|
"x-user-id": userId ?? "",
|
|||
|
|
},
|
|||
|
|
credentials: "include",
|
|||
|
|
});
|
|||
|
|
if (!res.ok) {
|
|||
|
|
const t = await res.text().catch(() => "");
|
|||
|
|
throw new Error(t || "Unlink failed");
|
|||
|
|
}
|
|||
|
|
setMsg("Ko-fi has been unlinked from your account.");
|
|||
|
|
} catch (e: any) {
|
|||
|
|
setErr(e?.message || "Unlink failed.");
|
|||
|
|
} finally {
|
|||
|
|
setBusy(false);
|
|||
|
|
}
|
|||
|
|
}, [userId]);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="rounded-md border p-4">
|
|||
|
|
<h3 className="mb-2 text-base font-semibold">Link Ko-fi</h3>
|
|||
|
|
<p className="mb-3 text-sm opacity-80">
|
|||
|
|
Enter the email you use on Ko-fi. We’ll send a one-time verification link to confirm it’s you.
|
|||
|
|
</p>
|
|||
|
|
|
|||
|
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
|||
|
|
<input
|
|||
|
|
type="email"
|
|||
|
|
className="w-full rounded border px-3 py-2 text-sm"
|
|||
|
|
placeholder="you@kofi-email.com"
|
|||
|
|
value={value}
|
|||
|
|
onChange={(e) => setValue(e.target.value)}
|
|||
|
|
disabled={busy}
|
|||
|
|
/>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={startClaim}
|
|||
|
|
disabled={!canSubmit || busy}
|
|||
|
|
className="inline-flex items-center justify-center rounded bg-black px-4 py-2 text-sm font-medium text-white disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white dark:text-black"
|
|||
|
|
>
|
|||
|
|
{busy ? "Sending…" : "Send Verify Link"}
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={unlink}
|
|||
|
|
disabled={busy}
|
|||
|
|
className="inline-flex items-center justify-center rounded border px-4 py-2 text-sm"
|
|||
|
|
title="Remove the Ko-fi link from your account"
|
|||
|
|
>
|
|||
|
|
Unlink
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{msg && (
|
|||
|
|
<div className="mt-3 rounded-md border border-emerald-300/50 bg-emerald-50 p-2 text-sm text-emerald-900">
|
|||
|
|
{msg}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
{err && (
|
|||
|
|
<div className="mt-3 rounded-md border border-red-300/50 bg-red-50 p-2 text-sm text-red-900">
|
|||
|
|
{err}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
<p className="mt-3 text-xs opacity-70">
|
|||
|
|
Tip: after you verify, badges update automatically. If you don’t see a badge yet, it’ll appear the next time a Ko-fi payment webhook arrives (or after backfill).
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|