confirm plan switch with a modal
This commit is contained in:
parent
24ce9d4a62
commit
37d9bd37f4
@ -93,12 +93,9 @@ export default function useSubscription({ initialData }: Params = {}) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function changePlan({ planId }: ChangePlanParams) {
|
async function changePlan({ planId }: ChangePlanParams) {
|
||||||
if (planId === -1) {
|
|
||||||
return cancelSubscription({ cancelUrl: subscription!.cancelUrl });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateSubscriptionMutation({ planId });
|
await updateSubscriptionMutation({ planId });
|
||||||
|
setIsWaitingForSubChange(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("error", error);
|
console.log("error", error);
|
||||||
}
|
}
|
||||||
|
@ -1,68 +1,87 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import * as Panelbear from "@panelbear/panelbear-js";
|
import * as Panelbear from "@panelbear/panelbear-js";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
import type { Subscription } from "db";
|
import type { Subscription } from "db";
|
||||||
import { SubscriptionStatus } from "db";
|
import { SubscriptionStatus } from "db";
|
||||||
import useSubscription from "app/core/hooks/use-subscription";
|
import useSubscription from "app/core/hooks/use-subscription";
|
||||||
|
import SwitchPlanModal from "./switch-plan-modal";
|
||||||
|
|
||||||
|
export type Plan = typeof pricing["tiers"][number];
|
||||||
|
|
||||||
export default function Plans() {
|
export default function Plans() {
|
||||||
const { hasActiveSubscription, subscription, subscribe, changePlan } = useSubscription();
|
const { hasActiveSubscription, subscription, subscribe, changePlan } = useSubscription();
|
||||||
|
const [nextPlan, setNextPlan] = useState<Plan | null>(null);
|
||||||
|
const [isSwitchPlanModalOpen, setIsSwitchPlanModalOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-6 flex flex-row flex-wrap gap-2">
|
<>
|
||||||
{pricing.tiers.map((tier) => {
|
<div className="mt-6 flex flex-row flex-wrap gap-2">
|
||||||
const isCurrentTier = subscription?.paddlePlanId === tier.planId;
|
{pricing.tiers.map((tier) => {
|
||||||
const isActiveTier = hasActiveSubscription && isCurrentTier;
|
const isCurrentTier = subscription?.paddlePlanId === tier.planId;
|
||||||
const cta = getCTA({ subscription, tier });
|
const isActiveTier = hasActiveSubscription && isCurrentTier;
|
||||||
|
const cta = getCTA({ subscription, tier });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={tier.title}
|
key={tier.title}
|
||||||
className={clsx(
|
|
||||||
"relative p-2 pt-4 bg-white border border-gray-200 rounded-xl shadow-sm flex flex-1 min-w-[250px] flex-col",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex-1 px-2">
|
|
||||||
<h3 className="text-xl font-mackinac font-semibold text-gray-900">{tier.title}</h3>
|
|
||||||
{tier.yearly ? (
|
|
||||||
<p className="absolute top-0 py-1.5 px-4 bg-primary-500 rounded-full text-xs font-semibold uppercase tracking-wide text-white transform -translate-y-1/2">
|
|
||||||
Get 2 months free!
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
<p className="mt-4 flex items-baseline text-gray-900">
|
|
||||||
<span className="text-2xl font-extrabold tracking-tight">{tier.price}€</span>
|
|
||||||
<span className="ml-1 text-lg font-semibold">{tier.frequency}</span>
|
|
||||||
</p>
|
|
||||||
{tier.yearly ? (
|
|
||||||
<p className="text-gray-500 text-sm">Billed yearly ({tier.price * 12}€)</p>
|
|
||||||
) : null}
|
|
||||||
<p className="mt-6 text-gray-500">{tier.description}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
disabled={isActiveTier}
|
|
||||||
onClick={() => {
|
|
||||||
if (hasActiveSubscription) {
|
|
||||||
changePlan({ planId: tier.planId });
|
|
||||||
Panelbear.track(`Subscribe to ${tier.title}`);
|
|
||||||
} else {
|
|
||||||
subscribe({ planId: tier.planId, coupon: "groot429" });
|
|
||||||
Panelbear.track(`Subscribe to ${tier.title}`);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={clsx(
|
className={clsx(
|
||||||
!isActiveTier
|
"relative p-2 pt-4 bg-white border border-gray-200 rounded-xl shadow-sm flex flex-1 min-w-[250px] flex-col",
|
||||||
? "bg-primary-500 text-white hover:bg-primary-600"
|
|
||||||
: "bg-primary-50 text-primary-700 cursor-not-allowed",
|
|
||||||
"mt-8 block w-full py-3 px-6 border border-transparent rounded-md text-center font-medium",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{cta}
|
<div className="flex-1 px-2">
|
||||||
</button>
|
<h3 className="text-xl font-mackinac font-semibold text-gray-900">{tier.title}</h3>
|
||||||
</div>
|
{tier.yearly ? (
|
||||||
);
|
<p className="absolute top-0 py-1.5 px-4 bg-primary-500 rounded-full text-xs font-semibold uppercase tracking-wide text-white transform -translate-y-1/2">
|
||||||
})}
|
Get 2 months free!
|
||||||
</div>
|
</p>
|
||||||
|
) : null}
|
||||||
|
<p className="mt-4 flex items-baseline text-gray-900">
|
||||||
|
<span className="text-2xl font-extrabold tracking-tight">{tier.price}€</span>
|
||||||
|
<span className="ml-1 text-lg font-semibold">{tier.frequency}</span>
|
||||||
|
</p>
|
||||||
|
{tier.yearly ? (
|
||||||
|
<p className="text-gray-500 text-sm">Billed yearly ({tier.price * 12}€)</p>
|
||||||
|
) : null}
|
||||||
|
<p className="mt-6 text-gray-500">{tier.description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
disabled={isActiveTier}
|
||||||
|
onClick={() => {
|
||||||
|
if (hasActiveSubscription) {
|
||||||
|
setNextPlan(tier);
|
||||||
|
setIsSwitchPlanModalOpen(true);
|
||||||
|
} else {
|
||||||
|
subscribe({ planId: tier.planId });
|
||||||
|
Panelbear.track(`Subscribe to ${tier.title}`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={clsx(
|
||||||
|
!isActiveTier
|
||||||
|
? "bg-primary-500 text-white hover:bg-primary-600"
|
||||||
|
: "bg-primary-50 text-primary-700 cursor-not-allowed",
|
||||||
|
"mt-8 block w-full py-3 px-6 border border-transparent rounded-md text-center font-medium",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{cta}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SwitchPlanModal
|
||||||
|
isOpen={isSwitchPlanModalOpen}
|
||||||
|
nextPlan={nextPlan}
|
||||||
|
confirm={(nextPlan: Plan) => {
|
||||||
|
changePlan({ planId: nextPlan.planId });
|
||||||
|
Panelbear.track(`Subscribe to ${nextPlan.title}`);
|
||||||
|
setIsSwitchPlanModalOpen(false);
|
||||||
|
}}
|
||||||
|
closeModal={() => setIsSwitchPlanModalOpen(false)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
52
app/settings/components/billing/switch-plan-modal.tsx
Normal file
52
app/settings/components/billing/switch-plan-modal.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import type { FunctionComponent } from "react";
|
||||||
|
import { useRef } from "react";
|
||||||
|
|
||||||
|
import Modal, { ModalTitle } from "app/core/components/modal";
|
||||||
|
import type { Plan } from "./plans";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
nextPlan: Plan | null;
|
||||||
|
confirm: (nextPlan: Plan) => void;
|
||||||
|
closeModal: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SwitchPlanModal: FunctionComponent<Props> = ({ isOpen, nextPlan, confirm, closeModal }) => {
|
||||||
|
const confirmButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal initialFocus={confirmButtonRef} isOpen={isOpen} onClose={closeModal}>
|
||||||
|
<div className="md:flex md:items-start">
|
||||||
|
<div className="mt-3 text-center md:mt-0 md:ml-4 md:text-left">
|
||||||
|
<ModalTitle>Are you sure you want to switch to {nextPlan?.title}?</ModalTitle>
|
||||||
|
<div className="mt-2 text-gray-500">
|
||||||
|
<p>
|
||||||
|
You're about to switch to the <strong>{nextPlan?.title}</strong> plan. You will be
|
||||||
|
billed immediately a prorated amount and the next billing date will be recalculated from
|
||||||
|
today.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5 md:mt-4 md:flex md:flex-row-reverse">
|
||||||
|
<button
|
||||||
|
ref={confirmButtonRef}
|
||||||
|
type="button"
|
||||||
|
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-primary-500 font-medium text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 md:mt-0 md:w-auto"
|
||||||
|
onClick={() => confirm(nextPlan!)}
|
||||||
|
>
|
||||||
|
Yes, I'm sure
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="md:mr-2 mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white font-medium text-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 md:mt-0 md:w-auto"
|
||||||
|
onClick={closeModal}
|
||||||
|
>
|
||||||
|
Nope, cancel it
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SwitchPlanModal;
|
@ -20,12 +20,6 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Billing: BlitzPage<Props> = (props) => {
|
const Billing: BlitzPage<Props> = (props) => {
|
||||||
/*
|
|
||||||
TODO: I want to be able to
|
|
||||||
- upgrade to yearly
|
|
||||||
- downgrade to monthly
|
|
||||||
*/
|
|
||||||
|
|
||||||
const { count: paymentsCount } = usePaymentsHistory();
|
const { count: paymentsCount } = usePaymentsHistory();
|
||||||
const { subscription, cancelSubscription, updatePaymentMethod } = useSubscription({
|
const { subscription, cancelSubscription, updatePaymentMethod } = useSubscription({
|
||||||
initialData: props.subscription,
|
initialData: props.subscription,
|
||||||
@ -37,8 +31,8 @@ const Billing: BlitzPage<Props> = (props) => {
|
|||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
{subscription.status === SubscriptionStatus.deleted ? (
|
{subscription.status === SubscriptionStatus.deleted ? (
|
||||||
<p>
|
<p>
|
||||||
Your {subscription.paddlePlanId} subscription is cancelled and will expire on{" "}
|
Your {plansName[subscription.paddlePlanId]?.toLowerCase()} subscription is cancelled and
|
||||||
{subscription.cancellationEffectiveDate!.toLocaleDateString()}.
|
will expire on {subscription.cancellationEffectiveDate!.toLocaleDateString()}.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@ -72,6 +66,11 @@ const Billing: BlitzPage<Props> = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const plansName: Record<number, string> = {
|
||||||
|
727544: "Yearly",
|
||||||
|
727540: "Monthly",
|
||||||
|
};
|
||||||
|
|
||||||
Billing.getLayout = (page) => <SettingsLayout>{page}</SettingsLayout>;
|
Billing.getLayout = (page) => <SettingsLayout>{page}</SettingsLayout>;
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps<Props> = async ({ req, res }) => {
|
export const getServerSideProps: GetServerSideProps<Props> = async ({ req, res }) => {
|
||||||
|
@ -93,12 +93,14 @@ export async function updateSubscriptionPlan({
|
|||||||
productId,
|
productId,
|
||||||
shouldProrate = true,
|
shouldProrate = true,
|
||||||
shouldKeepModifiers = true,
|
shouldKeepModifiers = true,
|
||||||
|
shouldMakeImmediatePayment = true,
|
||||||
}: PaddleSdkUpdateSubscriptionRequest<Metadata>) {
|
}: PaddleSdkUpdateSubscriptionRequest<Metadata>) {
|
||||||
return paddleSdk.updateSubscription({
|
return paddleSdk.updateSubscription({
|
||||||
subscriptionId,
|
subscriptionId,
|
||||||
productId,
|
productId,
|
||||||
shouldProrate,
|
shouldProrate,
|
||||||
shouldKeepModifiers,
|
shouldKeepModifiers,
|
||||||
|
shouldMakeImmediatePayment,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user