First build
Some checks failed
Client Admin / build-deploy (push) Failing after 8s
Client Client / build-deploy (push) Failing after 3s
Client Registration / build-deploy (push) Failing after 20s
Client Tech / build-deploy (push) Failing after 1s
Client Home / build-deploy (push) Successful in 14s
Some checks failed
Client Admin / build-deploy (push) Failing after 8s
Client Client / build-deploy (push) Failing after 3s
Client Registration / build-deploy (push) Failing after 20s
Client Tech / build-deploy (push) Failing after 1s
Client Home / build-deploy (push) Successful in 14s
This commit is contained in:
564
Client-Client/src/components/wizard/steps/ReviewStep.jsx
Normal file
564
Client-Client/src/components/wizard/steps/ReviewStep.jsx
Normal file
@@ -0,0 +1,564 @@
|
||||
import React, { useState, memo } from 'react';
|
||||
import { useAuth } from '../../../auth/AuthProvider';
|
||||
import { useWizardConfig } from '../../../context/WizardConfigContext';
|
||||
import { stageInitiative, launchInitiative } from '../../../services/initiativeApi';
|
||||
import HelpIcon from '../../../components/HelpIcon';
|
||||
|
||||
const statusIcons = {
|
||||
submitted: '✓', pending_review: '⏳', error: '✗', pending: '…',
|
||||
};
|
||||
const statusLabels = {
|
||||
submitted: 'Submitted', pending_review: 'Queued for Review',
|
||||
error: 'Failed', pending: 'Pending',
|
||||
};
|
||||
|
||||
const ageLabels = {
|
||||
AGE_18_24: '18–24', AGE_25_34: '25–34', AGE_35_44: '35–44',
|
||||
AGE_45_54: '45–54', AGE_55_64: '55–64', AGE_65_UP: '65+',
|
||||
};
|
||||
const genderLabels = { MALE: 'Male', FEMALE: 'Female', UNDETERMINED: 'All Genders' };
|
||||
const scopeLabels = { local: 'Local', regional: 'Regional', national: 'National' };
|
||||
const skewLabels = { young: 'Younger', mature: 'Mature' };
|
||||
|
||||
function SectionCard({ title, icon, children, onEdit, issues, onFix }) {
|
||||
const hasIssues = issues && issues.length > 0;
|
||||
return (
|
||||
<div className={`review-section ${hasIssues ? 'has-issues' : ''}`}>
|
||||
<div className="review-section-header">
|
||||
<div className="review-section-title">
|
||||
<span className="review-icon">{icon}</span>
|
||||
<h3>{title}</h3>
|
||||
</div>
|
||||
<div className="review-section-actions">
|
||||
{hasIssues && (
|
||||
<button className="btn btn-sm btn-fix" onClick={onFix || onEdit}>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<path d="M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
Fix
|
||||
</button>
|
||||
)}
|
||||
{onEdit && (
|
||||
<button className="btn btn-sm btn-outline" onClick={onEdit}>Edit</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{hasIssues && (
|
||||
<div className="review-section-issues">
|
||||
{issues.map((msg, idx) => (
|
||||
<span key={idx} className="review-issue-tag">{msg}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="review-section-body">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ReviewRow({ label, value, mono }) {
|
||||
return (
|
||||
<div className="review-row">
|
||||
<span className="review-label">{label}</span>
|
||||
<span className={`review-value ${mono ? 'mono' : ''}`}>{value || '—'}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ReviewStep = memo(function ReviewStep({ stepData, onGoToStep }) {
|
||||
const { sessionToken } = useAuth();
|
||||
const { toDisplayLabel, getChannelLabel, getChannelColor } = useWizardConfig();
|
||||
|
||||
const [phase, setPhase] = useState('review');
|
||||
const [error, setError] = useState(null);
|
||||
const [initiativeId, setInitiativeId] = useState(null);
|
||||
const [billing, setBilling] = useState(null);
|
||||
const [launchResults, setLaunchResults] = useState(null);
|
||||
const [showConfirm, setShowConfirm] = useState(false);
|
||||
const [termsAccepted, setTermsAccepted] = useState(false);
|
||||
|
||||
// ── Step data: 1=Objective, 2=Audience, 3=Budget, 4=Channels+Allocation, 5=Creative ──
|
||||
const objective = stepData?.[1] || {};
|
||||
const audience = stepData?.[2] || {};
|
||||
const budget = stepData?.[3] || {};
|
||||
const channelsAlloc = stepData?.[4] || {};
|
||||
const creative = stepData?.[5] || {};
|
||||
|
||||
const selectedChannels = channelsAlloc.selectedChannels || [];
|
||||
const isMultiChannel = selectedChannels.length > 1;
|
||||
const totalBudget = parseFloat(budget.totalBudget) || 0;
|
||||
const allocations = channelsAlloc.allocations || {};
|
||||
|
||||
const objectiveLabel = toDisplayLabel(objective.objective);
|
||||
const categoryLabel = toDisplayLabel(objective.businessCategory);
|
||||
|
||||
const formatBudget = (val) => {
|
||||
const n = parseFloat(val);
|
||||
return isNaN(n) ? '—' : `$${n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||
};
|
||||
|
||||
// Validation
|
||||
const issues = [];
|
||||
if (!objective.campaignName) issues.push({ step: 1, msg: 'Campaign name is required' });
|
||||
if (!objective.objective) issues.push({ step: 1, msg: 'Objective is required' });
|
||||
if (selectedChannels.length === 0) issues.push({ step: 4, msg: 'Select at least one channel' });
|
||||
if (!totalBudget || totalBudget <= 0) issues.push({ step: 3, msg: 'Budget must be set' });
|
||||
if (isMultiChannel) {
|
||||
const totalPct = Object.values(allocations).reduce((s, v) => s + (v || 0), 0);
|
||||
if (Math.abs(totalPct - 100) > 0.01) issues.push({ step: 4, msg: 'Channel allocations must total 100%' });
|
||||
}
|
||||
|
||||
// ── Stage ──
|
||||
const handleStage = async () => {
|
||||
if (issues.length > 0) return;
|
||||
setError(null);
|
||||
setPhase('staging');
|
||||
|
||||
const channelPayloads = selectedChannels.map(ch => ({
|
||||
channelType: ch,
|
||||
allocationPct: isMultiChannel ? (allocations[ch] || 0) : 100,
|
||||
}));
|
||||
|
||||
const payload = {
|
||||
name: objective.campaignName,
|
||||
objective: objective.objective,
|
||||
totalBudget: totalBudget,
|
||||
budgetPeriod: budget.budgetPeriod || 'monthly',
|
||||
startDate: budget.startDate,
|
||||
endDate: budget.hasEndDate ? budget.endDate : null,
|
||||
allocationStrategy: channelsAlloc.allocationStrategy || 'manual',
|
||||
businessCategory: objective.businessCategory,
|
||||
channels: channelPayloads,
|
||||
// Audience factors
|
||||
ageSkew: audience.ageSkew || null,
|
||||
marketScope: audience.marketScope || null,
|
||||
locations: audience.locations || [],
|
||||
demographics: audience.demographics || {},
|
||||
...(initiativeId ? { initiativeId } : {}),
|
||||
};
|
||||
|
||||
const stageRes = await stageInitiative(payload, sessionToken);
|
||||
|
||||
if (!stageRes.ok) {
|
||||
setPhase('error');
|
||||
setError(stageRes.error || stageRes.data?.error || 'Failed to stage initiative');
|
||||
return;
|
||||
}
|
||||
|
||||
const newId = stageRes.data?.initiativeId;
|
||||
if (!newId) {
|
||||
setPhase('error');
|
||||
setError('Initiative staged but no ID returned');
|
||||
return;
|
||||
}
|
||||
|
||||
setInitiativeId(newId);
|
||||
setBilling(stageRes.data?.billing || null);
|
||||
setTermsAccepted(false);
|
||||
setShowConfirm(true);
|
||||
setPhase('confirm');
|
||||
};
|
||||
|
||||
// ── Launch ──
|
||||
const handleConfirmLaunch = async () => {
|
||||
setShowConfirm(false);
|
||||
setError(null);
|
||||
setPhase('launching');
|
||||
|
||||
const launchRes = await launchInitiative(initiativeId, sessionToken);
|
||||
|
||||
if (launchRes.ok || launchRes.data?.channels) {
|
||||
setLaunchResults(launchRes.data);
|
||||
setPhase('done');
|
||||
} else {
|
||||
setLaunchResults(launchRes.data);
|
||||
setPhase('done');
|
||||
if (!launchRes.data?.channels?.length) {
|
||||
setError(launchRes.error || launchRes.data?.error || 'Launch failed — campaigns saved but not dispatched');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelConfirm = () => {
|
||||
setShowConfirm(false);
|
||||
setPhase('review');
|
||||
};
|
||||
|
||||
// ── Success / Results Screen ──
|
||||
if (phase === 'done') {
|
||||
const channelResults = launchResults?.channels || [];
|
||||
const successCount = channelResults.filter(c => c.status === 'submitted' || c.status === 'pending_review').length;
|
||||
const failCount = channelResults.filter(c => c.status === 'error').length;
|
||||
|
||||
return (
|
||||
<div className="wizard-step-content">
|
||||
<div className="submit-success">
|
||||
<div className="success-icon">
|
||||
{failCount === 0 ? (
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#10B981" strokeWidth="2">
|
||||
<path d="M22 11.08V12a10 10 0 11-5.93-9.14" />
|
||||
<path d="M22 4L12 14.01l-3-3" />
|
||||
</svg>
|
||||
) : successCount > 0 ? (
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#F59E0B" strokeWidth="2">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<line x1="12" y1="8" x2="12" y2="12" />
|
||||
<line x1="12" y1="16" x2="12.01" y2="16" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#EF4444" strokeWidth="2">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<line x1="15" y1="9" x2="9" y2="15" />
|
||||
<line x1="9" y1="9" x2="15" y2="15" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h2>
|
||||
{failCount === 0
|
||||
? `Campaign ${isMultiChannel ? 'Initiative ' : ''}Launched!`
|
||||
: successCount > 0
|
||||
? 'Partially Launched'
|
||||
: 'Launch Failed'}
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
{failCount === 0
|
||||
? `Your campaign "${objective.campaignName}" has been submitted${isMultiChannel ? ` across ${selectedChannels.length} channels` : ''}.`
|
||||
: successCount > 0
|
||||
? `${successCount} of ${channelResults.length} channels submitted successfully.`
|
||||
: 'All channel dispatches failed. Your campaign has been saved — you can retry from the Campaigns view.'}
|
||||
</p>
|
||||
|
||||
{channelResults.length > 0 && (
|
||||
<div className="launch-results">
|
||||
{channelResults.map((ch, idx) => (
|
||||
<div key={idx} className={`launch-result-row ${ch.status}`}>
|
||||
<span className="launch-ch-dot" style={{ background: getChannelColor(ch.channelType) }} />
|
||||
<span className="launch-ch-name">{getChannelLabel(ch.channelType)}</span>
|
||||
<span className={`launch-status ${ch.status}`}>
|
||||
{statusIcons[ch.status] || '?'} {statusLabels[ch.status] || ch.status}
|
||||
</span>
|
||||
{ch.error && <span className="launch-error">{ch.error}</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Loading states ──
|
||||
if (phase === 'staging') {
|
||||
return (
|
||||
<div className="wizard-step-content">
|
||||
<div className="phase-loading">
|
||||
<div className="loading-spinner" />
|
||||
<h3>Preparing your campaign…</h3>
|
||||
<p>Calculating fees and validating configuration</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (phase === 'launching') {
|
||||
return (
|
||||
<div className="wizard-step-content">
|
||||
<div className="phase-loading">
|
||||
<div className="loading-spinner" />
|
||||
<h3>Launching campaign…</h3>
|
||||
<p>Submitting to {isMultiChannel ? `${selectedChannels.length} ad networks` : 'ad network'}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Audience summary helpers ──
|
||||
const locationSummary = audience.locations?.length > 0
|
||||
? audience.locations.map(l => l.name).join(', ')
|
||||
: 'All regions (national)';
|
||||
const ageSummary = audience.demographics?.ageRanges?.length > 0
|
||||
? audience.demographics.ageRanges.map(a => ageLabels[a] || a).join(', ')
|
||||
: 'All ages';
|
||||
const genderSummary = audience.demographics?.genders?.length > 0
|
||||
? audience.demographics.genders.map(g => genderLabels[g] || g).join(', ')
|
||||
: 'All genders';
|
||||
|
||||
// Group issues by step for inline display
|
||||
const issuesByStep = {};
|
||||
issues.forEach(issue => {
|
||||
if (!issuesByStep[issue.step]) issuesByStep[issue.step] = [];
|
||||
issuesByStep[issue.step].push(issue.msg);
|
||||
});
|
||||
|
||||
// ── Review Layout ──
|
||||
return (
|
||||
<div className="wizard-step-content">
|
||||
<div className="step-intro">
|
||||
<div className="step-intro-header">
|
||||
<h2>Review your campaign</h2>
|
||||
<HelpIcon helpKey="client.wizard.review" label="About this step" />
|
||||
</div>
|
||||
<p>Double-check everything below before launching.</p>
|
||||
</div>
|
||||
|
||||
{error && <div className="error-box">{error}</div>}
|
||||
|
||||
{/* Campaign Summary - Compact Two-Column */}
|
||||
<div className="review-grid">
|
||||
{/* Left Column: Campaign + Audience */}
|
||||
<div className="review-grid-col">
|
||||
<SectionCard
|
||||
title="Campaign" icon="🎯"
|
||||
onEdit={() => onGoToStep(1)}
|
||||
issues={issuesByStep[1]}
|
||||
onFix={() => onGoToStep(1)}
|
||||
>
|
||||
<ReviewRow label="Name" value={objective.campaignName} />
|
||||
<ReviewRow label="Objective" value={objectiveLabel} />
|
||||
<ReviewRow label="Category" value={categoryLabel} />
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard title="Audience" icon="👥" onEdit={() => onGoToStep(2)}>
|
||||
<ReviewRow label="Locations" value={locationSummary} />
|
||||
<ReviewRow label="Age" value={ageSummary} />
|
||||
<ReviewRow label="Gender" value={genderSummary} />
|
||||
<div className="review-factors" style={{ marginTop: 8 }}>
|
||||
<span className="review-factor-chip">
|
||||
{scopeLabels[audience.marketScope] || 'National'}
|
||||
</span>
|
||||
<span className="review-factor-chip">
|
||||
{audience.ageSkew ? skewLabels[audience.ageSkew] : 'Balanced'}
|
||||
</span>
|
||||
</div>
|
||||
</SectionCard>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Budget + Channels + Creative */}
|
||||
<div className="review-grid-col">
|
||||
<SectionCard
|
||||
title="Budget" icon="💰"
|
||||
onEdit={() => onGoToStep(3)}
|
||||
issues={issuesByStep[3]}
|
||||
onFix={() => onGoToStep(3)}
|
||||
>
|
||||
<ReviewRow
|
||||
label={budget.budgetPeriod === 'daily' ? 'Daily' : 'Monthly'}
|
||||
value={formatBudget(totalBudget)}
|
||||
/>
|
||||
<ReviewRow label="Start" value={budget.startDate} />
|
||||
{budget.hasEndDate && <ReviewRow label="End" value={budget.endDate} />}
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard
|
||||
title="Channels" icon="📡"
|
||||
onEdit={() => onGoToStep(4)}
|
||||
issues={issuesByStep[4]}
|
||||
onFix={() => onGoToStep(4)}
|
||||
>
|
||||
<div className="review-channels">
|
||||
{selectedChannels.length > 0 ? selectedChannels.map(ch => (
|
||||
<span key={ch} className="review-channel-chip" style={{ borderColor: getChannelColor(ch) }}>
|
||||
<span className="review-ch-dot" style={{ background: getChannelColor(ch) }} />
|
||||
{getChannelLabel(ch)}
|
||||
{isMultiChannel && <span style={{ marginLeft: 'auto', fontSize: 12, color: 'var(--color-text-muted)' }}>{allocations[ch] || 0}%</span>}
|
||||
</span>
|
||||
)) : (
|
||||
<div className="review-empty-note">No channels selected</div>
|
||||
)}
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard title="Creative" icon="🖼️" onEdit={() => onGoToStep(5)}>
|
||||
{(creative.headlines?.length > 0 || creative.descriptions?.length > 0) ? (
|
||||
<div className="review-creative-summary">
|
||||
<span>{creative.headlines?.length || 0} headline{(creative.headlines?.length || 0) !== 1 ? 's' : ''}</span>
|
||||
<span className="review-dot-sep">·</span>
|
||||
<span>{creative.descriptions?.length || 0} description{(creative.descriptions?.length || 0) !== 1 ? 's' : ''}</span>
|
||||
<span className="review-dot-sep">·</span>
|
||||
<span>{creative.images?.length || 0} image{(creative.images?.length || 0) !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="review-empty-note">No creative assets added yet</div>
|
||||
)}
|
||||
</SectionCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit */}
|
||||
<div className="submit-bar">
|
||||
<div className="submit-bar-info">
|
||||
<strong>{issues.length > 0 ? 'Almost there' : 'Ready to launch?'}</strong>
|
||||
<span>
|
||||
{issues.length > 0
|
||||
? `Fix ${issues.length} issue${issues.length > 1 ? 's' : ''} above to enable launch.`
|
||||
: isMultiChannel
|
||||
? `Your campaign will be submitted to ${selectedChannels.map(ch => getChannelLabel(ch)).join(' and ')} for review.`
|
||||
: `Your campaign will be submitted to ${getChannelLabel(selectedChannels[0])} for review.`}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-primary btn-lg"
|
||||
onClick={handleStage}
|
||||
disabled={issues.length > 0}
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z" />
|
||||
</svg>
|
||||
{' '}Launch Campaign
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Confirmation Modal */}
|
||||
{showConfirm && billing && (
|
||||
<ConfirmLaunchModal
|
||||
campaignName={objective.campaignName}
|
||||
objectiveLabel={objectiveLabel}
|
||||
billing={billing}
|
||||
selectedChannels={selectedChannels}
|
||||
isMultiChannel={isMultiChannel}
|
||||
termsAccepted={termsAccepted}
|
||||
onToggleTerms={() => setTermsAccepted(!termsAccepted)}
|
||||
onConfirm={handleConfirmLaunch}
|
||||
onCancel={handleCancelConfirm}
|
||||
formatBudget={formatBudget}
|
||||
getChannelLabel={getChannelLabel}
|
||||
getChannelColor={getChannelColor}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default ReviewStep;
|
||||
|
||||
// ────────────────────────────────────────────────
|
||||
// Confirmation Modal
|
||||
// ────────────────────────────────────────────────
|
||||
|
||||
function ConfirmLaunchModal({
|
||||
campaignName, objectiveLabel, billing,
|
||||
selectedChannels, isMultiChannel,
|
||||
termsAccepted, onToggleTerms, onConfirm, onCancel, formatBudget,
|
||||
getChannelLabel, getChannelColor,
|
||||
}) {
|
||||
const periodLabel = billing.budgetPeriod === 'daily' ? 'day' : 'month';
|
||||
const marginPct = ((billing.marginRate || 0) * 100).toFixed(0);
|
||||
const channelBilling = billing.channels || [];
|
||||
|
||||
return (
|
||||
<div className="confirm-overlay" onClick={onCancel}>
|
||||
<div className="confirm-modal" onClick={e => e.stopPropagation()}>
|
||||
<div className="confirm-header">
|
||||
<div className="confirm-icon-wrap">
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#d97706" strokeWidth="2">
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
|
||||
<line x1="12" y1="9" x2="12" y2="13" />
|
||||
<line x1="12" y1="17" x2="12.01" y2="17" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2>Confirm Campaign Launch</h2>
|
||||
</div>
|
||||
|
||||
<p className="confirm-subtitle">
|
||||
You are about to launch <strong>{campaignName}</strong>. By proceeding,
|
||||
you authorize AdPlatform to charge your payment method on file for the
|
||||
advertising spend and management fees described below.
|
||||
</p>
|
||||
|
||||
<div className="confirm-billing">
|
||||
<div className="confirm-billing-row">
|
||||
<span>Campaign objective</span>
|
||||
<span>{objectiveLabel}</span>
|
||||
</div>
|
||||
|
||||
<div className="confirm-billing-row">
|
||||
<span>Ad spend</span>
|
||||
<span>{formatBudget(billing.adSpend)} / {periodLabel}</span>
|
||||
</div>
|
||||
|
||||
{isMultiChannel && channelBilling.map((ch, idx) => (
|
||||
<div key={idx} className="confirm-billing-row confirm-billing-sub">
|
||||
<span>
|
||||
<span className="confirm-ch-dot" style={{ background: getChannelColor(ch.channelType) }} />
|
||||
{getChannelLabel(ch.channelType)} ({ch.allocationPct}%)
|
||||
</span>
|
||||
<span>{formatBudget(ch.adSpend)} / {periodLabel}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="confirm-billing-row">
|
||||
<span>Management fee ({marginPct}%)</span>
|
||||
<span>{formatBudget(billing.managementFee)} / {periodLabel}</span>
|
||||
</div>
|
||||
|
||||
{isMultiChannel && channelBilling.map((ch, idx) => (
|
||||
<div key={`fee-${idx}`} className="confirm-billing-row confirm-billing-sub">
|
||||
<span>
|
||||
<span className="confirm-ch-dot" style={{ background: getChannelColor(ch.channelType) }} />
|
||||
{getChannelLabel(ch.channelType)}
|
||||
</span>
|
||||
<span>{formatBudget(ch.managementFee)} / {periodLabel}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{billing.platformFee > 0 && (
|
||||
<div className="confirm-billing-row">
|
||||
<span>Platform fee</span>
|
||||
<span>{formatBudget(billing.platformFee)} / {periodLabel}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="confirm-billing-row confirm-billing-total">
|
||||
<span>Total recurring charge</span>
|
||||
<span className="confirm-amount">{formatBudget(billing.totalCharge)} / {periodLabel}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="confirm-note">
|
||||
Ad spend will be billed to your payment method on file. Actual ad network
|
||||
charges may vary based on delivery and auction dynamics. Management fees
|
||||
are calculated at {marginPct}% of ad spend
|
||||
{billing.minManagementFee > 0 && ` (${formatBudget(billing.minManagementFee)}/mo minimum)`}.
|
||||
You can pause or cancel campaigns at any time from the Campaigns dashboard.
|
||||
{billing.pricingSource === 'client' && (
|
||||
<span className="confirm-pricing-source" title="This client has a negotiated rate">
|
||||
{' '}— Custom rate applied
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<label className="confirm-terms">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={termsAccepted}
|
||||
onChange={onToggleTerms}
|
||||
/>
|
||||
<span>
|
||||
I authorize AdPlatform to charge up to <strong>{formatBudget(billing.totalCharge)} / {periodLabel}</strong> to
|
||||
my payment method on file. I understand that ad networks may take up
|
||||
to 48 hours to review and approve my campaigns, and that actual
|
||||
delivery and charges may vary.
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div className="confirm-actions">
|
||||
<button className="btn btn-outline" onClick={onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary btn-lg"
|
||||
onClick={onConfirm}
|
||||
disabled={!termsAccepted}
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<rect x="1" y="4" width="22" height="16" rx="2" ry="2" />
|
||||
<line x1="1" y1="10" x2="23" y2="10" />
|
||||
</svg>
|
||||
{' '}Confirm & Launch
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user