Skip to content

Commit

Permalink
Unpaid status; better data
Browse files Browse the repository at this point in the history
  • Loading branch information
danrowden committed Aug 16, 2023
1 parent b6f0a76 commit 610df16
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 66 deletions.
26 changes: 23 additions & 3 deletions app/api/subscriptions/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getSession } from "@/lib/auth";
import { getSubscription } from '@/lib/data';
import { getPlan } from '@/lib/data';
import LemonSqueezy from '@lemonsqueezy/lemonsqueezy.js'

const ls = new LemonSqueezy(process.env.LEMONSQUEEZY_API_KEY);
Expand Down Expand Up @@ -61,7 +61,7 @@ export async function POST(request, { params }) {
data: {
attributes: {
status: 'cancelled',

// endsAt
}
}
}
Expand All @@ -79,6 +79,26 @@ export async function POST(request, { params }) {

// Return values needed to refresh state in UI
// DB will be updated in the background with webhooks
return Response.json({ error: false, subscription: subscription['data']['attributes'] }, { status: 200 })

// Filtered object
const sub = {
product_id: subscription['data']['attributes']['product_id'],
variant_id: subscription['data']['attributes']['variant_id'],
status: subscription['data']['attributes']['status'],
card_brand: subscription['data']['attributes']['card_brand'],
card_last_four: subscription['data']['attributes']['card_last_four'],
trial_ends_at: subscription['data']['attributes']['trial_ends_at'],
renews_at: subscription['data']['attributes']['renews_at'],
ends_at: subscription['data']['attributes']['ends_at'],
}

// Get new plan's data
const plan = await getPlan(sub.variant_id)
sub.plan = {
interval: plan.interval,
name: plan.variantName
}

return Response.json({ error: false, subscription: sub }, { status: 200 })

}
28 changes: 20 additions & 8 deletions components/manage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { toast } from 'sonner';
import Plans from '@/components/plan';


export function UpdateBillingLink({ subscription }) {
export function UpdateBillingLink({ subscription, type }) {

const [isMutating, setIsMutating] = useState(false)

Expand All @@ -31,12 +31,21 @@ export function UpdateBillingLink({ subscription }) {
}
}

return (
<a href="" className="mb-2 text-sm text-gray-500" onClick={openUpdateModal}>
Update your payment method
<Loader2 size={16} className={"animate-spin inline-block relative top-[-1px] ml-2 w-8" + (!isMutating ? ' invisible' : 'visible')} />
</a>
)
if (type == 'button') {
return (
<a href="" className="inline-block px-6 py-2 rounded-full bg-amber-200 text-amber-800 font-bold" onClick={openUpdateModal}>
<Loader2 className={"animate-spin inline-block relative top-[-1px] mr-2" + (!isMutating ? ' hidden' : '')} />
Update your payment method
</a>
)
} else {
return (
<a href="" className="mb-2 text-sm text-gray-500" onClick={openUpdateModal}>
Update your payment method
<Loader2 size={16} className={"animate-spin inline-block relative top-[-1px] ml-2 w-8" + (!isMutating ? ' invisible' : 'visible')} />
</a>
)
}
}

export function CancelLink({ subscription, setSubscription }) {
Expand Down Expand Up @@ -70,6 +79,7 @@ export function CancelLink({ subscription, setSubscription }) {
status: result['subscription']['status'],
expiryDate: result['subscription']['ends_at'],
})

toast.success('Your subscription has been cancelled.')

}
Expand Down Expand Up @@ -114,8 +124,10 @@ export function ResumeButton({ subscription, setSubscription }) {

setSubscription({
...subscription,
status: result['subscription']['status']
status: result['subscription']['status'],
renewalDate: result['subscription']['renews_at'],
})

toast.success('Your subscription is now active again!')

}
Expand Down
13 changes: 9 additions & 4 deletions components/plan-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export default function PlanButton({ plan, subscription, setSubscription }) {
e.preventDefault()

setIsMutating(true)
/* Create a checkout */

// Create a checkout
const res = await fetch('/api/checkouts', {
method: 'POST',
body: JSON.stringify({
Expand All @@ -42,7 +43,7 @@ For upgrades you will be charged a prorated amount.`)) {

setIsMutating(true)

/* Send request */
// Send request
const res = await fetch('/api/subscriptions/'+subscription.id, {
method: 'POST',
body: JSON.stringify({
Expand All @@ -58,8 +59,12 @@ For upgrades you will be charged a prorated amount.`)) {
// Update page's subscription state
setSubscription({
...subscription,
productId: result.subscription.product_id,
variantId: result.subscription.variant_id
productId: result['subscription']['product_id'],
variantId: result['subscription']['variant_id'],
planName: result['subscription']['plan']['name'],
planInterval: result['subscription']['plan']['interval'],
status: result['subscription']['status'],
renewalDate: result['subscription']['renews_at'],
})

toast.success('Your subscription plan has changed!')
Expand Down
121 changes: 70 additions & 51 deletions components/subscription.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,61 @@
'use client'

import { useState } from 'react';
import { Loader2 } from 'lucide-react';
import Link from 'next/link';
import Plans from '@/components/plan';
import { UpdateBillingLink, CancelLink, ResumeButton } from '@/components/manage'


// Main component
export function SubscriptionComponent({ sub, plans }) {

const [subscription, setSubscription] = useState(() => {
if (sub) {
return {
id: sub.lemonSqueezyId,
planName: sub.plan?.variantName,
planInterval: sub.plan?.interval,
productId: sub.plan?.productId,
variantId: sub.plan?.variantId,
status: sub.status,
renewalDate: sub.renewsAt,
trialEndDate: sub.trialEndsAt,
expiryDate: sub.endsAt,
}
} else {
return {}
}
})

switch(subscription.status) {

case 'active':
return <ActiveSubscription subscription={subscription} setSubscription={setSubscription} />
case 'on_trial':
return <TrialSubscription subscription={subscription} setSubscription={setSubscription} />;
case 'past_due':
return <PastDueSubscription subscription={subscription} setSubscription={setSubscription} />;
case 'cancelled':
return <CancelledSubscription subscription={subscription} setSubscription={setSubscription} />;
case 'unpaid':
return <UnpaidSubscription subscription={subscription} plans={plans} setSubscription={setSubscription} />;
case 'expired':
return <ExpiredSubscription subscription={subscription} plans={plans} setSubscription={setSubscription} />;

// TODO: Paused, Unpaused

}
}


function ActiveSubscription({ subscription, setSubscription }) {
const renewalDate = formatDate(subscription.renewalDate)
return (
<>
<p className="mb-2">
You are currently on the <b>{subscription.planName} {subscription.planInterval}ly</b> plan.
</p>

<p className="mb-2">Your next renewal will be on {renewalDate}.</p>
<p className="mb-2">Your next renewal will be on {formatDate(subscription.renewalDate)}.</p>

<hr className="my-8" />

Expand All @@ -34,14 +74,13 @@ function ActiveSubscription({ subscription, setSubscription }) {


function CancelledSubscription({ subscription, setSubscription }) {
const expiryDate = formatDate(subscription.expiryDate)
return (
<>
<p className="mb-2">
You are currently on the <b>{subscription.planName} {subscription.planInterval}ly</b> plan.
</p>

<p className="mb-8">Your subscription has been cancelled and <b>will end on {expiryDate}</b>. After this date you will no longer have access to the app.</p>
<p className="mb-8">Your subscription has been cancelled and <b>will end on {formatDate(subscription.expiryDate)}</b>. After this date you will no longer have access to the app.</p>

<p><ResumeButton subscription={subscription} setSubscription={setSubscription} /></p>
</>
Expand All @@ -50,14 +89,13 @@ function CancelledSubscription({ subscription, setSubscription }) {


function TrialSubscription({ subscription, setSubscription }) {
const trialEndDate = formatDate(subscription.trialEndDate)
return (
<>
<p className="mb-2">
You are currently on a free trial of the <b>{subscription.planName} {subscription.planInterval}ly</b> plan.
</p>

<p className="mb-6">Your trial ends on {trialEndDate}. You can cancel your subscription before this date and you won't be charged.</p>
<p className="mb-6">Your trial ends on {formatDate(subscription.trialEndDate)}. You can cancel your subscription before this date and you won't be charged.</p>

<hr className="my-8" />

Expand All @@ -75,47 +113,7 @@ function TrialSubscription({ subscription, setSubscription }) {
}


// Components for different subscription states
export function SubscriptionComponent({ sub, plans }) {
const [subscription, setSubscription] = useState(() => {
if (sub) {
return {
id: sub.lemonSqueezyId,
planName: sub.plan?.variantName,
planInterval: sub.plan?.interval,
productId: sub.plan?.productId,
variantId: sub.plan?.variantId,
status: sub.status,
renewalDate: sub.renewsAt,
trialEndDate: sub.trialEndsAt,
expiryDate: sub.endsAt,
}
} else {
return {}
}
})

switch(subscription.status) {

case 'active':
return <ActiveSubscription subscription={subscription} setSubscription={setSubscription} />
case 'on_trial':
return <TrialSubscription subscription={subscription} setSubscription={setSubscription} />;
case 'past_due':
return <PastDueSubscription subscription={subscription} setSubscription={setSubscription} />;
case 'cancelled':
return <CancelledSubscription subscription={subscription} setSubscription={setSubscription} />;
case 'expired':
return <ExpiredSubscription subscription={subscription} plans={plans} setSubscription={setSubscription} />;

// TODO: Paused, Unpaused

}
}


function PastDueSubscription({ subscription, setSubscription }) {
const renewalDate = formatDate(subscription.renewsAt)
return (
<>
<div className="my-8 p-4 text-sm text-red-800 rounded-md border border-red-200 bg-red-50">
Expand All @@ -124,10 +122,10 @@ function PastDueSubscription({ subscription, setSubscription }) {
</div>

<p className="mb-2">
You are currently on the <b>{subscription.plan.variantName} {subscription.plan.interval}ly</b> plan.
You are currently on the <b>{subscription.planName} {subscription.planInterval}ly</b> plan.
</p>

<p className="mb-2">The next payment attempt will be on {renewalDate}.</p>
<p className="mb-2">We will attempt a payment on {formatDate(subscription.renewalDate)}.</p>

<hr className="my-8" />

Expand All @@ -138,11 +136,32 @@ function PastDueSubscription({ subscription, setSubscription }) {
)
}

function UnpaidSubscription({ subscription, plans, setSubscription }) {
/*
Unpaid subscriptions have had four failed recovery payments.
If you have dunning enabled in your store settings, customers will be sent emails trying to reactivate their subscription.
If you don't have dunning enabled the subscription will remain "unpaid".
*/
return (
<>
<p className="mb-2">We haven't been able to make a successful payment and your subscription is currently marked as unpaid.</p>

<p className="mb-6">Please updated your billing information to regain access.</p>

<p><UpdateBillingLink subscription={subscription} type="button" /></p>

<hr className="my-8" />

<p><CancelLink subscription={subscription} setSubscription={setSubscription} /></p>

</>
)
}

function ExpiredSubscription({ subscription, plans, setSubscription }) {
const expiryDate = formatDate(subscription.expiryDate)
return (
<>
<p className="mb-2">Your subscription expired on {expiryDate}.</p>
<p className="mb-2">Your subscription expired on {formatDate(subscription.expiryDate)}.</p>

<p className="mb-2">Please create a new subscription to regain access.</p>

Expand Down
13 changes: 13 additions & 0 deletions lib/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ export async function getPlans() {
}


export async function getPlan(variantId) {
// Gets all active plans
return await prisma.plan.findFirst({
where: {
variantId: variantId,
NOT: {
status: 'draft'
}
}
});
}


export async function getSubscription(userId) {
// Gets the most recent subscription
return await prisma.subscription.findFirst({
Expand Down

0 comments on commit 610df16

Please sign in to comment.