- launch.ts policy: LAUNCH_MODE=free_early_access default; effective access = published && (free || early); intended tier (access) never rewritten - Billing off server-side: /api/subscribe 503 honest copy; subscribe CTAs render disabled 'قريباً' from first paint; /subscription early-access panel + /api/notify (LaunchNotifications) with honest 503 when mail transport off - Signup trust: verification email at signup (Users afterChange), no confirm-password, 429 message fidelity, typo suggestion, live password hint - Agent pipeline: pickFields strips access/readingMinutes, sourceId dedup, secret-scan 422 (pattern names only), string-content rejected (was silently replaced), publish stays explicit opt-in - lexical.ts: root unwrap fix — lexicalToText/countLexicalWords returned 0 for real serialized trees (secret scan + readingMinutes were inert) - llms-full.txt: fetchIndexDocs(true) — bodies were never selected - Content honesty: contentType/authorName/sourceId fields + migration, readingMinutes computed at write (160 wpm) + backfill script, byline, 'حُدّث' freshness - SEO/AI-SEO: metadata rewrite (published-only, og:image 1200x630), Article + BreadcrumbList + FAQPage + Organization JSON-LD, isAccessibleForFree honest, table renderer, robots AI-crawler policy (10 named bots), llms.txt + llms-full.txt, /topics/* hubs + sitemap, noindex auth pages, www 301 - Ops: backup-db.sh (restore rehearsed on VPS), docs copies (launch readiness + marketing audit + owner-inputs for L-03), GLOSSARY.md entries Verified: tsc --noEmit 0, npm run build 0, local smoke (ghost VIP read, subscribe/notify/agent APIs, signup token, 429 on 6th, llms/topics/sitemap, og:image/JSON-LD)
90 lines
4.0 KiB
TypeScript
90 lines
4.0 KiB
TypeScript
'use client'
|
|
|
|
import Link from 'next/link'
|
|
import { useEffect, useState } from 'react'
|
|
|
|
/* Resend button + status — real /api/auth/resend-verification endpoint.
|
|
Without SMTP configured the API answers 503 with a clear message.
|
|
«البريد غير صحيح؟» — collapsed change-email affordance: PATCH /api/users/:id
|
|
(self-update allowed in Users.ts access) then resend; 400 → duplicate-email
|
|
copy. Hidden without a session (no id → nothing to patch). */
|
|
export default function VerifyActions({ verified, email, userId }: { verified: boolean; email: string | null; userId: number | null }) {
|
|
const [status, setStatus] = useState('')
|
|
const [newEmail, setNewEmail] = useState('')
|
|
const [busy, setBusy] = useState(false)
|
|
|
|
useEffect(() => {
|
|
if (email) {
|
|
setNewEmail(email)
|
|
return
|
|
}
|
|
try {
|
|
const saved = sessionStorage.getItem('anwarpi:signup-email')
|
|
if (saved) setNewEmail(saved)
|
|
} catch { /* private mode */ }
|
|
}, [email])
|
|
|
|
async function changeEmail() {
|
|
if (busy || !userId) return
|
|
setBusy(true)
|
|
setStatus('جارٍ التحديث…')
|
|
try {
|
|
const patchRes = await fetch(`/api/users/${userId}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email: newEmail.trim() }),
|
|
})
|
|
if (!patchRes.ok) {
|
|
setStatus(patchRes.status === 400 ? 'هذا البريد مسجَّل لدينا.' : 'تعذّر تحديث البريد، حاول مرة أخرى.')
|
|
return
|
|
}
|
|
const resendRes = await fetch('/api/auth/resend-verification', { method: 'POST' })
|
|
const data = await resendRes.json().catch(() => null) as { error?: string; already?: boolean } | null
|
|
setStatus(resendRes.ok || data?.already ? 'أرسلنا رسالة تأكيد جديدة إلى بريدك المحدَّث.' : data?.error || 'تم تحديث البريد لكن تعذّر الإرسال الآن.')
|
|
} catch {
|
|
setStatus('تعذّر الاتصال، حاول مرة أخرى.')
|
|
} finally {
|
|
setBusy(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="actions">
|
|
<Link className="btn btn-primary" href="/onboarding" data-od-id="continue-onboarding-cta">متابعة إعداد الحساب</Link>
|
|
{!verified && (
|
|
<button className="btn btn-secondary" id="resendButton" type="button" data-od-id="resend-email-button"
|
|
onClick={async (e) => {
|
|
const button = e.currentTarget
|
|
button.disabled = true
|
|
setStatus('جارٍ الإرسال…')
|
|
const res = await fetch('/api/auth/resend-verification', { method: 'POST' })
|
|
const data = await res.json().catch(() => null) as { error?: string; already?: boolean } | null
|
|
if (res.ok || data?.already) {
|
|
setStatus('أرسلنا الرسالة من جديد — افحص صندوق الوارد.')
|
|
} else {
|
|
setStatus(data?.error || 'تعذّر الإرسال، جرّب بعد قليل.')
|
|
}
|
|
setTimeout(() => { button.disabled = false }, 3000)
|
|
}}
|
|
>
|
|
إعادة إرسال الرسالة
|
|
</button>
|
|
)}
|
|
</div>
|
|
<p className="status" id="resendStatus" role="status" aria-live="polite">{status}</p>
|
|
{userId && !verified && (
|
|
<details className="change-email" data-od-id="change-email">
|
|
<summary>البريد غير صحيح؟</summary>
|
|
<div className="change-email-row">
|
|
<input className="input" type="email" inputMode="email" dir="ltr" value={newEmail} onChange={e => setNewEmail(e.target.value)} aria-label="البريد الإلكتروني الصحيح" />
|
|
<button className="btn btn-secondary" type="button" onClick={changeEmail} disabled={busy || newEmail.trim() === ''}>
|
|
{busy ? 'جارٍ التحديث…' : 'حدّث البريد وأعد الإرسال'}
|
|
</button>
|
|
</div>
|
|
</details>
|
|
)}
|
|
</>
|
|
)
|
|
}
|