bolt-templates-sms-extensio.../client/src/components/WhitelistModal.jsx

240 lines
9.1 KiB
JavaScript

import { useEffect, useMemo, useState } from 'react';
import apiClient from '../api/client';
const BASE_PROFILE_KEYS = new Set(['providerName', 'senderId', 'dltEntityId']);
function buildProfilePatchPayload(inputs = [], values = {}) {
const provider = {};
const profileInputValues = {};
inputs.forEach((input) => {
const rawValue = String(values[input.key] ?? '').trim();
if (!rawValue) return;
if (BASE_PROFILE_KEYS.has(input.key)) {
provider[input.key] = input.key === 'senderId' ? rawValue.toUpperCase() : rawValue;
return;
}
profileInputValues[input.key] = rawValue;
});
return {
...(Object.keys(provider).length > 0 ? { provider } : {}),
...(Object.keys(profileInputValues).length > 0 ? { profileInputValues } : {}),
};
}
function getInitialValues(inputs = []) {
return inputs.reduce((accumulator, input) => {
accumulator[input.key] = input.value || '';
return accumulator;
}, {});
}
export default function WhitelistModal({ businessId, template, boundProfile, onClose, onSuccess }) {
const [profile, setProfile] = useState(boundProfile);
const [profileForm, setProfileForm] = useState({});
const [templateId, setTemplateId] = useState('');
const [toNumber, setToNumber] = useState('');
const [savingProfile, setSavingProfile] = useState(false);
const [publishing, setPublishing] = useState(false);
const [error, setError] = useState('');
const [step, setStep] = useState('profile');
const missingInputs = useMemo(
() => profile?.executionReadiness?.missingProfileInputs || [],
[profile],
);
useEffect(() => {
setProfile(boundProfile);
}, [boundProfile]);
useEffect(() => {
setProfileForm(getInitialValues(missingInputs));
}, [missingInputs]);
useEffect(() => {
if (!boundProfile) {
setError('The cURL profile bound to this template is missing. Re-select the template from Events before publishing.');
setStep('profile');
return;
}
setError('');
setStep(missingInputs.length > 0 ? 'profile' : 'publish');
}, [boundProfile, missingInputs]);
async function handleProfileSubmit(event) {
event.preventDefault();
if (!profile?.id || missingInputs.length === 0) return;
setSavingProfile(true);
setError('');
try {
const payload = buildProfilePatchPayload(missingInputs, profileForm);
const res = await apiClient.patch(
`/api/businesses/${businessId}/global-sms/profiles/${profile.id}`,
payload,
);
setProfile(res.data);
setStep(res.data?.executionReadiness?.missingProfileInputs?.length > 0 ? 'profile' : 'publish');
} catch (err) {
setError(err.response?.data?.error || 'Failed to save required profile fields');
} finally {
setSavingProfile(false);
}
}
async function handlePublish(event) {
event.preventDefault();
if (!templateId.trim() || !toNumber.trim()) return;
setPublishing(true);
setError('');
try {
await apiClient.post(`/api/businesses/${businessId}/templates/${template.eventSlug}/publish`, {
templateId: templateId.trim(),
toNumber: toNumber.trim(),
});
await Promise.resolve(onSuccess());
} catch (err) {
if (err.response?.data?.missingFields?.length) {
setError(`Missing profile fields: ${err.response.data.missingFields.join(', ')}`);
setStep('profile');
} else {
setError(err.response?.data?.error || 'Failed to publish template');
}
} finally {
setPublishing(false);
}
}
const isProfileMissing = !profile?.id;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto bg-gray-900/50 pb-10 pt-10 backdrop-blur-sm">
<div className="my-auto w-full max-w-md rounded-lg border border-border-main bg-surface-white p-5">
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full border border-gray-200 bg-white">
<span className="text-xl"></span>
</div>
<h3 className="mb-1 text-center text-lg font-bold text-text-primary">
{step === 'profile' ? 'Complete Profile Setup' : 'Publish Template'}
</h3>
<p className="mb-1 text-center text-sm text-text-muted">
{step === 'profile'
? 'Complete the required fields on the bound cURL profile before publishing.'
: 'Provide the DLT template ID and destination number to complete publish.'}
</p>
<p className="mb-2 text-center text-sm font-semibold capitalize text-text-primary">
{template.eventLabel || template.eventSlug.replace(/_/g, ' ')}
</p>
{profile && (
<p className="mb-6 text-center text-xs font-semibold uppercase tracking-wide text-text-muted">
Bound Profile: {profile.name}
</p>
)}
{error && (
<div className="mb-4 rounded-md border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-error-text">
{error}
</div>
)}
{step === 'profile' ? (
<form onSubmit={handleProfileSubmit} className="space-y-4">
{missingInputs.map((input) => (
<div key={input.key}>
<label className="mb-1.5 block text-sm font-semibold text-text-primary">{input.label}</label>
<input
type={input.secret ? 'password' : 'text'}
value={profileForm[input.key] || ''}
onChange={(event) => setProfileForm((current) => ({
...current,
[input.key]: input.key === 'senderId'
? event.target.value.toUpperCase()
: event.target.value,
}))}
className="w-full rounded-lg border border-border-main bg-page-bg px-4 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-primary-blue"
placeholder={input.label}
required={input.required !== false}
autoFocus={input.key === missingInputs[0]?.key}
/>
</div>
))}
<div className="flex gap-3 pt-4">
<button
type="button"
onClick={onClose}
disabled={savingProfile}
className="flex-1 rounded-lg border border-border-main py-2 text-sm font-medium text-text-primary transition hover:bg-page-bg disabled:opacity-50"
>
Cancel
</button>
<button
type="submit"
disabled={savingProfile || isProfileMissing || missingInputs.some((input) => !String(profileForm[input.key] || '').trim())}
className="flex-1 rounded-lg bg-primary-blue py-2 text-sm font-semibold text-white transition hover:bg-primary-dark disabled:opacity-50"
>
{savingProfile ? 'Saving…' : 'Save Details'}
</button>
</div>
</form>
) : (
<form onSubmit={handlePublish} className="space-y-4">
<div>
<label className="mb-1.5 block text-sm font-semibold text-text-primary">DLT Template ID</label>
<input
type="text"
value={templateId}
onChange={(event) => setTemplateId(event.target.value)}
placeholder="e.g. 1234567890987654321"
className="w-full rounded-lg border border-border-main bg-page-bg px-4 py-2 font-mono text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-primary-blue"
autoFocus
required
/>
</div>
<div>
<label className="mb-1.5 block text-sm font-semibold text-text-primary">Destination Phone Number</label>
<input
type="text"
value={toNumber}
onChange={(event) => setToNumber(event.target.value)}
placeholder="e.g. 919876543210"
className="w-full rounded-lg border border-border-main bg-page-bg px-4 py-2 font-mono text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-primary-blue"
required
/>
<p className="mt-1 text-xs text-text-muted">This sends the publish-triggering SMS request.</p>
</div>
<div className="flex gap-3 pt-4">
<button
type="button"
onClick={onClose}
disabled={publishing}
className="flex-1 rounded-lg border border-border-main py-2 text-sm font-medium text-text-primary transition hover:bg-page-bg disabled:opacity-50"
>
Cancel
</button>
<button
type="submit"
disabled={publishing || !templateId.trim() || !toNumber.trim()}
className="flex-1 rounded-lg bg-primary-blue py-2 text-sm font-semibold text-white transition hover:bg-primary-dark disabled:opacity-50"
>
{publishing ? 'Publishing…' : 'Publish'}
</button>
</div>
</form>
)}
</div>
</div>
);
}