feat: paste Meta token after App Review; Google-only discover
While Meta review is pending, agency can match Google Ads alone and paste the system user token later without a redeploy. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -23,9 +23,12 @@ type ProviderResult = {
|
||||
|
||||
export default function AdsDiscoverPanel() {
|
||||
const [metaOk, setMetaOk] = useState(false);
|
||||
const [metaPending, setMetaPending] = useState(true);
|
||||
const [gadsOk, setGadsOk] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [running, setRunning] = useState(false);
|
||||
const [savingToken, setSavingToken] = useState(false);
|
||||
const [metaToken, setMetaToken] = useState('');
|
||||
const [meta, setMeta] = useState<ProviderResult | null>(null);
|
||||
const [google, setGoogle] = useState<ProviderResult | null>(null);
|
||||
const [msg, setMsg] = useState<string | null>(null);
|
||||
@@ -36,6 +39,7 @@ export default function AdsDiscoverPanel() {
|
||||
const res = await fetch('/api/admin/ads/status');
|
||||
const data = await res.json();
|
||||
setMetaOk(Boolean(data.meta?.configured));
|
||||
setMetaPending(Boolean(data.meta?.pendingReview));
|
||||
setGadsOk(Boolean(data.googleAds?.configured));
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -44,14 +48,33 @@ export default function AdsDiscoverPanel() {
|
||||
void loadStatus();
|
||||
}, []);
|
||||
|
||||
async function discover() {
|
||||
async function saveToken() {
|
||||
setSavingToken(true);
|
||||
setErr(null);
|
||||
const res = await fetch('/api/admin/ads/meta-token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: metaToken }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
setErr(data.error || 'No se pudo guardar el token');
|
||||
} else {
|
||||
setMsg(`Token Meta guardado${data.name ? ` · ${data.name}` : ''}`);
|
||||
setMetaToken('');
|
||||
await loadStatus();
|
||||
}
|
||||
setSavingToken(false);
|
||||
}
|
||||
|
||||
async function discover(providers?: Array<'meta_ads' | 'google_ads'>) {
|
||||
setRunning(true);
|
||||
setErr(null);
|
||||
setMsg(null);
|
||||
const res = await fetch('/api/admin/ads/discover', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ apply: true }),
|
||||
body: JSON.stringify({ apply: true, providers }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
@@ -90,7 +113,7 @@ export default function AdsDiscoverPanel() {
|
||||
assign: { provider, tenantId, accountId, accountName },
|
||||
}),
|
||||
});
|
||||
await discover();
|
||||
await discover([provider]);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
@@ -120,40 +143,82 @@ export default function AdsDiscoverPanel() {
|
||||
Meta Ads + Google Ads (agencia)
|
||||
</h3>
|
||||
<p className="text-xs text-gray-400 mt-0.5 max-w-xl">
|
||||
Lista las cuentas accesibles con marketing@ / token de agencia y
|
||||
las enlaza a cada cliente por nombre.
|
||||
Lista cuentas accesibles y las enlaza a cada cliente por nombre.
|
||||
Google Ads ya puede emparejarse; Meta cuando tengas el token de
|
||||
App Review.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void discover()}
|
||||
disabled={running || (!metaOk && !gadsOk)}
|
||||
className="inline-flex items-center gap-1.5 text-xs font-medium px-3 py-1.5 rounded-lg bg-brand-500/90 hover:bg-brand-500 text-white disabled:opacity-40"
|
||||
>
|
||||
{running ? (
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="w-3.5 h-3.5" />
|
||||
)}
|
||||
Descubrir y emparejar
|
||||
</button>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void discover(['google_ads'])}
|
||||
disabled={running || !gadsOk}
|
||||
className="inline-flex items-center gap-1.5 text-xs font-medium px-3 py-1.5 rounded-lg border border-gray-700 text-gray-200 hover:border-gray-500 disabled:opacity-40"
|
||||
>
|
||||
{running ? (
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="w-3.5 h-3.5" />
|
||||
)}
|
||||
Solo Google Ads
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void discover()}
|
||||
disabled={running || (!metaOk && !gadsOk)}
|
||||
className="inline-flex items-center gap-1.5 text-xs font-medium px-3 py-1.5 rounded-lg bg-brand-500/90 hover:bg-brand-500 text-white disabled:opacity-40"
|
||||
>
|
||||
{running ? (
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="w-3.5 h-3.5" />
|
||||
)}
|
||||
Descubrir ambos
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-3 text-[11px]">
|
||||
<span
|
||||
className={metaOk ? 'text-emerald-400' : 'text-amber-400'}
|
||||
>
|
||||
Meta {metaOk ? 'listo' : 'falta META_ACCESS_TOKEN'}
|
||||
<span className={metaOk ? 'text-emerald-400' : 'text-amber-400'}>
|
||||
Meta {metaOk ? 'listo' : 'pendiente App Review / token'}
|
||||
</span>
|
||||
<span
|
||||
className={gadsOk ? 'text-emerald-400' : 'text-amber-400'}
|
||||
>
|
||||
Google Ads{' '}
|
||||
{gadsOk ? 'listo' : 'falta GOOGLE_ADS_DEVELOPER_TOKEN + refresh'}
|
||||
<span className={gadsOk ? 'text-emerald-400' : 'text-amber-400'}>
|
||||
Google Ads {gadsOk ? 'listo' : 'falta GOOGLE_ADS_*'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{metaPending && (
|
||||
<div className="rounded-lg border border-amber-500/20 bg-amber-500/5 px-3 py-3 space-y-2">
|
||||
<p className="text-xs text-amber-200/90">
|
||||
Cuando Meta apruebe la solicitud, genera el token del usuario del
|
||||
sistema y pégalo aquí (se guarda sin redeploy).
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<input
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
className="flex-1 text-xs rounded-lg bg-gray-900 border border-gray-700 px-3 py-2 text-white placeholder:text-gray-600"
|
||||
placeholder="EAAG… system user token"
|
||||
value={metaToken}
|
||||
onChange={(e) => setMetaToken(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
disabled={savingToken || metaToken.trim().length < 20}
|
||||
onClick={() => void saveToken()}
|
||||
className="text-xs px-3 py-2 rounded-lg border border-amber-500/40 text-amber-200 hover:bg-amber-500/10 disabled:opacity-40"
|
||||
>
|
||||
{savingToken ? (
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin inline" />
|
||||
) : (
|
||||
'Guardar token Meta'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{msg && <p className="text-xs text-gray-300">{msg}</p>}
|
||||
{err && <p className="text-xs text-red-400">{err}</p>}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user