Skip to content

Commit 3c4692a

Browse files
authored
Merge pull request #1657 from Shelf-nu/1652-feature-subscription-management-ui
feature: subscription management UI
2 parents e4b610f + 35a6ffa commit 3c4692a

File tree

19 files changed

+581
-282
lines changed

19 files changed

+581
-282
lines changed

app/components/subscription/current-plan-details.tsx

Lines changed: 0 additions & 52 deletions
This file was deleted.

app/components/subscription/customer-portal-form.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,22 @@ import { Button } from "../shared/button";
44

55
export const CustomerPortalForm = ({
66
buttonText = "Go to Customer Portal",
7+
buttonProps,
8+
className,
79
}: {
810
buttonText?: string;
11+
buttonProps?: React.ComponentProps<typeof Button>;
12+
className?: string;
913
}) => {
1014
const customerPortalFetcher = useFetcher();
1115
const isProcessing = isFormProcessing(customerPortalFetcher.state);
1216
return (
1317
<customerPortalFetcher.Form
1418
method="post"
1519
action="/account-details/subscription/customer-portal"
20+
className={className}
1621
>
17-
<Button disabled={isProcessing}>
22+
<Button disabled={isProcessing} {...buttonProps}>
1823
{isProcessing ? "Redirecting to Customer Portal..." : buttonText}
1924
</Button>
2025
</customerPortalFetcher.Form>

app/components/subscription/helpers.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
export const FREE_PLAN = {
22
id: "free",
3+
metadata: {
4+
show_on_table: true,
5+
},
36
product: {
47
name: "Free",
58
metadata: {

app/components/subscription/price-box.tsx

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type Stripe from "stripe";
21
import { tw } from "~/utils/tw";
32
import type { Price } from "./prices";
43
import {
@@ -15,19 +14,7 @@ import {
1514
TooltipTrigger,
1615
} from "../shared/tooltip";
1716

18-
export const PriceBox = ({
19-
activePlan,
20-
subscription,
21-
price,
22-
isTrialSubscription,
23-
customPlanName,
24-
}: {
25-
activePlan: Stripe.Plan | undefined;
26-
subscription: Stripe.Subscription | null;
27-
price: Price;
28-
isTrialSubscription: boolean;
29-
customPlanName?: string | React.ReactNode;
30-
}) => {
17+
export const PriceBox = ({ price }: { price: Price }) => {
3118
const amount =
3219
price.unit_amount != null
3320
? price?.recurring?.interval === "year"
@@ -39,12 +26,7 @@ export const PriceBox = ({
3926

4027
return (
4128
<div
42-
className={tw(
43-
"price-box mb-8 rounded-2xl border p-8",
44-
activePlan?.id === price.id || (!subscription && price.id === "free")
45-
? "border-primary-500 bg-primary-50"
46-
: "bg-white"
47-
)}
29+
className={tw("price-box mb-8 rounded-2xl border bg-white p-8")}
4830
key={price.id}
4931
>
5032
<div className="text-center">
@@ -55,14 +37,8 @@ export const PriceBox = ({
5537
</div>
5638
<div className="mb-3 flex items-center justify-center gap-2">
5739
<h2 className=" text-xl font-semibold text-primary-700">
58-
{customPlanName || price.product.name}
40+
{price.product.name}
5941
</h2>
60-
{activePlan?.id === price.id ||
61-
(!subscription && price.id === "free") ? (
62-
<div className="rounded-2xl bg-primary-50 px-2 py-0.5 text-[12px] font-medium text-primary-700 mix-blend-multiply">
63-
Current {isTrialSubscription ? "(Free Trial)" : ""}
64-
</div>
65-
) : null}
6642
</div>
6743
{amount != null ? (
6844
<div className="mb-3 ">

app/components/subscription/price-cta.tsx

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,18 @@
11
import { useLoaderData } from "@remix-run/react";
22
import { config } from "~/config/shelf.config";
33
import type { loader } from "~/routes/_layout+/account-details.subscription";
4-
import { CustomerPortalForm } from "./customer-portal-form";
54
import type { Price } from "./prices";
65
import { Form } from "../custom-form";
76
import { Button } from "../shared/button";
87

9-
export const PriceCta = ({
10-
price,
11-
subscription,
12-
}: {
13-
price: Price;
14-
subscription: Object | null;
15-
}) => {
8+
export const PriceCta = ({ price }: { price: Price }) => {
169
const { usedFreeTrial } = useLoaderData<typeof loader>();
1710

1811
if (price.id === "free") return null;
1912

2013
const isTeamSubscriptionColumn =
2114
price.product.metadata.shelf_tier === "tier_2";
2215

23-
if (subscription) {
24-
return (
25-
<CustomerPortalForm
26-
buttonText={subscription ? "Manage subscription" : undefined}
27-
/>
28-
);
29-
}
30-
3116
return (
3217
<>
3318
<Form method="post">
@@ -41,7 +26,7 @@ export const PriceCta = ({
4126
Upgrade to {price.product.name}
4227
</Button>
4328

44-
{isTeamSubscriptionColumn && !subscription && !usedFreeTrial && (
29+
{isTeamSubscriptionColumn && !usedFreeTrial && (
4530
<Button
4631
variant="secondary"
4732
className="mt-2"

app/components/subscription/prices.tsx

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import { useLoaderData } from "@remix-run/react";
21
import type Stripe from "stripe";
3-
import type { loader } from "~/routes/_layout+/account-details.subscription";
42
import { FREE_PLAN } from "./helpers";
53
import { PriceBox } from "./price-box";
64
import { PriceCta } from "./price-cta";
@@ -14,18 +12,28 @@ export type PriceWithProduct = Stripe.Price & {
1412
export const Prices = ({ prices }: { prices: PriceWithProduct[] }) => (
1513
<div className="gap-8 xl:flex xl:justify-center">
1614
<Price key={FREE_PLAN.id} price={FREE_PLAN} />
17-
{prices.map((price, index) => (
18-
<Price
19-
key={price.id}
20-
price={price}
21-
previousPlanName={prices[index - 1]?.product.name}
22-
/>
23-
))}
15+
{prices
16+
.filter(
17+
(p) =>
18+
p.metadata.show_on_table &&
19+
p.metadata.show_on_table === "true" &&
20+
p.metadata.legacy !== "true"
21+
)
22+
.map((price, index) => (
23+
<Price
24+
key={price.id}
25+
price={price}
26+
previousPlanName={prices[index - 1]?.product.name}
27+
/>
28+
))}
2429
</div>
2530
);
2631

2732
export interface Price {
2833
id: string;
34+
metadata?: {
35+
show_on_table?: boolean;
36+
};
2937
product: {
3038
name: string;
3139
metadata: {
@@ -48,21 +56,15 @@ export const Price = ({
4856
price: Price;
4957
previousPlanName?: string;
5058
}) => {
51-
const { subscription, isTrialSubscription } = useLoaderData<typeof loader>();
52-
const activePlan = subscription?.items.data[0]?.plan;
5359
const isFreePlan = price.id === "free";
5460
const isTeamPlan = price.product.metadata.shelf_tier === "tier_2";
5561
const features = price.product.metadata.features?.split(",") || [];
62+
5663
return (
5764
<div className="subscription-plan mb-12 w-full xl:mb-0 xl:max-w-[410px]">
58-
<PriceBox
59-
activePlan={activePlan}
60-
subscription={subscription}
61-
price={price}
62-
isTrialSubscription={isTrialSubscription}
63-
/>
65+
<PriceBox price={price} />
6466
<div className="mb-8">
65-
<PriceCta price={price} subscription={subscription} />
67+
<PriceCta price={price} />
6668
</div>
6769
{features ? (
6870
<>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { PriceWithProduct } from "./prices";
2+
import { Prices } from "./prices";
3+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../shared/tabs";
4+
5+
export function PricingTable({
6+
prices,
7+
}: {
8+
prices: {
9+
[key: string]: PriceWithProduct[];
10+
};
11+
}) {
12+
return (
13+
<Tabs defaultValue={"year"} className="flex w-full flex-col">
14+
<TabsList className="center mx-auto mb-8">
15+
<TabsTrigger value="year">
16+
Yearly{" "}
17+
<span className="ml-2 rounded-[16px] bg-primary-50 px-2 py-1 text-xs font-medium text-primary-700">
18+
Save 54%
19+
</span>
20+
</TabsTrigger>
21+
<TabsTrigger value="month">Monthly</TabsTrigger>
22+
</TabsList>
23+
<TabsContent value="year">
24+
<Prices prices={prices["year"]} />
25+
</TabsContent>
26+
<TabsContent value="month">
27+
<Prices prices={prices["month"]} />
28+
</TabsContent>
29+
</Tabs>
30+
);
31+
}

0 commit comments

Comments
 (0)