647 lines
28 KiB
JavaScript
647 lines
28 KiB
JavaScript
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
import { useNavigate, useParams } from 'react-router-dom';
|
|
import apiClient from '../api/client';
|
|
import { useBusiness } from '../context/BusinessContext';
|
|
|
|
const BASE_PROFILE_KEYS = new Set(['providerName', 'senderId', 'dltEntityId']);
|
|
const PENDING_SENDER_ID_PROFILE_NAME = 'Pending Sender ID';
|
|
|
|
function formatUpdatedAt(value) {
|
|
if (!value) return 'Not updated yet';
|
|
|
|
try {
|
|
return new Date(value).toLocaleString();
|
|
} catch {
|
|
return 'Not updated yet';
|
|
}
|
|
}
|
|
|
|
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 getInputInitialValues(inputs = []) {
|
|
return inputs.reduce((accumulator, input) => {
|
|
accumulator[input.key] = input.value || '';
|
|
return accumulator;
|
|
}, {});
|
|
}
|
|
|
|
function getProfileSummary(profile) {
|
|
const parts = [];
|
|
const provider = profile?.provider || {};
|
|
const missingCount = profile?.executionReadiness?.missingProfileInputs?.length || 0;
|
|
|
|
if (provider.providerName) parts.push(provider.providerName);
|
|
if (provider.senderId) parts.push(`Sender ${provider.senderId}`);
|
|
if (provider.dltEntityId) parts.push('DLT ready');
|
|
if (missingCount > 0) parts.push(`${missingCount} required field${missingCount === 1 ? '' : 's'} pending`);
|
|
|
|
return parts.join(' • ') || 'Profile saved. Complete the remaining setup fields to continue.';
|
|
}
|
|
|
|
function isPendingSenderIdProfile(profile) {
|
|
const normalizedName = String(profile?.name || '').trim();
|
|
const senderId = String(profile?.provider?.senderId || '').trim();
|
|
return (profile?.isAutoNamed === true && !senderId) || normalizedName === PENDING_SENDER_ID_PROFILE_NAME;
|
|
}
|
|
|
|
function DeleteProfileModal({ preview, deleting, onCancel, onConfirm }) {
|
|
if (!preview) return null;
|
|
|
|
const impactedTemplates = Array.isArray(preview.impactedTemplates) ? preview.impactedTemplates : [];
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-900/55 p-4 backdrop-blur-sm">
|
|
<div className="w-full max-w-xl rounded-2xl border border-gray-200 bg-white shadow-xl">
|
|
<div className="border-b border-gray-200 px-6 py-5">
|
|
<h3 className="text-lg font-bold text-gray-900">Delete cURL Profile</h3>
|
|
<p className="mt-1 text-sm text-gray-500">
|
|
{preview.profile?.name || 'This profile'} will be deleted. Bound templates will be removed, but event definitions will stay.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-4 px-6 py-5">
|
|
{impactedTemplates.length > 0 ? (
|
|
<>
|
|
<p className="text-sm font-semibold text-gray-900">Affected templates</p>
|
|
<div className="max-h-72 space-y-3 overflow-y-auto">
|
|
{impactedTemplates.map((template) => (
|
|
<div key={`${template.eventSlug}-${template.status}`} className="rounded-xl border border-gray-200 bg-gray-50 px-4 py-3">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<p className="font-medium text-gray-900">{template.eventLabel || template.eventSlug}</p>
|
|
<span className="rounded-full border border-gray-200 bg-white px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.14em] text-gray-500">
|
|
{template.status || 'generated'}
|
|
</span>
|
|
</div>
|
|
{template.templateId && (
|
|
<p className="mt-2 font-mono text-xs text-gray-500">DLT Template ID: {template.templateId}</p>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</>
|
|
) : (
|
|
<div className="rounded-xl border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-600">
|
|
No templates are currently bound to this profile.
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex gap-3 border-t border-gray-200 px-6 py-4">
|
|
<button
|
|
type="button"
|
|
onClick={onCancel}
|
|
disabled={deleting}
|
|
className="flex-1 rounded-lg border border-gray-300 px-4 py-2 text-sm font-semibold text-gray-700 transition hover:bg-gray-50 disabled:opacity-50"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={onConfirm}
|
|
disabled={deleting}
|
|
className="flex-1 rounded-lg bg-red-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-red-700 disabled:opacity-50"
|
|
>
|
|
{deleting ? 'Deleting…' : 'Delete Profile'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function GlobalSms() {
|
|
const { businessId } = useParams();
|
|
const navigate = useNavigate();
|
|
const { isSetupComplete, setHasGlobalSms, setIsSetupComplete } = useBusiness();
|
|
const hasLoadedProfilesRef = useRef(false);
|
|
const [initialLoading, setInitialLoading] = useState(true);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
const [profiles, setProfiles] = useState([]);
|
|
const [activeProfileId, setActiveProfileId] = useState(null);
|
|
const [saving, setSaving] = useState(false);
|
|
const [savingInputs, setSavingInputs] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const [success, setSuccess] = useState('');
|
|
const [formCurl, setFormCurl] = useState('');
|
|
const [formSetActive, setFormSetActive] = useState(true);
|
|
const [inputForm, setInputForm] = useState({});
|
|
const [revealedProfiles, setRevealedProfiles] = useState({});
|
|
const [visibleProfileIds, setVisibleProfileIds] = useState({});
|
|
const [deletePreview, setDeletePreview] = useState(null);
|
|
const [deletingProfileId, setDeletingProfileId] = useState('');
|
|
|
|
const activeProfile = useMemo(
|
|
() => profiles.find((profile) => profile.id === activeProfileId) || null,
|
|
[profiles, activeProfileId],
|
|
);
|
|
const missingInputs = useMemo(
|
|
() => activeProfile?.executionReadiness?.missingProfileInputs || [],
|
|
[activeProfile?.executionReadiness?.missingProfileInputs],
|
|
);
|
|
const hasProfiles = profiles.length > 0;
|
|
const eventsPath = `/${businessId}/events`;
|
|
|
|
const loadProfiles = useCallback(async ({ background = false } = {}) => {
|
|
try {
|
|
if (background) {
|
|
setRefreshing(true);
|
|
} else {
|
|
setInitialLoading(true);
|
|
}
|
|
const res = await apiClient.get(`/api/businesses/${businessId}/global-sms/profiles`);
|
|
const fetchedProfiles = res.data?.profiles || [];
|
|
const nextActiveProfileId = res.data?.activeProfileId || null;
|
|
const nextActiveProfile = fetchedProfiles.find((profile) => profile.id === nextActiveProfileId) || null;
|
|
const nextIsSetupComplete = nextActiveProfile?.executionReadiness?.isSetupComplete === true;
|
|
|
|
setProfiles(fetchedProfiles);
|
|
setActiveProfileId(nextActiveProfileId);
|
|
setHasGlobalSms(fetchedProfiles.length > 0);
|
|
setIsSetupComplete(nextIsSetupComplete);
|
|
setError('');
|
|
|
|
return {
|
|
activeProfile: nextActiveProfile,
|
|
hasProfile: !!nextActiveProfile,
|
|
complete: nextIsSetupComplete,
|
|
};
|
|
} catch {
|
|
setError('Failed to load cURL profiles');
|
|
setHasGlobalSms(false);
|
|
setIsSetupComplete(false);
|
|
return { activeProfile: null, hasProfile: false, complete: false };
|
|
} finally {
|
|
if (background) {
|
|
setRefreshing(false);
|
|
} else {
|
|
hasLoadedProfilesRef.current = true;
|
|
setInitialLoading(false);
|
|
}
|
|
}
|
|
}, [businessId, setHasGlobalSms, setIsSetupComplete]);
|
|
|
|
useEffect(() => {
|
|
loadProfiles({ background: hasLoadedProfilesRef.current });
|
|
}, [loadProfiles]);
|
|
|
|
useEffect(() => {
|
|
setInputForm(getInputInitialValues(missingInputs));
|
|
}, [activeProfileId, missingInputs]);
|
|
|
|
const ensureRevealData = useCallback(async (profileId) => {
|
|
if (revealedProfiles[profileId]) return revealedProfiles[profileId];
|
|
|
|
const res = await apiClient.get(`/api/businesses/${businessId}/global-sms/profiles/${profileId}/reveal`);
|
|
setRevealedProfiles((current) => ({ ...current, [profileId]: res.data }));
|
|
return res.data;
|
|
}, [businessId, revealedProfiles]);
|
|
|
|
async function handleSubmit(event) {
|
|
event.preventDefault();
|
|
if (!formCurl.trim()) return;
|
|
|
|
setSaving(true);
|
|
setError('');
|
|
setSuccess('');
|
|
const shouldAutoAdvance = !isSetupComplete;
|
|
|
|
try {
|
|
await apiClient.post(`/api/businesses/${businessId}/global-sms/profiles`, {
|
|
rawCurl: formCurl.trim(),
|
|
setActive: formSetActive,
|
|
});
|
|
|
|
setFormCurl('');
|
|
setFormSetActive(true);
|
|
setSuccess('Profile created successfully.');
|
|
|
|
const nextState = await loadProfiles({ background: true });
|
|
if (shouldAutoAdvance && nextState.complete) {
|
|
navigate(eventsPath);
|
|
}
|
|
} catch (err) {
|
|
setError(err.response?.data?.error || 'Failed to save cURL profile');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}
|
|
|
|
async function handleActivate(profileId) {
|
|
const shouldAutoAdvance = !isSetupComplete;
|
|
setError('');
|
|
setSuccess('');
|
|
|
|
try {
|
|
await apiClient.post(`/api/businesses/${businessId}/global-sms/profiles/${profileId}/activate`);
|
|
const nextState = await loadProfiles({ background: true });
|
|
setSuccess('Active profile updated.');
|
|
if (shouldAutoAdvance && nextState.complete) {
|
|
navigate(eventsPath);
|
|
}
|
|
} catch (err) {
|
|
setError(err.response?.data?.error || 'Failed to activate profile');
|
|
}
|
|
}
|
|
|
|
async function handleCopyCurl(profile) {
|
|
try {
|
|
const revealData = await ensureRevealData(profile.id);
|
|
const textToCopy = revealData?.rawCurl || profile.maskedCurl || '';
|
|
if (!textToCopy) return;
|
|
|
|
await navigator.clipboard.writeText(textToCopy);
|
|
setSuccess(`Copied ${profile.name} cURL.`);
|
|
} catch {
|
|
setError('Failed to copy the cURL command.');
|
|
}
|
|
}
|
|
|
|
async function handleToggleCurl(profile) {
|
|
setError('');
|
|
|
|
if (!visibleProfileIds[profile.id]) {
|
|
try {
|
|
await ensureRevealData(profile.id);
|
|
} catch (err) {
|
|
setError(err.response?.data?.error || 'Failed to reveal stored cURL');
|
|
return;
|
|
}
|
|
}
|
|
|
|
setVisibleProfileIds((current) => ({
|
|
...current,
|
|
[profile.id]: !current[profile.id],
|
|
}));
|
|
}
|
|
|
|
async function handleProviderSubmit(event) {
|
|
event.preventDefault();
|
|
if (!activeProfileId || missingInputs.length === 0) return;
|
|
|
|
setSavingInputs(true);
|
|
setError('');
|
|
setSuccess('');
|
|
const shouldAutoAdvance = !isSetupComplete;
|
|
|
|
try {
|
|
const payload = buildProfilePatchPayload(missingInputs, inputForm);
|
|
await apiClient.patch(`/api/businesses/${businessId}/global-sms/profiles/${activeProfileId}`, payload);
|
|
setSuccess('Required profile fields saved.');
|
|
const nextState = await loadProfiles({ background: true });
|
|
if (shouldAutoAdvance && nextState.complete) {
|
|
navigate(eventsPath);
|
|
}
|
|
} catch (err) {
|
|
setError(err.response?.data?.error || 'Failed to save required profile fields');
|
|
} finally {
|
|
setSavingInputs(false);
|
|
}
|
|
}
|
|
|
|
async function handleDeleteRequest(profile) {
|
|
setError('');
|
|
setSuccess('');
|
|
|
|
try {
|
|
const res = await apiClient.get(`/api/businesses/${businessId}/global-sms/profiles/${profile.id}/delete-impact`);
|
|
setDeletePreview(res.data);
|
|
} catch (err) {
|
|
setError(err.response?.data?.error || 'Failed to load delete impact');
|
|
}
|
|
}
|
|
|
|
async function handleDeleteConfirm() {
|
|
if (!deletePreview?.profile?.id) return;
|
|
|
|
setDeletingProfileId(deletePreview.profile.id);
|
|
setError('');
|
|
setSuccess('');
|
|
|
|
try {
|
|
await apiClient.delete(`/api/businesses/${businessId}/global-sms/profiles/${deletePreview.profile.id}`);
|
|
setDeletePreview(null);
|
|
setVisibleProfileIds((current) => {
|
|
const nextState = { ...current };
|
|
delete nextState[deletePreview.profile.id];
|
|
return nextState;
|
|
});
|
|
setRevealedProfiles((current) => {
|
|
const nextState = { ...current };
|
|
delete nextState[deletePreview.profile.id];
|
|
return nextState;
|
|
});
|
|
await loadProfiles({ background: true });
|
|
setSuccess('Profile deleted successfully.');
|
|
} catch (err) {
|
|
setError(err.response?.data?.error || 'Failed to delete profile');
|
|
} finally {
|
|
setDeletingProfileId('');
|
|
}
|
|
}
|
|
|
|
if (initialLoading) {
|
|
return (
|
|
<div className="flex h-64 items-center justify-center">
|
|
<span className="h-8 w-8 animate-spin rounded-full border-4 border-spinner-track border-t-primary-blue" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<DeleteProfileModal
|
|
preview={deletePreview}
|
|
deleting={deletingProfileId === deletePreview?.profile?.id}
|
|
onCancel={() => setDeletePreview(null)}
|
|
onConfirm={handleDeleteConfirm}
|
|
/>
|
|
|
|
<div className="mx-auto max-w-4xl space-y-8 pb-12">
|
|
<div>
|
|
<div className="mb-2 flex flex-wrap items-center gap-3">
|
|
<h2 className="text-2xl font-bold text-text-primary">Omni-channel SMS</h2>
|
|
{refreshing && (
|
|
<span className="inline-flex items-center gap-2 rounded-full border border-gray-200 bg-white px-3 py-1 text-xs font-semibold text-gray-500">
|
|
<span className="h-2 w-2 animate-pulse rounded-full bg-primary-blue" />
|
|
Refreshing profiles
|
|
</span>
|
|
)}
|
|
</div>
|
|
<p className="text-sm text-text-muted">
|
|
Add and activate a validated provider cURL, complete the required profile fields, and then continue to event template setup.
|
|
</p>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="flex items-center justify-between rounded-md border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-error-text">
|
|
{error}
|
|
<button type="button" onClick={() => setError('')} className="font-bold text-error-text hover:text-red-900">
|
|
×
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{success && (
|
|
<div className="flex items-center justify-between rounded-md border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700">
|
|
{success}
|
|
<button type="button" onClick={() => setSuccess('')} className="font-bold text-gray-700 hover:opacity-75">
|
|
×
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{activeProfile ? (
|
|
<div className={`rounded-lg border p-5 ${activeProfile.executionReadiness?.isSetupComplete ? 'border-primary-blue bg-white' : 'border-gray-200 bg-white'}`}>
|
|
<div className="mb-4 flex items-center gap-3">
|
|
<h3 className="text-lg font-bold text-text-primary">
|
|
Active Setup:{' '}
|
|
<span className={isPendingSenderIdProfile(activeProfile) ? 'text-error-text' : 'text-text-primary'}>
|
|
{activeProfile.name}
|
|
</span>
|
|
</h3>
|
|
<span className="rounded-full border border-gray-200 bg-white px-3 py-1 text-xs font-bold uppercase tracking-wide text-gray-700">
|
|
{activeProfile.executionReadiness?.isSetupComplete ? 'Setup Complete' : 'Missing Information'}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="grid gap-5 md:grid-cols-2">
|
|
<div className="space-y-3">
|
|
<p className="text-sm font-medium text-text-primary">Current Profile Summary</p>
|
|
<ul className="space-y-2 text-sm">
|
|
<li className="flex items-center justify-between rounded border border-border-soft bg-surface-white p-2">
|
|
<span className="text-text-muted">Provider</span>
|
|
<span className="font-bold text-text-primary">
|
|
{activeProfile.provider?.providerName || <span className="text-xs uppercase text-error-text">Missing</span>}
|
|
</span>
|
|
</li>
|
|
<li className="flex items-center justify-between rounded border border-border-soft bg-surface-white p-2">
|
|
<span className="text-text-muted">Sender ID</span>
|
|
<span className="font-bold text-text-primary">
|
|
{activeProfile.provider?.senderId || <span className="text-xs uppercase text-error-text">Missing</span>}
|
|
</span>
|
|
</li>
|
|
<li className="flex items-center justify-between rounded border border-border-soft bg-surface-white p-2">
|
|
<span className="text-text-muted">DLT Entity ID</span>
|
|
<span className="font-bold text-text-primary">
|
|
{activeProfile.provider?.dltEntityId || <span className="text-xs uppercase text-error-text">Missing</span>}
|
|
</span>
|
|
</li>
|
|
<li className="rounded border border-border-soft bg-surface-white p-3">
|
|
<p className="text-xs font-semibold uppercase tracking-wide text-text-muted">Setup Status</p>
|
|
<p className="mt-2 text-sm text-text-primary">{getProfileSummary(activeProfile)}</p>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
{!activeProfile.executionReadiness?.isSetupComplete ? (
|
|
<div className="rounded-lg border border-border-main bg-surface-white p-4">
|
|
<p className="mb-3 text-sm font-semibold text-text-primary">Complete the required fields</p>
|
|
<form onSubmit={handleProviderSubmit} className="space-y-3">
|
|
{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={inputForm[input.key] || ''}
|
|
onChange={(event) => setInputForm((current) => ({
|
|
...current,
|
|
[input.key]: input.key === 'senderId'
|
|
? event.target.value.toUpperCase()
|
|
: event.target.value,
|
|
}))}
|
|
className="w-full rounded border border-border-main bg-page-bg px-3 py-2 text-sm text-text-primary focus:ring-1 focus:ring-primary-blue"
|
|
placeholder={input.label}
|
|
required={input.required !== false}
|
|
/>
|
|
</div>
|
|
))}
|
|
<button
|
|
type="submit"
|
|
disabled={savingInputs || missingInputs.some((input) => !String(inputForm[input.key] || '').trim())}
|
|
className="w-full rounded bg-primary-blue py-2 text-sm font-bold text-white transition hover:bg-primary-dark disabled:opacity-50"
|
|
>
|
|
{savingInputs ? 'Saving...' : 'Save Required Details'}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
) : (
|
|
<div className="flex h-full flex-col items-center justify-center space-y-4">
|
|
<p className="text-center text-sm font-medium text-text-muted">Your active cURL profile is fully configured.</p>
|
|
<button
|
|
type="button"
|
|
onClick={() => navigate(eventsPath)}
|
|
className="w-full rounded-lg bg-primary-blue px-6 py-2 text-sm font-semibold text-white transition hover:bg-primary-dark"
|
|
>
|
|
Continue to Events →
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
) : hasProfiles ? (
|
|
<div className="rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">
|
|
Select an active cURL profile to continue. Your saved profiles are still available below.
|
|
</div>
|
|
) : null}
|
|
|
|
{hasProfiles && (
|
|
<div className="space-y-4 border-t border-border-soft pt-4">
|
|
<h3 className="text-lg font-bold text-text-primary">Saved Profiles</h3>
|
|
{profiles.map((profile) => {
|
|
const isActive = profile.id === activeProfileId;
|
|
const isVisible = visibleProfileIds[profile.id] === true;
|
|
const revealedProfile = revealedProfiles[profile.id];
|
|
const displayCurl = isVisible ? (revealedProfile?.rawCurl || profile.maskedCurl) : profile.maskedCurl;
|
|
|
|
return (
|
|
<div
|
|
key={profile.id}
|
|
className={`rounded-xl border p-5 transition-colors ${isActive ? 'border-primary-blue bg-white' : 'border-border-main bg-surface-white'}`}
|
|
>
|
|
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
|
<div className="min-w-0 flex-1">
|
|
<div className="mb-3 flex flex-wrap items-center gap-3">
|
|
<h3 className={`truncate text-base font-bold ${isPendingSenderIdProfile(profile) ? 'text-error-text' : 'text-text-primary'}`}>{profile.name}</h3>
|
|
{isActive && (
|
|
<span className="rounded-full border border-indigo-200 bg-indigo-50 px-2.5 py-1 text-[11px] font-bold uppercase tracking-wider text-primary-dark">
|
|
Active Profile
|
|
</span>
|
|
)}
|
|
<span className={`rounded-full border px-2.5 py-1 text-[11px] font-bold uppercase tracking-wider ${profile.executionReadiness?.isSetupComplete ? 'border-emerald-200 bg-emerald-50 text-emerald-700' : 'border-amber-200 bg-amber-50 text-amber-700'}`}>
|
|
{profile.executionReadiness?.isSetupComplete ? 'Ready' : 'Needs Fields'}
|
|
</span>
|
|
</div>
|
|
<p className="mb-2 text-xs font-medium text-text-muted">Updated: {formatUpdatedAt(profile.updatedAt)}</p>
|
|
<p className="mb-3 text-sm text-text-muted">{getProfileSummary(profile)}</p>
|
|
<div className="overflow-hidden rounded-xl border border-gray-200 bg-gray-950">
|
|
<div className="flex items-center justify-between border-b border-gray-800 px-4 py-2">
|
|
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-gray-400">Stored cURL</p>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleToggleCurl(profile)}
|
|
className="text-xs font-semibold text-gray-300 transition hover:text-white"
|
|
>
|
|
{isVisible ? 'Hide' : 'Show'}
|
|
</button>
|
|
</div>
|
|
<pre className="max-h-56 overflow-auto px-4 py-4 text-xs leading-relaxed text-gray-100">
|
|
<code>{displayCurl || 'No cURL stored.'}</code>
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex shrink-0 flex-wrap items-center gap-2 lg:justify-end">
|
|
{!isActive && (
|
|
<button
|
|
type="button"
|
|
onClick={() => handleActivate(profile.id)}
|
|
className="rounded-lg bg-primary-blue px-4 py-2 text-sm font-semibold text-white transition hover:bg-primary-dark"
|
|
>
|
|
Use this cURL
|
|
</button>
|
|
)}
|
|
<button
|
|
type="button"
|
|
onClick={() => handleCopyCurl(profile)}
|
|
className="rounded-lg border border-border-main px-3 py-2 text-sm font-medium text-text-muted transition hover:border-primary-blue hover:bg-page-bg hover:text-primary-blue"
|
|
>
|
|
Copy
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleDeleteRequest(profile)}
|
|
className="rounded-lg border border-border-main px-3 py-2 text-sm font-medium text-text-muted transition hover:border-error-text hover:bg-red-50 hover:text-error-text"
|
|
>
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
<div className={`overflow-hidden rounded-xl border ${!hasProfiles ? 'border-indigo-100 bg-white shadow-sm' : 'border-border-main bg-surface-white'}`}>
|
|
<div className={`flex items-start justify-between gap-4 px-6 py-5 ${!hasProfiles ? 'border-b border-indigo-100 bg-indigo-50/60' : 'border-b border-border-main bg-table-header'}`}>
|
|
<div>
|
|
<h3 className="text-md font-bold text-text-primary">Add New Profile</h3>
|
|
<p className="mt-1 text-sm text-text-muted">
|
|
Paste a provider cURL exactly once. After validation, the stored cURL becomes immutable and can only be replaced by creating a new profile.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-6">
|
|
{!hasProfiles && (
|
|
<div className="mb-6 rounded-xl border border-indigo-100 bg-indigo-50/50 px-4 py-4">
|
|
<p className="text-sm font-semibold text-text-primary">Start by adding a cURL profile</p>
|
|
<p className="mt-1 text-sm text-text-muted">
|
|
This becomes the base for validating provider details and unlocking event template generation.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-5">
|
|
<div>
|
|
<label className="mb-1.5 block text-sm font-semibold text-text-primary">Provider cURL Command</label>
|
|
<textarea
|
|
value={formCurl}
|
|
onChange={(event) => setFormCurl(event.target.value)}
|
|
placeholder="curl --request POST --url ..."
|
|
className="h-48 w-full resize-none rounded-lg border border-border-main bg-white px-4 py-3 font-mono text-sm leading-relaxed text-text-primary transition focus:outline-none focus:ring-2 focus:ring-primary-blue"
|
|
required
|
|
spellCheck="false"
|
|
/>
|
|
<p className="mt-2 text-xs font-medium text-text-muted">
|
|
Profile name is generated automatically from Sender ID. If Sender ID is not detected yet, it stays as <span className="text-error-text">{PENDING_SENDER_ID_PROFILE_NAME}</span> until completed.
|
|
</p>
|
|
</div>
|
|
|
|
<label className="flex cursor-pointer items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
className="h-4 w-4 rounded border-border-main text-primary-blue focus:ring-primary-blue"
|
|
checked={formSetActive}
|
|
onChange={(event) => setFormSetActive(event.target.checked)}
|
|
/>
|
|
<span className="text-sm font-semibold text-text-primary">Set as active profile immediately</span>
|
|
</label>
|
|
|
|
<div className="flex gap-3 pt-2">
|
|
<button
|
|
type="submit"
|
|
disabled={saving}
|
|
className="flex items-center justify-center gap-2 rounded-lg bg-primary-blue px-6 py-2 text-sm font-semibold text-white transition hover:bg-primary-dark disabled:opacity-50"
|
|
>
|
|
{saving ? 'Saving…' : 'Save Profile'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|