template
This commit is contained in:
parent
a109df2deb
commit
8901d99dd2
11 changed files with 380 additions and 13 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import { Loader2 } from 'lucide-react'
|
||||
import { useEffect, useState, type FormEvent } from 'react'
|
||||
import { Link, useSearchParams } from 'react-router-dom'
|
||||
import { whatsappApi, type WhatsAppAccount, type WhatsAppTemplateRow } from '../../lib/api'
|
||||
import { waTemplateApi, whatsappApi, type WaManagedTemplate, type WhatsAppAccount, type WhatsAppTemplateRow } from '../../lib/api'
|
||||
|
||||
const TEAM_TEMPLATE_NAME = 'urmi_team_aircheck'
|
||||
const TEAM_TEMPLATE_BODY = "Team, the broadcast desk is on air. This is the first note Urmi is sending through Meta's marketing line, and it is written for the people building it. Read it on your phone, then bring it to standup so we can close the test."
|
||||
|
|
@ -23,7 +23,9 @@ export function WhatsAppBroadcastPage() {
|
|||
const [accounts, setAccounts] = useState<WhatsAppAccount[]>([])
|
||||
const [accountId, setAccountId] = useState('')
|
||||
const [templates, setTemplates] = useState<WhatsAppTemplateRow[]>([])
|
||||
const [managed, setManaged] = useState<WaManagedTemplate[]>([])
|
||||
const [templatesReady, setTemplatesReady] = useState(false)
|
||||
const [useSample, setUseSample] = useState(true)
|
||||
const [templateName, setTemplateName] = useState(TEAM_TEMPLATE_NAME)
|
||||
const [templateLanguage, setTemplateLanguage] = useState('en')
|
||||
const [draftName, setDraftName] = useState(TEAM_TEMPLATE_NAME)
|
||||
|
|
@ -72,6 +74,9 @@ export function WhatsAppBroadcastPage() {
|
|||
setTemplates([])
|
||||
setTemplatesReady(false)
|
||||
})
|
||||
waTemplateApi.list(accountId)
|
||||
.then((payload) => setManaged(payload.templates))
|
||||
.catch(() => setManaged([]))
|
||||
}, [accountId, templateTick])
|
||||
|
||||
async function createTemplate(event: FormEvent) {
|
||||
|
|
@ -103,6 +108,16 @@ export function WhatsAppBroadcastPage() {
|
|||
setResult(null)
|
||||
const numbers = recipients.split(/[\n,]/).map((item) => item.trim()).filter(Boolean)
|
||||
const parameters = bodyValues.split('\n').map((item) => item.trim()).filter(Boolean)
|
||||
if (mediaHeader && !savedSample) {
|
||||
setError(`Add the sample ${headerLabel} on the template page. Meta rejects this send without it.`)
|
||||
setBusy(false)
|
||||
return
|
||||
}
|
||||
if (mediaHeader && !useSample) {
|
||||
setError(`Turn on the saved sample ${headerLabel}. This template’s header is a ${headerLabel}, and Meta rejects the send without it.`)
|
||||
setBusy(false)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const payload = await whatsappApi.sendBroadcast(accountId, {
|
||||
template_name: templateName.trim(),
|
||||
|
|
@ -110,6 +125,7 @@ export function WhatsAppBroadcastPage() {
|
|||
recipients: numbers,
|
||||
body_parameters: parameters,
|
||||
product_policy: strict ? 'STRICT' : 'CLOUD_API_FALLBACK',
|
||||
use_sample_header: Boolean(mediaHeader && useSample && savedSample),
|
||||
})
|
||||
setResult(payload)
|
||||
} catch (reason) {
|
||||
|
|
@ -121,6 +137,11 @@ export function WhatsAppBroadcastPage() {
|
|||
|
||||
const approved = templates.filter((item) => (item.status || '').toUpperCase() === 'APPROVED' && item.name)
|
||||
const pending = templates.filter((item) => (item.status || '').toUpperCase() !== 'APPROVED' && item.name)
|
||||
const selectedTemplate = managed.find((item) => item.name === templateName && item.language === templateLanguage)
|
||||
|| managed.find((item) => item.name === templateName)
|
||||
const mediaHeader = ['IMAGE', 'VIDEO', 'DOCUMENT'].includes(selectedTemplate?.header_format || '')
|
||||
const savedSample = Boolean(selectedTemplate?.sample_stored)
|
||||
const headerLabel = selectedTemplate?.header_format === 'VIDEO' ? 'video' : selectedTemplate?.header_format === 'DOCUMENT' ? 'document' : 'image'
|
||||
|
||||
return (
|
||||
<div className="wa-biz wa-token-page">
|
||||
|
|
@ -198,6 +219,7 @@ export function WhatsAppBroadcastPage() {
|
|||
const next = approved.find((item) => item.name === event.target.value)
|
||||
setTemplateName(event.target.value)
|
||||
if (next?.language) setTemplateLanguage(next.language)
|
||||
setUseSample(true)
|
||||
}}
|
||||
>
|
||||
{approved.map((item) => (
|
||||
|
|
@ -220,6 +242,23 @@ export function WhatsAppBroadcastPage() {
|
|||
<span>One WhatsApp number per line, up to 20. Include the country code.</span>
|
||||
<textarea value={recipients} onChange={(event) => setRecipients(event.target.value)} rows={4} required placeholder="9198XXXXXXXX" />
|
||||
</label>
|
||||
{mediaHeader && (
|
||||
<label className="explorer-input">
|
||||
<strong>Header {headerLabel}</strong>
|
||||
<span>
|
||||
{savedSample
|
||||
? `Use the ${headerLabel} saved on this template (${selectedTemplate?.sample_name || 'sample file'}). Meta does not keep the approval file, so each send has to attach it.`
|
||||
: `This template needs a ${headerLabel} on every send, and none is saved yet.`}
|
||||
</span>
|
||||
{savedSample ? (
|
||||
<input type="checkbox" checked={useSample} onChange={(event) => setUseSample(event.target.checked)} />
|
||||
) : selectedTemplate ? (
|
||||
<Link to={`/console/whatsapp/templates/${selectedTemplate.id}`}>Add the sample on the template</Link>
|
||||
) : (
|
||||
<span>Open the template and add the sample file first.</span>
|
||||
)}
|
||||
</label>
|
||||
)}
|
||||
<label className="explorer-input">
|
||||
<strong>Body values</strong>
|
||||
<span>Leave this empty for the team aircheck. If a template has {'{{1}}'}, {'{{2}}'}, put one value per line in that order.</span>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ export function WhatsAppTemplateDetailPage() {
|
|||
const [row, setRow] = useState<WaManagedTemplate | null>(null)
|
||||
const [error, setError] = useState('')
|
||||
const [tab, setTab] = useState<'details' | 'analytics'>('details')
|
||||
const [sampleUrl, setSampleUrl] = useState('')
|
||||
const [savingSample, setSavingSample] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
waTemplateApi.get(templateId)
|
||||
|
|
@ -26,6 +28,41 @@ export function WhatsAppTemplateDetailPage() {
|
|||
.catch((reason) => setError(reason instanceof Error ? reason.message : 'Template not found'))
|
||||
}, [templateId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!row?.sample_stored || row.header_format !== 'IMAGE') {
|
||||
setSampleUrl('')
|
||||
return
|
||||
}
|
||||
let cancelled = false
|
||||
let url = ''
|
||||
waTemplateApi.sampleBlob(row.id)
|
||||
.then((blob) => {
|
||||
if (cancelled) return
|
||||
url = URL.createObjectURL(blob)
|
||||
setSampleUrl(url)
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setSampleUrl('')
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
if (url) URL.revokeObjectURL(url)
|
||||
}
|
||||
}, [row?.id, row?.sample_stored, row?.header_format])
|
||||
|
||||
async function saveSample(file: File) {
|
||||
if (!row) return
|
||||
setSavingSample(true)
|
||||
setError('')
|
||||
try {
|
||||
setRow(await waTemplateApi.saveSample(row.id, file))
|
||||
} catch (reason) {
|
||||
setError(reason instanceof Error ? reason.message : 'The sample file was not saved')
|
||||
} finally {
|
||||
setSavingSample(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function remove() {
|
||||
if (!row || !window.confirm(`Delete ${row.name}?`)) return
|
||||
try {
|
||||
|
|
@ -45,7 +82,12 @@ export function WhatsAppTemplateDetailPage() {
|
|||
category: ((row.definition.category || row.category) as WaTemplateDraft['category']) || '',
|
||||
name: row.name,
|
||||
language: row.language,
|
||||
header: { ...emptyDraft().header, ...row.definition.header },
|
||||
header: {
|
||||
...emptyDraft().header,
|
||||
...row.definition.header,
|
||||
preview_name: row.sample_name || row.definition.header?.preview_name || '',
|
||||
preview_url: sampleUrl || row.definition.header?.preview_url || '',
|
||||
},
|
||||
auth: { ...emptyDraft().auth, ...row.definition.auth },
|
||||
buttons: row.definition.buttons || [],
|
||||
body_examples: row.definition.body_examples || [],
|
||||
|
|
@ -85,6 +127,29 @@ export function WhatsAppTemplateDetailPage() {
|
|||
<div><dt>Quality</dt><dd>{row.quality || 'Not rated yet'}</dd></div>
|
||||
<div><dt>Updated</dt><dd>{row.updated_at ? new Date(row.updated_at).toLocaleString() : '—'}</dd></div>
|
||||
</dl>
|
||||
{['IMAGE', 'VIDEO', 'DOCUMENT'].includes(row.header_format) && (
|
||||
<div className="wa-tpl-sample">
|
||||
<strong>Sample file for sending</strong>
|
||||
<p>
|
||||
{row.sample_stored
|
||||
? `${row.sample_name || 'A sample file'} is saved. A broadcast can attach this file. Meta still needs it on every send, because the approval upload is not reused.`
|
||||
: 'No sample file is saved. Meta approved the layout, but each send still needs this file. Add it here, then turn it on when you broadcast.'}
|
||||
</p>
|
||||
<label className="wa-tpl-drop">
|
||||
<strong>{savingSample ? 'Saving…' : row.sample_stored ? 'Replace the sample file' : 'Add the sample file'}</strong>
|
||||
<input
|
||||
type="file"
|
||||
disabled={savingSample}
|
||||
accept={row.header_format === 'IMAGE' ? 'image/jpeg,image/png' : row.header_format === 'VIDEO' ? 'video/mp4' : 'application/pdf'}
|
||||
onChange={(event) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (file) void saveSample(file)
|
||||
event.target.value = ''
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
{row.rejected_reason && (
|
||||
<div className="wa-tpl-reason">
|
||||
<strong>Why Meta paused or rejected it</strong>
|
||||
|
|
|
|||
|
|
@ -127,6 +127,9 @@ export function WhatsAppTemplateWizard() {
|
|||
...draft.header,
|
||||
type: uploaded.format as WaTemplateDraft['header']['type'],
|
||||
handle: uploaded.handle,
|
||||
sample_id: uploaded.sample_id,
|
||||
sample_name: uploaded.sample_name,
|
||||
sample_type: uploaded.sample_type,
|
||||
preview_name: file.name,
|
||||
preview_url: file.type.startsWith('image/') ? URL.createObjectURL(file) : '',
|
||||
},
|
||||
|
|
@ -252,8 +255,10 @@ export function WhatsAppTemplateWizard() {
|
|||
onClick={() => patch({ category: item.id, subtype: item.subtypes[0][0], buttons: item.id === 'AUTHENTICATION' ? [] : draft.buttons })}
|
||||
>
|
||||
<Icon size={18} />
|
||||
<strong>{item.title}</strong>
|
||||
<span>{item.description}</span>
|
||||
<span>
|
||||
<strong>{item.title}</strong>
|
||||
<small>{item.description}</small>
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
|
|
@ -308,6 +313,7 @@ export function WhatsAppTemplateWizard() {
|
|||
{['IMAGE', 'VIDEO', 'DOCUMENT'].includes(draft.header.type) && (
|
||||
<label className="wa-tpl-drop">
|
||||
<strong>{draft.header.preview_name || 'Drop a JPEG, PNG, MP4, or PDF'}</strong>
|
||||
<small>Meta uses this file only to review the template. Urmi keeps a copy so a broadcast can attach it when the message is sent.</small>
|
||||
<input type="file" accept={draft.header.type === 'IMAGE' ? 'image/jpeg,image/png' : draft.header.type === 'VIDEO' ? 'video/mp4' : 'application/pdf'} onChange={(event) => { const file = event.target.files?.[0]; if (file) void uploadHeader(file) }} />
|
||||
</label>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export function emptyDraft(): WaTemplateDraft {
|
|||
category: '',
|
||||
subtype: 'default',
|
||||
parameter_format: 'POSITIONAL',
|
||||
header: { type: 'NONE', text: '', examples: [], handle: '', preview_name: '', preview_url: '' },
|
||||
header: { type: 'NONE', text: '', examples: [], handle: '', sample_id: '', sample_name: '', sample_type: '', preview_name: '', preview_url: '' },
|
||||
body: '',
|
||||
body_examples: [],
|
||||
footer: '',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue