fix V2
This commit is contained in:
@@ -4,7 +4,7 @@ import React, { useEffect, useRef, useState } from 'react';
|
||||
import { X, Loader2, Pause, Download, ArrowRight } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
|
||||
export type UpgradeReason = 'limit' | 'logo' | 'shapes';
|
||||
export type UpgradeReason = 'limit' | 'logo' | 'shapes' | 'business-shapes';
|
||||
|
||||
export interface ActiveCodeSummary {
|
||||
id: string;
|
||||
@@ -15,6 +15,11 @@ export interface ActiveCodeSummary {
|
||||
interface UpgradeModalProps {
|
||||
open: boolean;
|
||||
reason: UpgradeReason;
|
||||
/** The user's current plan. Determines which plan is offered next
|
||||
* (Free -> Pro, Pro -> Business) and whether a paid CTA makes sense
|
||||
* at all (Business/Enterprise are already at or above the ceiling
|
||||
* a plain checkout button can sell). Defaults to 'FREE'. */
|
||||
plan?: string;
|
||||
/** How many dynamic codes the user already has. Only used for reason="limit". */
|
||||
currentCount?: number;
|
||||
/** The plan limit that was hit. Only used for reason="limit". */
|
||||
@@ -49,7 +54,7 @@ function ordinal(n: number): string {
|
||||
* hostage, and this is the one screen where that has to be demonstrated
|
||||
* rather than claimed.
|
||||
*/
|
||||
function getCopy(reason: UpgradeReason, currentCount?: number, limit?: number) {
|
||||
function getCopy(reason: UpgradeReason, plan: string, currentCount?: number, limit?: number) {
|
||||
if (reason === 'logo') {
|
||||
return {
|
||||
headline: 'Your logo belongs inside this code.',
|
||||
@@ -58,6 +63,7 @@ function getCopy(reason: UpgradeReason, currentCount?: number, limit?: number) {
|
||||
'Pro puts your logo in the center of every code you make, at the error-correction level that keeps it scannable.',
|
||||
cta: 'Add my logo - €9 / month',
|
||||
reassurance: 'Your current codes keep working exactly as they are.',
|
||||
ctaType: 'checkout' as const,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -69,24 +75,64 @@ function getCopy(reason: UpgradeReason, currentCount?: number, limit?: number) {
|
||||
'Pro unlocks four module shapes and your own eye styles. Colors stay free on every plan.',
|
||||
cta: 'Unlock shapes - €9 / month',
|
||||
reassurance: 'Your current codes keep working exactly as they are.',
|
||||
ctaType: 'checkout' as const,
|
||||
};
|
||||
}
|
||||
|
||||
if (reason === 'business-shapes') {
|
||||
return {
|
||||
headline: 'This shape needs Business.',
|
||||
body: 'Classy, diamond, hexagon, plus, mosaic, liquid and star modules are the shapes agencies and larger teams use to make a code feel fully custom, not just styled.',
|
||||
mechanism:
|
||||
'Business unlocks every module shape, plus white-label branding and priority support. Colors stay free on every plan.',
|
||||
cta: 'Unlock all shapes - €29 / month',
|
||||
reassurance: 'Your current codes keep working exactly as they are.',
|
||||
ctaType: 'checkout' as const,
|
||||
};
|
||||
}
|
||||
|
||||
const next = (currentCount ?? 3) + 1;
|
||||
const cap = limit ?? 3;
|
||||
|
||||
// Free -> Pro is the default upsell. Pro -> Business and Business/Enterprise
|
||||
// "already at the top" need their own copy - otherwise a Pro user who fills
|
||||
// their 50 slots gets told to buy the Pro plan they already have.
|
||||
if (plan === 'BUSINESS' || plan === 'ENTERPRISE') {
|
||||
return {
|
||||
headline: `Your ${ordinal(next)} code is finished. It just needs a slot.`,
|
||||
body: `You are using all ${cap} dynamic codes on your Business plan. This one is built and waiting - you can keep it, or free a slot from the codes you already have.`,
|
||||
mechanism: 'You are already on our highest self-serve plan. For a higher ceiling than 500 dynamic codes, contact us directly.',
|
||||
cta: 'Contact us about a higher limit',
|
||||
reassurance: `Your ${cap} active codes keep running, whichever way you decide.`,
|
||||
ctaType: 'contact' as const,
|
||||
};
|
||||
}
|
||||
|
||||
if (plan === 'PRO') {
|
||||
return {
|
||||
headline: `Your ${ordinal(next)} code is finished. It just needs a slot.`,
|
||||
body: `You are using all ${cap} dynamic codes on your Pro plan. This one is built and waiting - you can keep it, or free a slot from the codes you already have.`,
|
||||
mechanism: 'Business raises the ceiling to 500 dynamic codes and adds bulk creation and priority support.',
|
||||
cta: 'Save this code with Business - €29 / month',
|
||||
reassurance: `Your ${cap} active codes keep running, whichever way you decide.`,
|
||||
ctaType: 'checkout' as const,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
headline: `Your ${ordinal(next)} code is finished. It just needs a slot.`,
|
||||
body: `You are using all ${cap} dynamic codes on your plan. This one is built and waiting - you can keep it, or free a slot from the codes you already have.`,
|
||||
mechanism: 'Pro raises the ceiling to 50 dynamic codes and adds device and location data for every scan.',
|
||||
cta: 'Save this code with Pro - €9 / month',
|
||||
reassurance: `Your ${cap} active codes keep running, whichever way you decide.`,
|
||||
ctaType: 'checkout' as const,
|
||||
};
|
||||
}
|
||||
|
||||
export default function UpgradeModal({
|
||||
open,
|
||||
reason,
|
||||
plan = 'FREE',
|
||||
currentCount,
|
||||
limit,
|
||||
activeCodes = [],
|
||||
@@ -119,7 +165,15 @@ export default function UpgradeModal({
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const copy = getCopy(reason, currentCount, limit);
|
||||
const copy = getCopy(reason, plan, currentCount, limit);
|
||||
|
||||
// Which plan a checkout should sell depends on where the user already is,
|
||||
// not just why the modal opened. A Pro user hitting their code limit needs
|
||||
// Business, not another Pro subscription.
|
||||
const targetPlan =
|
||||
reason === 'business-shapes' || (reason === 'limit' && plan === 'PRO')
|
||||
? 'BUSINESS'
|
||||
: 'PRO';
|
||||
|
||||
const handleCheckout = async () => {
|
||||
setCheckoutLoading(true);
|
||||
@@ -135,7 +189,7 @@ export default function UpgradeModal({
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
plan: 'PRO',
|
||||
plan: targetPlan,
|
||||
billingInterval: 'month',
|
||||
returnPath: path,
|
||||
}),
|
||||
@@ -205,21 +259,30 @@ export default function UpgradeModal({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={handleCheckout}
|
||||
disabled={checkoutLoading}
|
||||
className="h-12 w-full bg-primary-600 text-base font-semibold text-white hover:bg-primary-700"
|
||||
>
|
||||
{checkoutLoading ? (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" /> Opening checkout...
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
{copy.cta} <ArrowRight className="h-4 w-4" />
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
{copy.ctaType === 'contact' ? (
|
||||
<a
|
||||
href="mailto:support@qrmaster.net?subject=Higher%20dynamic%20QR%20limit"
|
||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-lg bg-primary-600 text-base font-semibold text-white transition-colors hover:bg-primary-700"
|
||||
>
|
||||
{copy.cta} <ArrowRight className="h-4 w-4" />
|
||||
</a>
|
||||
) : (
|
||||
<Button
|
||||
onClick={handleCheckout}
|
||||
disabled={checkoutLoading}
|
||||
className="h-12 w-full bg-primary-600 text-base font-semibold text-white hover:bg-primary-700"
|
||||
>
|
||||
{checkoutLoading ? (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" /> Opening checkout...
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
{copy.cta} <ArrowRight className="h-4 w-4" />
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{reason === 'limit' && onPauseCode && activeCodes.length > 0 && !showCodeList && (
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user