Skip to content

Commit

Permalink
Merge pull request #726 from Shelf-nu/free-trial-webhooks
Browse files Browse the repository at this point in the history
improvement: webhook automation for free trial
  • Loading branch information
DonKoko authored Feb 8, 2024
2 parents 7f43190 + 69b833f commit 8c4e649
Show file tree
Hide file tree
Showing 11 changed files with 230 additions and 60 deletions.
33 changes: 26 additions & 7 deletions app/components/subscription/current-plan-details.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { useLoaderData } from "@remix-run/react";
import { Link, useLoaderData } from "@remix-run/react";

Check warning on line 1 in app/components/subscription/current-plan-details.tsx

View workflow job for this annotation

GitHub Actions / tests / ⬣ ESLint

'Link' is defined but never used
import type { loader } from "~/routes/_layout+/settings.subscription";
import { Button } from "../shared";

export const CurrentPlanDetails = () => {
const { activeProduct, expiration, activeSubscription } =
const { activeProduct, expiration, subscription, isTrialSubscription } =
useLoaderData<typeof loader>();

return (
<div>
<p>
You’re currently using the <b>{activeProduct?.name}</b> version of
Shelf.
You’re currently using the <b>{activeProduct?.name}</b> version of Shelf{" "}
{isTrialSubscription ? " on a free trial" : ""}.
</p>
<div>
{activeSubscription?.canceled_at ? (
{subscription?.canceled_at ? (
<>
<p>
Your plan has been canceled and will be active until{" "}
Expand All @@ -22,8 +23,26 @@ export const CurrentPlanDetails = () => {
</>
) : (
<p>
Your subscription renews on <b>{expiration.date}</b> at{" "}
<b>{expiration.time}</b>
{!isTrialSubscription ? (
<>
{" "}
Your subscription renews on <b>{expiration.date}</b> at{" "}
<b>{expiration.time}</b>
</>
) : (
<>
{" "}
Your{" "}
<Button
to="https://www.shelf.nu/knowledge-base/free-trial"
target="_blank"
variant="link"
>
free trial
</Button>{" "}
expires on <b>{expiration.date}</b> at <b>{expiration.time}</b>
</>
)}
</p>
)}
</div>
Expand Down
8 changes: 4 additions & 4 deletions app/components/subscription/price-cta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@ import { Button } from "../shared";

export const PriceCta = ({
price,
activeSubscription,
subscription,
}: {
price: Price;
activeSubscription: Object | null;
subscription: Object | null;
}) => {
if (price.id === "free") return null;

if (activeSubscription) {
if (subscription) {
return (
<CustomerPortalForm
buttonText={activeSubscription ? "Manage subscription" : undefined}
buttonText={subscription ? "Manage subscription" : undefined}
/>
);
}
Expand Down
13 changes: 6 additions & 7 deletions app/components/subscription/prices.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ export const Price = ({
price: Price;
previousPlanName?: string;
}) => {
const { activeSubscription } = useLoaderData<typeof loader>();
const activePlan = activeSubscription?.items.data[0]?.plan;
const { subscription, isTrialSubscription } = useLoaderData<typeof loader>();
const activePlan = subscription?.items.data[0]?.plan;
const isFreePlan = price.id != "free";
const features = price.product.metadata.features?.split(",") || [];

Expand All @@ -71,8 +71,7 @@ export const Price = ({
<div
className={tw(
"mb-8 rounded-2xl border p-8",
activePlan?.id === price.id ||
(!activeSubscription && price.id === "free")
activePlan?.id === price.id || (!subscription && price.id === "free")
? "border-primary-500 bg-primary-50"
: "bg-white"
)}
Expand All @@ -91,9 +90,9 @@ export const Price = ({
{price.product.name}
</h2>
{activePlan?.id === price.id ||
(!activeSubscription && price.id === "free") ? (
(!subscription && price.id === "free") ? (
<div className="rounded-2xl bg-primary-50 px-2 py-0.5 text-[12px] font-medium text-primary-700 mix-blend-multiply">
Current
Current {isTrialSubscription ? "(Free Trial)" : ""}
</div>
) : null}
</div>
Expand All @@ -115,7 +114,7 @@ export const Price = ({
</div>
</div>
<div className="mb-8">
<PriceCta price={price} activeSubscription={activeSubscription} />
<PriceCta price={price} subscription={subscription} />
</div>
{features ? (
<>
Expand Down
22 changes: 22 additions & 0 deletions app/emails/stripe/trial-ends-soon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type Stripe from "stripe";
import { SERVER_URL } from "~/utils";
interface Props {
user: { firstName?: string | null; lastName?: string | null; email: string };
subscription: Stripe.Subscription;
}

export const trialEndsSoonText = ({ user, subscription }: Props) => `Howdy ${
user.firstName ? user.firstName : ""
} ${user.lastName ? user.lastName : ""},
You are reaching the end of your trial period with Shelf, which concludes on ${new Date(
(subscription.trial_end as number) * 1000 // We force this as we check it before even calling the send email function
).toLocaleDateString()}. It's been a pleasure having you explore what Shelf has to offer. To maintain uninterrupted access to our premium features, we invite you to transition to one of our paid plans. You can make this upgrade by visiting your subscription settings: ${SERVER_URL}/settings/subscription .
Should you have any inquiries or require further assistance, our support team is at your disposal. You can reach us via email at [email protected].
Thank you for considering Shelf for your needs. We look forward to continuing to support your journey.
Warm regards,
The Shelf Team
`;
4 changes: 3 additions & 1 deletion app/entry.server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ schedulerService
.init()
.then(() => {
registerBookingWorkers();
})
.finally(() => {
// eslint-disable-next-line no-console
console.log("Scheduler and workers registered");
console.log("Scheduler and workers registration completed");
})
// eslint-disable-next-line no-console
.catch((e) => console.error(e));
Expand Down
3 changes: 0 additions & 3 deletions app/modules/booking/worker.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,7 @@ import type { SchedulerData } from "./types";

/** ===== start: listens and creates chain of jobs for a given booking ===== */

let counter = 0;
export const registerBookingWorkers = () => {
console.log(`called registerBookingWorkers ${++counter} `);

/** Check-out reminder */
scheduler.work<SchedulerData>(
schedulerKeys.checkoutReminder,
Expand Down
2 changes: 1 addition & 1 deletion app/routes/_auth+/join.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export async function action({ request }: ActionFunctionArgs) {
// Handle the results of the sign up
if (signUpResult.status === "error") {
return json(
{ errors: { email: "unable-to-create-account", password: null } },
{ errors: { email: signUpResult.error, password: null } },
{ status: 500 }
);
} else if (
Expand Down
35 changes: 23 additions & 12 deletions app/routes/_layout+/settings.subscription.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { db } from "~/database";
import { getUserByID } from "~/modules/user";

import { appendToMetaTitle } from "~/utils/append-to-meta-title";
import { ShelfStackError } from "~/utils/error";
import { PermissionAction, PermissionEntity } from "~/utils/permissions";
import { requirePermision } from "~/utils/roles.server";
import type { CustomerWithSubscriptions } from "~/utils/stripe.server";
Expand All @@ -33,6 +34,7 @@ import {
getStripeCustomer,
getActiveProduct,
getCustomerActiveSubscription,
getCustomerTrialSubscription,
} from "~/utils/stripe.server";

export async function loader({ request }: LoaderFunctionArgs) {
Expand All @@ -52,19 +54,23 @@ export async function loader({ request }: LoaderFunctionArgs) {
? ((await getStripeCustomer(user.customerId)) as CustomerWithSubscriptions)
: null;

let subscription = getCustomerActiveSubscription({ customer });
/** Check if the customer has an active subscription */
const activeSubscription = getCustomerActiveSubscription({ customer });

if (!subscription) {
subscription = getCustomerTrialSubscription({ customer });
}

/* Get the prices and products from Stripe */
const prices = await getStripePricesAndProducts();

let activeProduct = null;
if (customer && activeSubscription) {
if (customer && subscription) {
/** Get the active subscription ID */

activeProduct = getActiveProduct({
prices,
priceId: activeSubscription?.items.data[0].plan.id,
priceId: subscription?.items.data[0].plan.id || null,
});
}

Expand All @@ -73,16 +79,17 @@ export async function loader({ request }: LoaderFunctionArgs) {
subTitle: "Pick an account plan that fits your workflow.",
prices,
customer,
activeSubscription,
subscription,
activeProduct,
expiration: {
date: new Date(
(activeSubscription?.current_period_end as number) * 1000
(subscription?.current_period_end as number) * 1000
).toLocaleDateString(),
time: new Date(
(activeSubscription?.current_period_end as number) * 1000
(subscription?.current_period_end as number) * 1000
).toLocaleTimeString(),
},
isTrialSubscription: !!subscription?.trial_end,
});
}

Expand All @@ -104,6 +111,10 @@ export const action = async ({ request }: ActionFunctionArgs) => {

if (!user) throw new Error("User not found");

/**
* We create the stripe customer on onboarding,
* however we keep this to double check in case something went wrong
*/
const customerId = user.customerId
? user.customerId
: await createStripeCustomer({
Expand All @@ -112,6 +123,8 @@ export const action = async ({ request }: ActionFunctionArgs) => {
userId,
});

if (!customerId) throw new ShelfStackError({ message: "Customer not found" });

const stripeRedirectUrl = await createStripeCheckoutSession({
userId,
priceId,
Expand All @@ -130,7 +143,7 @@ export const handle = {
};

export default function UserPage() {
const { title, subTitle, prices, activeSubscription } =
const { title, subTitle, prices, subscription } =
useLoaderData<typeof loader>();

return (
Expand All @@ -140,7 +153,7 @@ export default function UserPage() {
<div className="inline-flex items-center justify-center rounded-full border-[5px] border-solid border-primary-50 bg-primary-100 p-1.5 text-primary">
<InfoIcon />
</div>
{!activeSubscription ? (
{!subscription ? (
<p className="text-[14px] font-medium text-gray-700">
You’re currently using the{" "}
<span className="font-semibold">FREE</span> version of Shelf
Expand All @@ -155,13 +168,11 @@ export default function UserPage() {
<h3 className="text-text-lg font-semibold">{title}</h3>
<p className="text-sm text-gray-600">{subTitle}</p>
</div>
{activeSubscription && <CustomerPortalForm />}
{subscription && <CustomerPortalForm />}
</div>

<Tabs
defaultValue={
activeSubscription?.items.data[0]?.plan.interval || "month"
}
defaultValue={subscription?.items.data[0]?.plan.interval || "month"}
className="flex w-full flex-col"
>
<TabsList className="center mx-auto mb-8">
Expand Down
19 changes: 16 additions & 3 deletions app/routes/_welcome+/onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type { UpdateUserPayload } from "~/modules/user/types";
import { assertIsPost } from "~/utils";
import { appendToMetaTitle } from "~/utils/append-to-meta-title";
import { setCookie } from "~/utils/cookies.server";
import { createStripeCustomer } from "~/utils/stripe.server";

function createOnboardingSchema(userSignedUpWithPassword: boolean) {
return z
Expand Down Expand Up @@ -116,10 +117,22 @@ export async function action({ request }: ActionFunctionArgs) {
};

/** Update the user */
const updatedUser = await updateUser(updateUserPayload);
const { user, errors } = await updateUser(updateUserPayload);

if (updatedUser.errors) {
return json({ errors: updatedUser.errors }, { status: 400 });
if (!user && errors) {
return json({ errors }, { status: 400 });
}

if (user) {
/** We create the stripe customer when the user gets onboarded.
* This is to make sure that we have a stripe customer for the user.
* We have to do it at this point, as its the first time we have the user's first and last name
*/
await createStripeCustomer({
email: user.email,
name: `${user.firstName} ${user.lastName}`,
userId: user.id,
});
}

const organizationIdFromForm =
Expand Down
Loading

0 comments on commit 8c4e649

Please sign in to comment.