import { mdiCreditCardOutline, mdiOpenInNew, mdiRefresh, mdiSend, mdiStarCircleOutline, } from '@mdi/js'; import axios from 'axios'; import Head from 'next/head'; import React, { ReactElement, useEffect, useMemo, useState } from 'react'; import BaseButton from '../components/BaseButton'; import CardBox from '../components/CardBox'; import FormField from '../components/FormField'; import SectionMain from '../components/SectionMain'; import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'; import LayoutAuthenticated from '../layouts/Authenticated'; import { getPageTitle } from '../config'; import { aiResponse } from '../stores/openAiSlice'; import { useAppDispatch, useAppSelector } from '../stores/hooks'; type BusinessType = 'local' | 'online' | 'hybrid'; type ReviewBusiness = { id: string; name?: string; business_type?: BusinessType; review_destination?: string; delay_days?: number; automation_mode?: string; followup_enabled?: boolean; followup_delay_days?: number; max_followups?: number; ai_reply_enabled?: boolean; referral_enabled?: boolean; referral_offer?: string; nps_enabled?: boolean; nps_question?: string; social_widget_enabled?: boolean; broadcast_enabled?: boolean; rebooking_enabled?: boolean; competitor_insights_enabled?: boolean; competitor_urls?: string; review_widget_theme?: string; brand_logo_url?: string; brand_primary_color?: string; email_sender_name?: string; email_reply_to?: string; email_footer_text?: string; email_subject_template?: string; email_body_template?: string; sms_template?: string; }; type SummaryResponse = { stats: { pending: number; sent: number; clicked: number; reviewed: number; customers: number; transactions: number; paymentEvents: number; }; businesses?: ReviewBusiness[]; primaryBusiness?: ReviewBusiness | null; }; type SubscriptionStatusResponse = { subscription: { planId: string; planName: string; effectiveStatus: string; isActive: boolean; }; }; type WidgetResponse = { embedCode?: string; business?: { name?: string }; reviews?: Array<{ id: string; rating?: number; title?: string; content?: string; reviewer?: string; source?: string; }>; }; type CompetitorInsightsResponse = { competitors: string[]; metrics: { reviewed: number; pending: number; sent: number; customers: number; }; recommendations: string[]; }; const businessTypeOptions: Array<{ key: BusinessType; label: string; help: string }> = [ { key: 'local', label: 'Local / service business', help: 'Use this for businesses that collect local profile reviews such as Google, Facebook, Yelp, Angi, or OpenTable.', }, { key: 'online', label: 'Online / ecommerce business', help: 'Use this for stores and online brands that collect product or ecommerce reviews.', }, { key: 'hybrid', label: 'Hybrid business', help: 'Use this when the same business needs both local and online review workflows.', }, ]; const reviewDestinationOptions = [ { key: 'google', label: 'Google', scope: 'local' }, { key: 'facebook', label: 'Facebook', scope: 'local' }, { key: 'yelp', label: 'Yelp', scope: 'local' }, { key: 'angi', label: 'Angi', scope: 'local' }, { key: 'opentable', label: 'OpenTable', scope: 'local' }, { key: 'shopify_hosted', label: 'Shopify hosted product review', scope: 'online' }, { key: 'trustpilot', label: 'Trustpilot', scope: 'online' }, { key: 'custom', label: 'Custom review page', scope: 'hybrid' }, ]; const defaultSettings = { businessName: 'Review Flow Business', businessType: 'hybrid' as BusinessType, reviewDestination: 'google', delayDays: '7', followupEnabled: true, followupDelayDays: '3', maxFollowups: '1', aiReplyEnabled: false, referralEnabled: false, referralOffer: 'Give $25, get $25 when a referred customer completes their first purchase.', npsEnabled: false, npsQuestion: 'How likely are you to recommend us to a friend?', socialWidgetEnabled: true, broadcastEnabled: false, rebookingEnabled: false, competitorInsightsEnabled: false, competitorUrls: '', reviewWidgetTheme: 'light', brandLogoUrl: '', brandPrimaryColor: '#4f46e5', emailSenderName: '', emailReplyTo: '', emailFooterText: 'Sent by Review Flow for {businessName}.', emailSubjectTemplate: 'How was your experience with {businessName}?', emailBodyTemplate: [ 'Hi {customerName},', '', 'Thank you for choosing {businessName}. We would love to hear about your experience.', '', 'Leave a review: {reviewLink}', '', 'Thank you,', '{businessName}', ].join('\n'), smsTemplate: 'Thanks for choosing {businessName}. Please leave a review: {reviewLink}', }; const defaultCampaign = { campaignType: 'broadcast', subject: 'Quick update from our team', message: 'Thanks for being a customer. We appreciate your support and wanted to share a quick update.', }; function normalizeBusinessType(value?: string): BusinessType { if (value === 'local' || value === 'online' || value === 'hybrid') return value; return 'hybrid'; } function destinationAllowedForBusinessType(businessType: BusinessType, destination: typeof reviewDestinationOptions[number]) { if (businessType === 'hybrid' || destination.scope === 'hybrid') return true; return destination.scope === businessType; } function getDestinationsForBusinessType(businessType: BusinessType) { return reviewDestinationOptions.filter((destination) => destinationAllowedForBusinessType(businessType, destination), ); } function getDefaultDestination(businessType: BusinessType) { return businessType === 'online' ? 'shopify_hosted' : 'google'; } function coerceDestination(businessType: BusinessType, destination?: string) { const destinations = getDestinationsForBusinessType(businessType); return destinations.some((option) => option.key === destination) ? destination || destinations[0].key : getDefaultDestination(businessType); } function businessToSettings(business?: ReviewBusiness | null) { if (!business) return defaultSettings; const businessType = normalizeBusinessType(business.business_type); return { businessName: business.name || defaultSettings.businessName, businessType, reviewDestination: coerceDestination(businessType, business.review_destination), delayDays: String(business.delay_days ?? 7), followupEnabled: business.followup_enabled !== false, followupDelayDays: String(business.followup_delay_days ?? 3), maxFollowups: String(business.max_followups ?? 1), aiReplyEnabled: Boolean(business.ai_reply_enabled), referralEnabled: Boolean(business.referral_enabled), referralOffer: business.referral_offer || defaultSettings.referralOffer, npsEnabled: Boolean(business.nps_enabled), npsQuestion: business.nps_question || defaultSettings.npsQuestion, socialWidgetEnabled: business.social_widget_enabled !== false, broadcastEnabled: Boolean(business.broadcast_enabled), rebookingEnabled: Boolean(business.rebooking_enabled), competitorInsightsEnabled: Boolean(business.competitor_insights_enabled), competitorUrls: business.competitor_urls || '', reviewWidgetTheme: business.review_widget_theme || 'light', brandLogoUrl: business.brand_logo_url || defaultSettings.brandLogoUrl, brandPrimaryColor: business.brand_primary_color || defaultSettings.brandPrimaryColor, emailSenderName: business.email_sender_name || defaultSettings.emailSenderName, emailReplyTo: business.email_reply_to || defaultSettings.emailReplyTo, emailFooterText: business.email_footer_text || defaultSettings.emailFooterText, emailSubjectTemplate: business.email_subject_template || defaultSettings.emailSubjectTemplate, emailBodyTemplate: business.email_body_template || defaultSettings.emailBodyTemplate, smsTemplate: business.sms_template || defaultSettings.smsTemplate, }; } function extractAiResponseText(response: any) { const output = response?.output || response?.data?.output || []; for (const item of output) { if (item?.type !== 'message') continue; for (const content of item.content || []) { if (content?.type === 'output_text' && content.text) { return String(content.text); } } } return ''; } function getCampaignLabel(campaignType: string) { if (campaignType === 'referral') return 'Referral campaign'; if (campaignType === 'nps') return 'NPS survey'; if (campaignType === 'rebooking') return 'Repeat business / rebooking'; return 'Marketing broadcast'; } export default function GrowthToolsPage() { const dispatch = useAppDispatch(); const { isAskingResponse, errorMessage: aiErrorMessage } = useAppSelector((state) => state.openAi); const [summary, setSummary] = useState(null); const [subscriptionStatus, setSubscriptionStatus] = useState(null); const [selectedBusinessId, setSelectedBusinessId] = useState(''); const [settingsForm, setSettingsForm] = useState(defaultSettings); const [campaignForm, setCampaignForm] = useState(defaultCampaign); const [aiReviewText, setAiReviewText] = useState('Great service, fast communication, and the team made everything easy.'); const [aiTone, setAiTone] = useState('friendly, concise, and professional'); const [aiSuggestion, setAiSuggestion] = useState(''); const [widget, setWidget] = useState(null); const [competitorInsights, setCompetitorInsights] = useState(null); const [message, setMessage] = useState(''); const [error, setError] = useState(''); const [isLoading, setIsLoading] = useState(true); const [isWorking, setIsWorking] = useState(false); const businesses = summary?.businesses || []; const selectedBusiness = useMemo( () => businesses.find((business) => business.id === selectedBusinessId) || summary?.primaryBusiness || null, [businesses, selectedBusinessId, summary?.primaryBusiness], ); const isGrowPlan = subscriptionStatus?.subscription.planId === 'starter'; const hasSelectedBusiness = Boolean(selectedBusinessId || selectedBusiness?.id); const loadData = async () => { setIsLoading(true); setError(''); try { const [summaryResponse, subscriptionResponse] = await Promise.all([ axios.get('/reviewflow/summary'), axios.get('/subscription/me'), ]); const loadedSummary = summaryResponse.data as SummaryResponse; const primaryBusiness = loadedSummary.primaryBusiness || loadedSummary.businesses?.[0] || null; setSummary(loadedSummary); setSubscriptionStatus(subscriptionResponse.data); if (primaryBusiness) { setSelectedBusinessId(primaryBusiness.id); setSettingsForm(businessToSettings(primaryBusiness)); } } catch (requestError) { console.error('Failed to load Growth Tools:', requestError); setError('Could not load Growth Tools. Refresh the page or try again.'); } finally { setIsLoading(false); } }; useEffect(() => { loadData(); }, []); const updateSettings = (key: keyof typeof defaultSettings, value: string | boolean) => { setSettingsForm((current) => { if (key === 'businessType') { const businessType = normalizeBusinessType(String(value)); return { ...current, businessType, reviewDestination: coerceDestination(businessType, current.reviewDestination), }; } return { ...current, [key]: value }; }); }; const selectBusiness = (businessId: string) => { const business = businesses.find((item) => item.id === businessId); setSelectedBusinessId(businessId); setSettingsForm(businessToSettings(business)); setWidget(null); setCompetitorInsights(null); }; const runDueAutomation = async () => { setIsWorking(true); setMessage(''); setError(''); try { const response = await axios.post('/reviewflow/automation/run-due', { limit: 100 }); setMessage( `Set-it-and-forget-it run complete: ${response.data.processed} processed, ${response.data.sent} handed off, ${response.data.failed} failed.`, ); await loadData(); } catch (requestError) { console.error('Failed to run due review automation:', requestError); if (axios.isAxiosError(requestError) && requestError.response?.data) { setError(String(requestError.response.data)); } else { setError('Could not run due automation. Please try again.'); } } finally { setIsWorking(false); } }; const loadWidget = async () => { if (!hasSelectedBusiness) return; setIsWorking(true); setMessage(''); setError(''); try { const response = await axios.get(`/reviewflow/social-widget/${selectedBusinessId || selectedBusiness?.id}`); setWidget(response.data); setMessage('Social proof widget refreshed. Copy the embed code into a website page where you want reviews to appear.'); } catch (requestError) { console.error('Failed to load social proof widget:', requestError); if (axios.isAxiosError(requestError) && requestError.response?.data) { setError(String(requestError.response.data)); } else { setError('Could not load the social proof widget.'); } } finally { setIsWorking(false); } }; const launchCampaign = async () => { setIsWorking(true); setMessage(''); setError(''); try { const response = await axios.post('/reviewflow/growth-tools/broadcast', { businessId: selectedBusinessId, ...campaignForm, }); setMessage(`${getCampaignLabel(campaignForm.campaignType)} queued: ${response.data.queued} customers, ${response.data.skipped} skipped.`); } catch (requestError) { console.error('Failed to queue Growth Tools campaign:', requestError); if (axios.isAxiosError(requestError) && requestError.response?.data) { setError(String(requestError.response.data)); } else { setError('Could not queue this campaign. Please try again.'); } } finally { setIsWorking(false); } }; const generateAiReply = async () => { setAiSuggestion(''); setError(''); if (isGrowPlan) { setError('Grow does not include AI review replies. Upgrade to Pro to unlock it.'); return; } const payload = { input: [ { role: 'system', content: 'You write short, warm, non-defensive review replies for a small business. Keep replies under 90 words and do not mention private customer data.', }, { role: 'user', content: `Business: ${settingsForm.businessName}\nTone: ${aiTone}\nCustomer review: ${aiReviewText}\nWrite one public reply.`, }, ], options: { poll_interval: 5, poll_timeout: 300 }, }; const resultAction = await dispatch(aiResponse(payload)); if (aiResponse.fulfilled.match(resultAction)) { const text = extractAiResponseText(resultAction.payload); setAiSuggestion(text || 'AI returned a response, but no text output was found.'); return; } console.error('AI reply assistant failed:', resultAction.payload || resultAction.error); setError('AI reply assistant failed. Check the AI proxy configuration and try again.'); }; const runCompetitorInsights = async () => { setIsWorking(true); setCompetitorInsights(null); setError(''); setMessage(''); try { const response = await axios.post('/reviewflow/growth-tools/competitor-insights', { businessId: selectedBusinessId, competitorUrls: settingsForm.competitorUrls, }); setCompetitorInsights(response.data); setMessage('Competitor insight checklist updated.'); } catch (requestError) { console.error('Failed to build competitor insights:', requestError); if (axios.isAxiosError(requestError) && requestError.response?.data) { setError(String(requestError.response.data)); } else { setError('Could not build competitor insights. Please try again.'); } } finally { setIsWorking(false); } }; return ( <> {getPageTitle('Growth Tools')} {''}

Growth actions · campaigns · widgets · AI replies

Use the tools that grow reviews after Setup is done.

Business configuration now lives in Setup. This page stays focused on the social widget, AI replies, campaigns, rebooking, NPS, competitor insights, and automation actions.

{[ ['Pending', summary?.stats.pending ?? 0], ['Sent', summary?.stats.sent ?? 0], ['Clicked', summary?.stats.clicked ?? 0], ['Reviewed', summary?.stats.reviewed ?? 0], ].map(([label, value]) => (
{value}
{label}
))}
{message && (
Done. {message}
)} {(error || aiErrorMessage) && (

{error || aiErrorMessage}

{(error || aiErrorMessage).includes('Upgrade to Pro') && ( )}
)}

Growth workspace

Use tools after Setup is complete

Growth Tools is now action-focused. Company info, payment connectors, review links, and branded templates live in Setup.

{businesses.length > 0 && ( )}

{selectedBusiness?.name || settingsForm.businessName || 'No business selected'}

Review workflow: {settingsForm.businessType} · destination: {settingsForm.reviewDestination}

If this looks wrong, update it in Setup instead of editing it here.

Grow

Social proof widget

Grow includes an embeddable widget for hosted reviews. It displays verified reviews after customers submit them through Review Flow.

{widget?.embedCode && (

Embed code

{widget.embedCode}
)} {widget?.reviews && (
{widget.reviews.length === 0 ? (
No hosted reviews yet.
) : widget.reviews.map((review) => (

{'★'.repeat(review.rating || 5)}

{review.title || 'Customer review'}

{review.content}

))}
)}

Pro

AI review reply assistant

{isGrowPlan && Pro}