feat: add SwitchOffer component to promote switching from competitors for enhanced user engagement
This commit is contained in:
@@ -230,6 +230,14 @@ export class Users {
|
||||
client_reference_id: project.id, // Store project ID for webhook
|
||||
line_items: lineItems,
|
||||
...(checkoutCurrency && {currency: checkoutCurrency}),
|
||||
custom_fields: [
|
||||
{
|
||||
key: 'promo_code',
|
||||
label: {type: 'custom', custom: 'Promo code'},
|
||||
type: 'text',
|
||||
optional: true,
|
||||
},
|
||||
],
|
||||
subscription_data: {
|
||||
billing_cycle_anchor: billingCycleAnchor,
|
||||
},
|
||||
|
||||
@@ -530,10 +530,24 @@ export class Webhooks {
|
||||
},
|
||||
});
|
||||
|
||||
// Base onboarding credit: refund the 1-unit card-verification charge
|
||||
let creditBalance = -100;
|
||||
|
||||
// Switching-offer promo: 2 extra units of credit if the customer typed SWITCH
|
||||
// into the Promo code custom field on Stripe Checkout.
|
||||
const promoField = session.custom_fields?.find((f) => f.key === 'promo_code');
|
||||
const promoCode = promoField?.text?.value?.trim().toUpperCase();
|
||||
if (promoCode === 'SWITCH') {
|
||||
creditBalance -= 200;
|
||||
signale.success(`[WEBHOOK] SWITCH promo applied for project ${projectId}`);
|
||||
} else if (promoCode) {
|
||||
signale.info(`[WEBHOOK] Unknown promo code "${promoCode}" entered for project ${projectId}`);
|
||||
}
|
||||
|
||||
// Update Stripe customer name to match project name and add credit for onboarding fee
|
||||
await stripe.customers.update(customerId, {
|
||||
name: updatedProject.name,
|
||||
balance: -100,
|
||||
balance: creditBalance,
|
||||
});
|
||||
|
||||
signale.success(`[WEBHOOK] Checkout completed for project ${projectId}`);
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import {AnimatePresence, motion} from 'framer-motion';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {ArrowRight, Gift, X} from 'lucide-react';
|
||||
import {DASHBOARD_URI} from '../lib/constants';
|
||||
|
||||
interface SwitchOfferProps {
|
||||
competitorName: string;
|
||||
}
|
||||
|
||||
export function SwitchOffer({competitorName}: SwitchOfferProps) {
|
||||
const storageKey = `switch-offer-dismissed:${competitorName}`;
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
if (window.localStorage.getItem(storageKey) === '1') return;
|
||||
const t = window.setTimeout(() => setVisible(true), 800);
|
||||
return () => window.clearTimeout(t);
|
||||
}, [storageKey]);
|
||||
|
||||
const dismiss = () => {
|
||||
setVisible(false);
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.setItem(storageKey, '1');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{visible && (
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 24, scale: 0.96}}
|
||||
animate={{opacity: 1, y: 0, scale: 1}}
|
||||
exit={{opacity: 0, y: 16, scale: 0.97}}
|
||||
transition={{duration: 0.45, ease: [0.22, 1, 0.36, 1]}}
|
||||
className={
|
||||
'pointer-events-auto fixed bottom-4 right-4 z-50 w-[calc(100vw-2rem)] max-w-sm sm:bottom-6 sm:right-6'
|
||||
}
|
||||
role="dialog"
|
||||
aria-label={`Switch from ${competitorName} to Plunk offer`}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
'relative overflow-hidden rounded-2xl border border-neutral-900 bg-white shadow-[0_24px_60px_-20px_rgba(0,0,0,0.35)]'
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={dismiss}
|
||||
aria-label="Dismiss offer"
|
||||
className={
|
||||
'absolute right-2.5 top-2.5 inline-flex h-7 w-7 items-center justify-center rounded-full text-neutral-500 transition hover:bg-neutral-100 hover:text-neutral-900'
|
||||
}
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
|
||||
<div className={'p-5 pr-10 sm:p-6 sm:pr-12'}>
|
||||
<div
|
||||
className={
|
||||
'inline-flex items-center gap-1.5 rounded-full border border-neutral-200 bg-neutral-50 px-2 py-1'
|
||||
}
|
||||
>
|
||||
<Gift className="h-3 w-3 text-neutral-700" />
|
||||
<span
|
||||
style={{fontFamily: 'var(--font-mono)'}}
|
||||
className={'text-[10px] uppercase tracking-[0.18em] text-neutral-600'}
|
||||
>
|
||||
Switching offer
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3
|
||||
style={{fontFamily: 'var(--font-display)'}}
|
||||
className={'mt-3 text-lg font-bold leading-[1.15] tracking-[-0.02em] text-neutral-900'}
|
||||
>
|
||||
Switching from {competitorName}?
|
||||
<br />
|
||||
<span className={'text-neutral-500'}>Get 2,000 free emails.</span>
|
||||
</h3>
|
||||
|
||||
<p className={'mt-3 text-xs leading-relaxed text-neutral-600'}>
|
||||
Sign up and enter this code at checkout to redeem 2,000 email credits.
|
||||
</p>
|
||||
|
||||
<div className={'mt-3 rounded-xl border border-dashed border-neutral-300 bg-neutral-50 px-3 py-2.5'}>
|
||||
<div className={'mt-1 flex items-baseline justify-between gap-2'}>
|
||||
<span
|
||||
style={{fontFamily: 'var(--font-mono)'}}
|
||||
className={'text-base font-bold tracking-[0.08em] text-neutral-900'}
|
||||
>
|
||||
SWITCH
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.a
|
||||
whileHover={{scale: 1.015}}
|
||||
whileTap={{scale: 0.985}}
|
||||
href={`${DASHBOARD_URI}/auth/signup`}
|
||||
className={
|
||||
'group mt-4 inline-flex w-full items-center justify-center gap-1.5 rounded-full bg-neutral-900 px-4 py-2.5 text-xs font-semibold text-white transition hover:bg-neutral-800'
|
||||
}
|
||||
>
|
||||
Claim your credits
|
||||
<ArrowRight className="h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5" />
|
||||
</motion.a>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
@@ -4,3 +4,4 @@ export * from './ComparisonTable';
|
||||
export * from './FAQSection';
|
||||
export * from './CodeBlock';
|
||||
export * from './SectionHeader';
|
||||
export * from './SwitchOffer';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar} from '../../components';
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar, SwitchOffer} from '../../components';
|
||||
import {motion} from 'framer-motion';
|
||||
import {DASHBOARD_URI, WIKI_URI} from '../../lib/constants';
|
||||
import React from 'react';
|
||||
@@ -170,6 +170,8 @@ export default function ActiveCampaignComparison() {
|
||||
{/* FAQ */}
|
||||
<FAQSection faqs={faqs} schemaId="faq-schema-activecampaign" />
|
||||
|
||||
<SwitchOffer competitorName="ActiveCampaign" />
|
||||
|
||||
{/* CTA */}
|
||||
<section className={'relative overflow-hidden border-t border-neutral-900 bg-neutral-900 text-white'}>
|
||||
<div className={'mx-auto max-w-[88rem] px-6 py-24 sm:px-10 sm:py-32'}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar} from '../../components';
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar, SwitchOffer} from '../../components';
|
||||
import {motion} from 'framer-motion';
|
||||
import {DASHBOARD_URI, WIKI_URI} from '../../lib/constants';
|
||||
import React from 'react';
|
||||
@@ -186,6 +186,8 @@ export default function AmazonSesComparison() {
|
||||
{/* FAQ */}
|
||||
<FAQSection faqs={faqs} schemaId="faq-schema-amazon-ses" />
|
||||
|
||||
<SwitchOffer competitorName="Amazon SES" />
|
||||
|
||||
{/* CTA */}
|
||||
<section className={'relative overflow-hidden border-t border-neutral-900 bg-neutral-900 text-white'}>
|
||||
<div className={'mx-auto max-w-[88rem] px-6 py-24 sm:px-10 sm:py-32'}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar} from '../../components';
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar, SwitchOffer} from '../../components';
|
||||
import {motion} from 'framer-motion';
|
||||
import {DASHBOARD_URI, WIKI_URI} from '../../lib/constants';
|
||||
import React from 'react';
|
||||
@@ -169,6 +169,8 @@ export default function BentoComparison() {
|
||||
{/* FAQ */}
|
||||
<FAQSection faqs={faqs} schemaId="faq-schema-bento" />
|
||||
|
||||
<SwitchOffer competitorName="Bento" />
|
||||
|
||||
{/* CTA */}
|
||||
<section className={'relative overflow-hidden border-t border-neutral-900 bg-neutral-900 text-white'}>
|
||||
<div className={'mx-auto max-w-[88rem] px-6 py-24 sm:px-10 sm:py-32'}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar} from '../../components';
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar, SwitchOffer} from '../../components';
|
||||
import {motion} from 'framer-motion';
|
||||
import {DASHBOARD_URI, WIKI_URI} from '../../lib/constants';
|
||||
import React from 'react';
|
||||
@@ -193,6 +193,8 @@ export default function BrevoComparison() {
|
||||
{/* FAQ */}
|
||||
<FAQSection faqs={faqs} schemaId="faq-schema-brevo" />
|
||||
|
||||
<SwitchOffer competitorName="Brevo" />
|
||||
|
||||
{/* CTA */}
|
||||
<section className={'relative overflow-hidden border-t border-neutral-900 bg-neutral-900 text-white'}>
|
||||
<div className={'mx-auto max-w-[88rem] px-6 py-24 sm:px-10 sm:py-32'}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar} from '../../components';
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar, SwitchOffer} from '../../components';
|
||||
import {motion} from 'framer-motion';
|
||||
import {DASHBOARD_URI, WIKI_URI} from '../../lib/constants';
|
||||
import React from 'react';
|
||||
@@ -186,6 +186,8 @@ export default function ButtondownComparison() {
|
||||
{/* FAQ */}
|
||||
<FAQSection faqs={faqs} schemaId="faq-schema-buttondown" />
|
||||
|
||||
<SwitchOffer competitorName="Buttondown" />
|
||||
|
||||
{/* CTA */}
|
||||
<section className={'relative overflow-hidden border-t border-neutral-900 bg-neutral-900 text-white'}>
|
||||
<div className={'mx-auto max-w-[88rem] px-6 py-24 sm:px-10 sm:py-32'}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar} from '../../components';
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar, SwitchOffer} from '../../components';
|
||||
import {motion} from 'framer-motion';
|
||||
import {DASHBOARD_URI, WIKI_URI} from '../../lib/constants';
|
||||
import React from 'react';
|
||||
@@ -193,6 +193,8 @@ export default function ConvertkitComparison() {
|
||||
{/* FAQ */}
|
||||
<FAQSection faqs={faqs} schemaId="faq-schema-convertkit" />
|
||||
|
||||
<SwitchOffer competitorName="ConvertKit" />
|
||||
|
||||
{/* CTA */}
|
||||
<section className={'relative overflow-hidden border-t border-neutral-900 bg-neutral-900 text-white'}>
|
||||
<div className={'mx-auto max-w-[88rem] px-6 py-24 sm:px-10 sm:py-32'}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar} from '../../components';
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar, SwitchOffer} from '../../components';
|
||||
import {motion} from 'framer-motion';
|
||||
import {DASHBOARD_URI, WIKI_URI} from '../../lib/constants';
|
||||
import React from 'react';
|
||||
@@ -189,6 +189,8 @@ export default function CustomerioComparison() {
|
||||
{/* FAQ */}
|
||||
<FAQSection faqs={faqs} schemaId="faq-schema-customerio" />
|
||||
|
||||
<SwitchOffer competitorName="Customer.io" />
|
||||
|
||||
{/* CTA */}
|
||||
<section className={'relative overflow-hidden border-t border-neutral-900 bg-neutral-900 text-white'}>
|
||||
<div className={'mx-auto max-w-[88rem] px-6 py-24 sm:px-10 sm:py-32'}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar} from '../../components';
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar, SwitchOffer} from '../../components';
|
||||
import {motion} from 'framer-motion';
|
||||
import {DASHBOARD_URI, WIKI_URI} from '../../lib/constants';
|
||||
import React from 'react';
|
||||
@@ -170,6 +170,8 @@ export default function KlaviyoComparison() {
|
||||
{/* FAQ */}
|
||||
<FAQSection faqs={faqs} schemaId="faq-schema-klaviyo" />
|
||||
|
||||
<SwitchOffer competitorName="Klaviyo" />
|
||||
|
||||
{/* CTA */}
|
||||
<section className={'relative overflow-hidden border-t border-neutral-900 bg-neutral-900 text-white'}>
|
||||
<div className={'mx-auto max-w-[88rem] px-6 py-24 sm:px-10 sm:py-32'}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar} from '../../components';
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar, SwitchOffer} from '../../components';
|
||||
import {motion} from 'framer-motion';
|
||||
import {DASHBOARD_URI, WIKI_URI} from '../../lib/constants';
|
||||
import React from 'react';
|
||||
@@ -193,6 +193,8 @@ export default function LoopsComparison() {
|
||||
{/* FAQ */}
|
||||
<FAQSection faqs={faqs} schemaId="faq-schema-loops" />
|
||||
|
||||
<SwitchOffer competitorName="Loops" />
|
||||
|
||||
{/* CTA */}
|
||||
<section className={'relative overflow-hidden border-t border-neutral-900 bg-neutral-900 text-white'}>
|
||||
<div className={'mx-auto max-w-[88rem] px-6 py-24 sm:px-10 sm:py-32'}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar} from '../../components';
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar, SwitchOffer} from '../../components';
|
||||
import {motion} from 'framer-motion';
|
||||
import {DASHBOARD_URI, WIKI_URI} from '../../lib/constants';
|
||||
import React from 'react';
|
||||
@@ -195,6 +195,8 @@ export default function MailchimpComparison() {
|
||||
{/* FAQ */}
|
||||
<FAQSection faqs={faqs} schemaId="faq-schema-mailchimp" />
|
||||
|
||||
<SwitchOffer competitorName="Mailchimp" />
|
||||
|
||||
{/* CTA */}
|
||||
<section className={'relative overflow-hidden border-t border-neutral-900 bg-neutral-900 text-white'}>
|
||||
<div className={'mx-auto max-w-[88rem] px-6 py-24 sm:px-10 sm:py-32'}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar} from '../../components';
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar, SwitchOffer} from '../../components';
|
||||
import {motion} from 'framer-motion';
|
||||
import {DASHBOARD_URI, WIKI_URI} from '../../lib/constants';
|
||||
import React from 'react';
|
||||
@@ -170,6 +170,8 @@ export default function MailerliteComparison() {
|
||||
{/* FAQ */}
|
||||
<FAQSection faqs={faqs} schemaId="faq-schema-mailerlite" />
|
||||
|
||||
<SwitchOffer competitorName="MailerLite" />
|
||||
|
||||
{/* CTA */}
|
||||
<section className={'relative overflow-hidden border-t border-neutral-900 bg-neutral-900 text-white'}>
|
||||
<div className={'mx-auto max-w-[88rem] px-6 py-24 sm:px-10 sm:py-32'}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar} from '../../components';
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar, SwitchOffer} from '../../components';
|
||||
import {motion} from 'framer-motion';
|
||||
import {DASHBOARD_URI, WIKI_URI} from '../../lib/constants';
|
||||
import React from 'react';
|
||||
@@ -189,6 +189,8 @@ export default function MailgunComparison() {
|
||||
{/* FAQ */}
|
||||
<FAQSection faqs={faqs} schemaId="faq-schema-mailgun" />
|
||||
|
||||
<SwitchOffer competitorName="Mailgun" />
|
||||
|
||||
{/* CTA */}
|
||||
<section className={'relative overflow-hidden border-t border-neutral-900 bg-neutral-900 text-white'}>
|
||||
<div className={'mx-auto max-w-[88rem] px-6 py-24 sm:px-10 sm:py-32'}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar} from '../../components';
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar, SwitchOffer} from '../../components';
|
||||
import {motion} from 'framer-motion';
|
||||
import {DASHBOARD_URI, WIKI_URI} from '../../lib/constants';
|
||||
import React from 'react';
|
||||
@@ -186,6 +186,8 @@ export default function MailjetComparison() {
|
||||
{/* FAQ */}
|
||||
<FAQSection faqs={faqs} schemaId="faq-schema-mailjet" />
|
||||
|
||||
<SwitchOffer competitorName="Mailjet" />
|
||||
|
||||
{/* CTA */}
|
||||
<section className={'relative overflow-hidden border-t border-neutral-900 bg-neutral-900 text-white'}>
|
||||
<div className={'mx-auto max-w-[88rem] px-6 py-24 sm:px-10 sm:py-32'}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar} from '../../components';
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar, SwitchOffer} from '../../components';
|
||||
import {motion} from 'framer-motion';
|
||||
import {DASHBOARD_URI, WIKI_URI} from '../../lib/constants';
|
||||
import React from 'react';
|
||||
@@ -190,6 +190,8 @@ export default function PostmarkComparison() {
|
||||
{/* FAQ */}
|
||||
<FAQSection faqs={faqs} schemaId="faq-schema-postmark" />
|
||||
|
||||
<SwitchOffer competitorName="Postmark" />
|
||||
|
||||
{/* CTA */}
|
||||
<section className={'relative overflow-hidden border-t border-neutral-900 bg-neutral-900 text-white'}>
|
||||
<div className={'mx-auto max-w-[88rem] px-6 py-24 sm:px-10 sm:py-32'}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar} from '../../components';
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar, SwitchOffer} from '../../components';
|
||||
import {motion} from 'framer-motion';
|
||||
import {DASHBOARD_URI, WIKI_URI} from '../../lib/constants';
|
||||
import React from 'react';
|
||||
@@ -189,6 +189,8 @@ export default function ResendComparison() {
|
||||
{/* FAQ */}
|
||||
<FAQSection faqs={faqs} schemaId="faq-schema-resend" />
|
||||
|
||||
<SwitchOffer competitorName="Resend" />
|
||||
|
||||
{/* CTA */}
|
||||
<section className={'relative overflow-hidden border-t border-neutral-900 bg-neutral-900 text-white'}>
|
||||
<div className={'mx-auto max-w-[88rem] px-6 py-24 sm:px-10 sm:py-32'}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar} from '../../components';
|
||||
import {ComparisonTable, FAQSection, Footer, Navbar, SwitchOffer} from '../../components';
|
||||
import {motion} from 'framer-motion';
|
||||
import {DASHBOARD_URI, WIKI_URI} from '../../lib/constants';
|
||||
import React from 'react';
|
||||
@@ -193,6 +193,8 @@ export default function SendGridComparison() {
|
||||
{/* FAQ */}
|
||||
<FAQSection faqs={faqs} schemaId="faq-schema-sendgrid" />
|
||||
|
||||
<SwitchOffer competitorName="SendGrid" />
|
||||
|
||||
{/* CTA */}
|
||||
<section className={'relative overflow-hidden border-t border-neutral-900 bg-neutral-900 text-white'}>
|
||||
<div className={'mx-auto max-w-[88rem] px-6 py-24 sm:px-10 sm:py-32'}>
|
||||
|
||||
Reference in New Issue
Block a user