confirm plan switch with a modal

This commit is contained in:
m5r 2021-10-25 00:32:33 +02:00
parent 24ce9d4a62
commit 37d9bd37f4
5 changed files with 131 additions and 62 deletions

View File

@ -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);
} }

View File

@ -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)}
/>
</>
); );
} }

View 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&#39;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&#39;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;

View File

@ -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 }) => {

View File

@ -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,
}); });
} }