template
This commit is contained in:
parent
84544e1153
commit
a109df2deb
7 changed files with 51 additions and 11 deletions
|
|
@ -227,6 +227,11 @@ def _serialize_phone(phone: WhatsAppPhoneAsset) -> dict[str, Any]:
|
|||
}
|
||||
|
||||
|
||||
def meta_calls_allowed(account: WhatsAppBusinessAccount) -> bool:
|
||||
"""A disconnected account keeps its saved records and is not sent to Meta."""
|
||||
return account.status != "disconnected"
|
||||
|
||||
|
||||
def serialize_account(account: WhatsAppBusinessAccount) -> dict[str, Any]:
|
||||
phones = [_serialize_phone(phone) for phone in account.phones]
|
||||
primary = next((phone for phone in phones if phone["status"] == "connected"), phones[0] if phones else None)
|
||||
|
|
@ -906,6 +911,8 @@ async def connect_with_token(
|
|||
|
||||
async def refresh_account(db: Session, user: UserAccount, account_id: Any) -> dict[str, Any]:
|
||||
account = _owned_account(db, user, account_id)
|
||||
if not meta_calls_allowed(account):
|
||||
return serialize_account(account)
|
||||
connection = account.connection
|
||||
token = connection.access_token if connection else None
|
||||
if connection is None or not token or connection.status == "disconnected":
|
||||
|
|
@ -933,6 +940,8 @@ async def register_phone_number(
|
|||
pin: str | None,
|
||||
) -> dict[str, Any]:
|
||||
account = _owned_account(db, user, account_id)
|
||||
if not meta_calls_allowed(account):
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Reconnect this WhatsApp account before registering the number")
|
||||
connection = account.connection
|
||||
token = connection.access_token if connection else None
|
||||
if connection is None or not token or connection.status == "disconnected":
|
||||
|
|
@ -970,6 +979,8 @@ async def register_phone_number(
|
|||
|
||||
async def disconnect_account(db: Session, user: UserAccount, account_id: Any) -> dict[str, Any]:
|
||||
account = _owned_account(db, user, account_id)
|
||||
if not meta_calls_allowed(account):
|
||||
return serialize_account(account)
|
||||
connection = account.connection
|
||||
token = connection.access_token if connection else None
|
||||
if token:
|
||||
|
|
@ -1012,7 +1023,7 @@ def account_detail(db: Session, user: UserAccount, account_id: Any) -> dict[str,
|
|||
|
||||
async def list_templates(db: Session, user: UserAccount, account_id: Any) -> dict[str, Any]:
|
||||
account = _owned_account(db, user, account_id)
|
||||
if account.templates_status != "active" or account.connection is None or not account.connection.access_token:
|
||||
if not meta_calls_allowed(account) or account.templates_status != "active" or account.connection is None or not account.connection.access_token:
|
||||
return {"available": False, "templates": []}
|
||||
data = await _graph(
|
||||
"GET",
|
||||
|
|
@ -1054,6 +1065,8 @@ async def create_marketing_template(
|
|||
) -> dict[str, Any]:
|
||||
"""Submit a marketing template to Meta for the connected WhatsApp account."""
|
||||
account = _owned_account(db, user, account_id)
|
||||
if not meta_calls_allowed(account):
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Reconnect this WhatsApp account before creating a template.")
|
||||
connection = account.connection
|
||||
token = connection.access_token if connection else None
|
||||
if connection is None or not token or connection.status == "disconnected":
|
||||
|
|
@ -1187,7 +1200,7 @@ def _messaging_phone(account: WhatsAppBusinessAccount) -> tuple[WhatsAppPhoneAss
|
|||
connection = account.connection
|
||||
token = connection.access_token if connection else None
|
||||
phone = next((item for item in account.phones if item.status == "connected"), None)
|
||||
if phone is None or account.messaging_status != "active" or not token:
|
||||
if not meta_calls_allowed(account) or phone is None or account.messaging_status != "active" or not token:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=account.last_error or "Messaging is unavailable until this number is connected on Cloud API",
|
||||
|
|
@ -1198,6 +1211,8 @@ def _messaging_phone(account: WhatsAppBusinessAccount) -> tuple[WhatsAppPhoneAss
|
|||
async def enable_customer_delivery(db: Session, user: UserAccount, account_id: Any, app_secret: str) -> dict[str, Any]:
|
||||
"""Point this account's replies and delivery updates at the workspace."""
|
||||
account = _owned_account(db, user, account_id)
|
||||
if not meta_calls_allowed(account):
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Reconnect this WhatsApp account before replies can arrive here.")
|
||||
connection = account.connection
|
||||
token = connection.access_token if connection else None
|
||||
if connection is None or not token or connection.status == "disconnected":
|
||||
|
|
@ -1383,6 +1398,8 @@ async def send_account_broadcast(
|
|||
|
||||
async def download_account_media(db: Session, user: UserAccount, account_id: Any, media_id: str) -> tuple[bytes, str]:
|
||||
account = _owned_account(db, user, account_id)
|
||||
if not meta_calls_allowed(account):
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Reconnect this WhatsApp account before opening media")
|
||||
token = account.connection.access_token if account.connection else None
|
||||
if not token:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Reconnect this WhatsApp account before opening media")
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from .whatsapp_onboarding import (
|
|||
_load_phones,
|
||||
_load_waba,
|
||||
_owned_account,
|
||||
meta_calls_allowed,
|
||||
_quality_label,
|
||||
_token_app_id,
|
||||
_waba_ids_from_debug,
|
||||
|
|
@ -613,6 +614,8 @@ async def _correct_waba_id(db: Session, account: WhatsAppBusinessAccount, token:
|
|||
|
||||
|
||||
async def sync_account_templates(db: Session, account: WhatsAppBusinessAccount) -> str | None:
|
||||
if not meta_calls_allowed(account):
|
||||
return None
|
||||
tokens = _candidate_tokens(db, account)
|
||||
if not tokens:
|
||||
if account.status == "disconnected":
|
||||
|
|
@ -671,6 +674,7 @@ async def list_managed_templates(db: Session, user: UserAccount, account_id: str
|
|||
accounts = db.scalars(query).all()
|
||||
if account_id:
|
||||
accounts = [account for account in accounts if str(account.id) == account_id]
|
||||
accounts = [account for account in accounts if meta_calls_allowed(account)]
|
||||
warnings = []
|
||||
for account in accounts:
|
||||
warning = await sync_account_templates(db, account)
|
||||
|
|
@ -746,8 +750,14 @@ def _validate_identity(payload: dict[str, Any]) -> tuple[str, str, str]:
|
|||
return name, language, category
|
||||
|
||||
|
||||
def _require_connected(account: WhatsAppBusinessAccount) -> None:
|
||||
if not meta_calls_allowed(account):
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Reconnect this WhatsApp account before managing templates.")
|
||||
|
||||
|
||||
async def create_managed_template(db: Session, user: UserAccount, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
account = _owned_account(db, user, payload.get("account_id"))
|
||||
_require_connected(account)
|
||||
token = _token(db, account)
|
||||
name, language, category = _validate_identity(payload)
|
||||
existing = db.scalar(
|
||||
|
|
@ -793,6 +803,7 @@ async def create_managed_template(db: Session, user: UserAccount, payload: dict[
|
|||
|
||||
async def resubmit_managed_template(db: Session, user: UserAccount, template_id: Any, payload: dict[str, Any]) -> dict[str, Any]:
|
||||
row, account = _owned_template(db, user, template_id)
|
||||
_require_connected(account)
|
||||
if row.status not in {"REJECTED", "PAUSED", "APPROVED"}:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="This template can be edited after Meta rejects it, pauses it, or approves it.")
|
||||
if not row.meta_template_id:
|
||||
|
|
@ -812,6 +823,7 @@ async def resubmit_managed_template(db: Session, user: UserAccount, template_id:
|
|||
|
||||
async def delete_managed_template(db: Session, user: UserAccount, template_id: Any) -> dict[str, str]:
|
||||
row, account = _owned_template(db, user, template_id)
|
||||
_require_connected(account)
|
||||
params: dict[str, Any] = {"name": row.name}
|
||||
if row.meta_template_id:
|
||||
params["hsm_id"] = row.meta_template_id
|
||||
|
|
@ -823,6 +835,7 @@ async def delete_managed_template(db: Session, user: UserAccount, template_id: A
|
|||
|
||||
async def upload_header_media(db: Session, user: UserAccount, account_id: Any, filename: str, content_type: str, data: bytes) -> dict[str, str]:
|
||||
account = _owned_account(db, user, account_id)
|
||||
_require_connected(account)
|
||||
token = _token(db, account)
|
||||
kind = _MEDIA.get(content_type)
|
||||
if kind is None:
|
||||
|
|
|
|||
|
|
@ -137,7 +137,9 @@ export function CustomerWhatsAppInbox({ account, onBack }: { account: WhatsAppAc
|
|||
useEffect(() => {
|
||||
Promise.all([
|
||||
whatsappApi.messages(account.id),
|
||||
whatsappApi.templates(account.id).catch(() => ({ available: false, templates: [] as WhatsAppTemplateRow[] })),
|
||||
account.status === 'disconnected'
|
||||
? Promise.resolve({ available: false, templates: [] as WhatsAppTemplateRow[] })
|
||||
: whatsappApi.templates(account.id).catch(() => ({ available: false, templates: [] as WhatsAppTemplateRow[] })),
|
||||
])
|
||||
.then(([items, templatePayload]) => {
|
||||
setMessages(items)
|
||||
|
|
|
|||
|
|
@ -193,6 +193,11 @@ export function WhatsAppSetupPage() {
|
|||
useEffect(() => {
|
||||
if (phase !== 'detail' || !selected) return
|
||||
if (tab === 'templates') {
|
||||
if (selected.status === 'disconnected') {
|
||||
setTemplates([])
|
||||
setTemplatesAvailable(false)
|
||||
return
|
||||
}
|
||||
setTemplates(null)
|
||||
whatsappApi.templates(selected.id)
|
||||
.then((payload) => {
|
||||
|
|
@ -213,7 +218,7 @@ export function WhatsAppSetupPage() {
|
|||
.then(setLogs)
|
||||
.catch((reason) => setError(reason instanceof Error ? reason.message : 'Unable to load logs'))
|
||||
}
|
||||
}, [phase, tab, selected?.id])
|
||||
}, [phase, tab, selected?.id, selected?.status])
|
||||
|
||||
function replaceAccount(next: WhatsAppAccount) {
|
||||
setAccounts((current) => current.map((account) => (account.id === next.id ? next : account)))
|
||||
|
|
@ -563,9 +568,11 @@ export function WhatsAppSetupPage() {
|
|||
<MessageCircle size={15} /> Chat
|
||||
</button>
|
||||
)}
|
||||
<button className="secondary-button" onClick={() => void refreshSelected()} disabled={busy}>
|
||||
<RefreshCw size={14} /> Refresh
|
||||
</button>
|
||||
{selected.status !== 'disconnected' && (
|
||||
<button className="secondary-button" onClick={() => void refreshSelected()} disabled={busy}>
|
||||
<RefreshCw size={14} /> Refresh
|
||||
</button>
|
||||
)}
|
||||
<button className="secondary-button" onClick={() => void startEmbeddedSignup()} disabled={busy}>Reconnect</button>
|
||||
<button className="secondary-button wa-biz-danger" onClick={() => setDisconnecting(selected)}>Disconnect</button>
|
||||
</div>
|
||||
|
|
@ -620,7 +627,7 @@ export function WhatsAppSetupPage() {
|
|||
</div>
|
||||
<StatusDot status={phone.status} />
|
||||
<span>Quality: {phone.quality || '—'}</span>
|
||||
{phone.needs_registration && !phone.needs_otp && (
|
||||
{selected.status !== 'disconnected' && phone.needs_registration && !phone.needs_otp && (
|
||||
<div className="wa-biz-register">
|
||||
<input
|
||||
inputMode="numeric"
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ export function WhatsAppTemplateWizard() {
|
|||
if (!editing && !duplicateId) window.localStorage.setItem(DRAFT_KEY, JSON.stringify(draft))
|
||||
}, [draft, editing, duplicateId])
|
||||
|
||||
const numbers = useMemo(() => accounts.flatMap((account) => (
|
||||
const numbers = useMemo(() => accounts.filter((account) => account.status !== 'disconnected').flatMap((account) => (
|
||||
account.phones.length
|
||||
? account.phones.map((phone) => ({ account, phone: phone as WhatsAppAccount['phones'][number] | null }))
|
||||
: [{ account, phone: null as WhatsAppAccount['phones'][number] | null }]
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ export function WhatsAppTemplatesPage() {
|
|||
<input value={search} onChange={(event) => setSearch(event.target.value)} placeholder="Search by name or ID" aria-label="Search templates" />
|
||||
<select value={accountId} onChange={(event) => { setAccountId(event.target.value); load(event.target.value) }} aria-label="WhatsApp account">
|
||||
<option value="">All accounts</option>
|
||||
{accounts.map((account) => {
|
||||
{accounts.filter((account) => account.status !== 'disconnected').map((account) => {
|
||||
const numbers = account.phones.map((phone) => phone.display_phone).filter(Boolean).join(', ')
|
||||
return <option key={account.id} value={account.id}>{numbers ? `${account.name} · ${numbers}` : account.name}</option>
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -2035,6 +2035,7 @@ main { padding: 30px clamp(20px, 3vw, 44px) 50px; }
|
|||
.wa-tpl-preview > p { margin: 0 0 8px; font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: #8a95a7; }
|
||||
.wa-tpl-phone { background: #efe7dc; border-radius: 16px; padding: 16px; }
|
||||
.wa-tpl-bubble { display: grid; gap: 6px; background: white; border-radius: 12px; padding: 10px 12px; color: #14233b; }
|
||||
.wa-tpl-bubble span, .wa-tpl-bubble strong, .wa-tpl-bubble small { white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.wa-tpl-bubble img, .wa-tpl-media { width: 100%; border-radius: 8px; background: #d9e2ec; min-height: 84px; object-fit: cover; }
|
||||
.wa-tpl-media { display: grid; place-items: center; color: #526176; font-size: 13px; }
|
||||
.wa-tpl-bubble small { color: #718096; }
|
||||
|
|
@ -2052,7 +2053,7 @@ main { padding: 30px clamp(20px, 3vw, 44px) 50px; }
|
|||
.wa-tpl-review { display: grid; gap: 8px; margin: 0; }
|
||||
.wa-tpl-review div { display: grid; grid-template-columns: 160px 1fr; gap: 8px; }
|
||||
.wa-tpl-review dt { color: #718096; }
|
||||
.wa-tpl-review dd { margin: 0; }
|
||||
.wa-tpl-review dd { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.wa-tpl-checks { margin: 12px 0 0; padding-left: 18px; }
|
||||
.wa-tpl-reason { margin-top: 16px; padding: 12px; border-radius: 10px; background: #fff4f2; color: #8d2b26; }
|
||||
.wa-tpl-tabs { display: flex; gap: 8px; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue