import clsx from "clsx"; import { SubscriptionFeatureDto } from "../../../../application/dtos/subscriptions/SubscriptionFeatureDto"; import { PricingModel } from "../../../../application/enums/subscriptions/PricingModel"; import { SubscriptionBillingPeriod } from "../../../../application/enums/subscriptions/SubscriptionBillingPeriod"; import { SubscriptionFeatureLimitType } from "../../../../application/enums/subscriptions/SubscriptionFeatureLimitType"; import currencies from "../../../../application/pricing/currencies"; import NumberUtils from "../../../../utils/shared/NumberUtils"; import { SubscriptionPriceDto } from "../../../../application/dtos/subscriptions/SubscriptionPriceDto"; import { SubscriptionUsageBasedPriceDto } from "../../../../application/dtos/subscriptions/SubscriptionUsageBasedPriceDto"; import { SubscriptionProductDto } from "../../../../application/dtos/subscriptions/SubscriptionProductDto"; import { useRef, useState } from "react"; import ErrorModal, { RefErrorModal } from "../../../ui/modals/ErrorModal"; import ConfirmModal, { RefConfirmModal } from "../../../ui/modals/ConfirmModal"; import { useApp } from "../../../../hooks"; import { useRouter, useSearchParams } from "next/navigation"; import Link from "next/link"; interface Props { product?: SubscriptionProductDto; title: string; description?: string; badge?: string; features: SubscriptionFeatureDto[]; billingPeriod: SubscriptionBillingPeriod; currency: string; model: PricingModel; prices: SubscriptionPriceDto[]; usageBasedPrices: SubscriptionUsageBasedPriceDto[]; className?: string; alreadyOwned?: boolean; canSubmit?: boolean; isUpgrade?: boolean; isDowngrade?: boolean; } export default function Plan({ product, title, description, badge, features, billingPeriod, model, currency, prices, usageBasedPrices, className, alreadyOwned, canSubmit, isUpgrade, isDowngrade, }: Props) { const { t } = useApp(); const [searchParams] = useSearchParams(); //const submit = useSubmit(); const navigation = useRouter(); const isSubscribing = false; //navigation.state === "submitting" && navigation.formData.get("action") === "subscribe"; const isLoading = false; // isSubscribing && navigation.formData.get("product-id") === product?.id; const [quantity, setQuantity] = useState(1); // const [usageBasedTiers, setUsageBasedTiers] = useState<{ from: number; to: number | undefined; prices: UsageBasedPriceDto[] }[]>([]); // useEffect(() => { // const tiers: { from: number; to: number | undefined; prices: UsageBasedPriceDto[] }[] = []; // usageBasedPrices.forEach((price) => { // const tier = tiers.find((t) => t.from === price.from && t.to === price.to); // if (!tier) { // tiers.push({ from: price.from, to: price.to, prices: [price] }); // } else { // tier.prices.push(price); // } // }); // setUsageBasedTiers(tiers.sort((a, b) => a.from - b.from)); // }, [usageBasedPrices]); function getCurrencySymbol() { return currencies.find((f) => f.value === currency)?.symbol; } function getCurrency() { return currencies.find((f) => f.value === currency)?.value; } function getFlatPrice(): SubscriptionPriceDto | undefined { if (model !== PricingModel.ONCE) { return prices.find((f) => f.currency === currency && f.billingPeriod === billingPeriod); } return prices.find((f) => f.currency === currency && f.billingPeriod === SubscriptionBillingPeriod.ONCE); } function getFormattedPrice() { const price = getFlatPrice(); if (!price) { return "?"; } return NumberUtils.numberFormat(Number(price.price)); } const errorModal = useRef(null); const confirmModal = useRef(null); function onClick() { const form = new FormData(); form.set("action", "subscribe"); form.set("product-id", product?.id?.toString() ?? ""); form.set("billing-period", billingPeriod.toString()); form.set("currency", currency); form.set("quantity", quantity.toString()); form.set("coupon", searchParams["coupon"]?.toString() ?? ""); if (isUpgrade) { form.set("is-upgrade", "true"); } else if (isDowngrade) { form.set("is-downgrade", "true"); } /* submit(form, { method: "post", }); */ // confirmModal.current?.setValue(product); // confirmModal.current?.show( // "SaasRock v1.0 is not ready", // "View pre-launch prices", // "Back", // "You're early! I'm still working on SaasRock pre-v1.0, checkout the roadmap at /docs/roadmap for more information or /newsletter to get notified when it's ready." // ); // submit(form, { // method: "post", // }); } function confirmed(product: SubscriptionProductDto | undefined) { if (!product) { return; } if (product?.title.includes("Core") || product?.title.includes("Enterprise")) { window.location.href = "https://alexandromg.gumroad.com/l/SaasRock"; } else { window.location.href = "https://alexandromg.gumroad.com/l/SaasRockDevelopment"; } } function isDisabled() { if (model === PricingModel.ONCE) { return isSubscribing || !product?.stripeId; } return alreadyOwned || isSubscribing || !product?.stripeId; } return ( <>
{badge && (
{t(badge)}
)}
{/* Title and Description */}

{t(title)}

{description &&

{t(description)}

}
{/* Price */} {model !== PricingModel.USAGE_BASED && (
{getCurrencySymbol()} {getFormattedPrice()}{" "} {getCurrency()} {model === PricingModel.PER_SEAT && /dev} {(() => { if (getFlatPrice()?.billingPeriod === SubscriptionBillingPeriod.MONTHLY) { return /{t("pricing.MONTHLYShort")}; } else if (getFlatPrice()?.billingPeriod === SubscriptionBillingPeriod.YEARLY) { return /{t("pricing.YEARLYShort")}; } // else if (getFlatPrice()?.billingPeriod === SubscriptionBillingPeriod.ONCE) { // return ({t("pricing.once")}); // } })()} {/*
Cancel anytime
*/}
)} {usageBasedPrices .sort((a, b) => (a.unit > b.unit ? 1 : -1)) .filter((f) => f.currency === currency) .map((usageBasedPrice, idx) => { return (
+ {t(usageBasedPrice.unitTitlePlural)}
{usageBasedPrice.tiersMode === "graduated" ? ( <> ) : ( )} {usageBasedPrice.tiers.map((tier, idx) => { return ( {usageBasedPrice.tiersMode === "graduated" && ( )} ); })}
{t(usageBasedPrice.unitTitlePlural)} {t("pricing.usageBased.units")} /m {t("pricing.usageBased.units")} /month {t("pricing.usageBased.perUnit")} {t("pricing.usageBased.flatFee")}
{idx === 0 ? t("pricing.usageBased.first") : t("pricing.usageBased.next")}
{tier.from} - {tier.to ? tier.to : "∞"} {tier.perUnitPrice ? ( <> {getCurrencySymbol()} {tier.perUnitPrice ?? "-"} {/* {getCurrency()} */} ) : ( "-" )} {tier.flatFeePrice ? ( <> {getCurrencySymbol() ?? ""} {tier.flatFeePrice ?? "-"} ) : ( "-" )}
); })} {/*
*/} {/* Features */}
    {features .sort((a, b) => (a.order > b.order ? 1 : -1)) .map((feature: any, idxFeature) => { return (
  • {feature.type !== SubscriptionFeatureLimitType.NOT_INCLUDED ? ( ) : ( )} {feature.href?.startsWith("http") ? ( {t(feature.title, [feature.value])} ) : feature.href?.startsWith("/") ? ( {t(feature.title, [feature.value])} ) : ( {t(feature.title, [feature.value])} )}
  • ); })}
{canSubmit && (
{(!alreadyOwned || model === PricingModel.ONCE) && getFlatPrice()?.price !== 0 && (model === PricingModel.PER_SEAT || model === PricingModel.ONCE) ? (
setQuantity(Number(e.currentTarget.value))} disabled={isDisabled()} className={clsx( "relative block w-full appearance-none rounded-md rounded-b-md border border-gray-300 px-3 py-2 text-center text-gray-800 placeholder-gray-500 focus:z-10 focus:border-theme-500 focus:outline-none focus:ring-theme-500 dark:border-gray-700 dark:bg-gray-900 dark:text-slate-200 sm:text-sm", isDisabled() && "cursor-not-allowed" )} />
) : ( )}
)}
); } interface SubscribeOrBuyButtonProps { isPreview?: boolean; model: PricingModel; badge?: string; disabled: boolean; loading: boolean; onClick: () => void; alreadyOwned?: boolean; price?: SubscriptionPriceDto; isUpgrade?: boolean; isDowngrade?: boolean; } function SubscribeOrBuyButton({ isPreview, model, price, badge, disabled, loading, onClick, alreadyOwned, isUpgrade, isDowngrade }: SubscribeOrBuyButtonProps) { const { t } = useApp(); return ( ); }