Skip to content

Commit

Permalink
feat: add past due to payment banner
Browse files Browse the repository at this point in the history
  • Loading branch information
jsfez committed Nov 23, 2023
1 parent 833cdc1 commit 6bfdadf
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* @param {import('knex').Knex} knex
*/
export const up = async (knex) => {
await knex.schema.alterTable("purchases", async (table) => {
await knex.schema.alterTable("purchases", (table) => {
table.string("status");
});

Expand All @@ -15,7 +15,7 @@ export const up = async (knex) => {
END`),
});

await knex.schema.alterTable("purchases", async (table) => {
await knex.schema.alterTable("purchases", (table) => {
table.string("status").notNullable().alter();
});
};
Expand All @@ -24,7 +24,7 @@ export const up = async (knex) => {
* @param {import('knex').Knex} knex
*/
export const down = async (knex) => {
await knex.schema.alterTable("purchases", async (table) => {
await knex.schema.alterTable("purchases", (table) => {
table.dropColumn("status");
});
};
12 changes: 2 additions & 10 deletions apps/backend/db/structure.sql
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ CREATE TABLE public.files (
key character varying(255) NOT NULL,
width integer,
height integer,
type text,
type text NOT NULL,
CONSTRAINT files_type_check CHECK ((type = ANY (ARRAY['screenshot'::text, 'screenshotDiff'::text, 'playwrightTrace'::text])))
);

Expand Down Expand Up @@ -403,7 +403,7 @@ ALTER SEQUENCE public.github_installations_id_seq OWNED BY public.github_install
--

CREATE TABLE public.github_pull_requests (
id integer NOT NULL,
id bigint NOT NULL,
"createdAt" timestamp with time zone NOT NULL,
"updatedAt" timestamp with time zone NOT NULL,
"commentDeleted" boolean DEFAULT false NOT NULL,
Expand Down Expand Up @@ -1609,14 +1609,6 @@ ALTER TABLE ONLY public.files
ADD CONSTRAINT files_pkey PRIMARY KEY (id);


--
-- Name: files files_type_not_null_constraint; Type: CHECK CONSTRAINT; Schema: public; Owner: postgres
--

ALTER TABLE public.files
ADD CONSTRAINT files_type_not_null_constraint CHECK ((type IS NOT NULL)) NOT VALID;


--
-- Name: github_accounts github_accounts_githubid_unique; Type: CONSTRAINT; Schema: public; Owner: postgres
--
Expand Down
3 changes: 1 addition & 2 deletions apps/backend/src/synchronize/github/updatePurchase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ export const updatePurchase = async (
Purchase.query(trx)
.patch({
endDate: effectiveDate,
status:
effectiveDate > new Date().toISOString() ? "cancelled" : "active",
status: new Date(effectiveDate) > new Date() ? "cancelled" : "active",
})
.findById(activePurchase.id),
Purchase.query(trx).insert({
Expand Down
133 changes: 61 additions & 72 deletions apps/frontend/src/containers/PaymentBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ import { Button } from "@/ui/Button";
import { Container } from "@/ui/Container";
import { StripePortalLink } from "@/ui/StripeLink";

const now = new Date();
const FREE_PLAN_EXPIRATION_DATE = new Date("2023-06-01");

export const PaymentBannerFragment = graphql(`
fragment PaymentBanner_Account on Account {
id
Expand Down Expand Up @@ -93,14 +90,14 @@ const BannerCta = ({
}
};

const getBannerProps = ({
const getTeamBannerProps = ({
purchaseStatus,
trialDaysRemaining,
hasGithubPurchase,
missingPaymentMethod,
pendingCancelAt,
}: {
purchaseStatus: PurchaseStatus | null;
purchaseStatus: PurchaseStatus;
trialDaysRemaining: number | null;
hasGithubPurchase: boolean;
missingPaymentMethod: boolean;
Expand All @@ -111,66 +108,63 @@ const getBannerProps = ({
bannerColor?: BannerProps["color"];
action: SubmitAction;
} => {
if (
(purchaseStatus === PurchaseStatus.Active ||
purchaseStatus === PurchaseStatus.Trialing ||
purchaseStatus === PurchaseStatus.Unpaid) &&
missingPaymentMethod
) {
const trialMessage =
trialDaysRemaining !== null
? `Your trial ends in ${trialDaysRemaining} days. `
: "";

return {
message: `${trialMessage}Add a payment method to retain access to team features.`,
buttonLabel: "Add payment method",
bannerColor:
!trialDaysRemaining || trialDaysRemaining < 5 ? "warning" : "neutral",
action: "stripePortalSession",
};
}

if (
purchaseStatus === PurchaseStatus.Trialing ||
purchaseStatus === PurchaseStatus.Active
) {
const subscriptionType =
purchaseStatus === PurchaseStatus.Trialing ? "trial" : "subscription";
const action = "stripePortalSession";

if (!pendingCancelAt) {
return { action, message: "" };
switch (purchaseStatus) {
case PurchaseStatus.PastDue:
return {
bannerColor: "warning",
message:
"Your subscription is past due. Please update your payment info.",
buttonLabel: "Manage subscription",
action: hasGithubPurchase ? "settings" : "stripeCheckoutSession",
};

case PurchaseStatus.Canceled:
case PurchaseStatus.Missing:
return {
bannerColor: "danger",
message: "Upgrade to Pro plan to use team features.",
...(hasGithubPurchase
? { action: "settings", buttonLabel: "Manage subscription" }
: { action: "stripeCheckoutSession", buttonLabel: "Upgrade" }),
};

case PurchaseStatus.Active:
case PurchaseStatus.Trialing: {
if (missingPaymentMethod) {
const remainingDayMessage = `Your trial ends in ${trialDaysRemaining} days. `;
return {
message: `${
trialDaysRemaining ? remainingDayMessage : ""
}Add a payment method to retain access to team features.`,
buttonLabel: "Add payment method",
bannerColor:
!trialDaysRemaining || trialDaysRemaining < 5
? "warning"
: "neutral",
action: "stripePortalSession",
};
}

if (pendingCancelAt) {
const formatDate = (date: string) => moment(date).format("LL");
const subscriptionTypeLabel =
purchaseStatus === "trialing" ? "trial" : "subscription";
return {
action: "stripePortalSession",
buttonLabel: `Reactivate ${subscriptionTypeLabel}`,
message: `Your ${subscriptionTypeLabel} has been canceled. You can still use team features until the trial ends on ${formatDate(
pendingCancelAt,
)}.`,
};
}

// Trial is active
return { action: "stripePortalSession", message: "" };
}

return {
action,
buttonLabel: `Reactivate ${subscriptionType}`,
message: `Your ${subscriptionType} has been canceled. You can still use team features until the trial ends on ${moment(
pendingCancelAt,
).format("LL")}.`,
};
}

const action = hasGithubPurchase ? "settings" : "stripeCheckoutSession";
const buttonLabel = hasGithubPurchase ? "Manage subscription" : "Upgrade";

if (now < FREE_PLAN_EXPIRATION_DATE) {
return {
action,
buttonLabel,
bannerColor: "neutral",
message:
"Starting June 1st, 2023, a Pro plan will be required to use team features.",
};
default:
return { action: "stripePortalSession", message: "" };
}

return {
action,
buttonLabel,
bannerColor: "danger",
message: "Upgrade to Pro plan to use team features.",
};
};

export type PaymentBannerProps = {
Expand All @@ -181,10 +175,6 @@ export const PaymentBanner = memo((props: PaymentBannerProps) => {
const account = useFragment(PaymentBannerFragment, props.account);
const { data: { me } = {} } = useQuery(PaymentBannerQuery);

if (!me || account.__typename === "User") {
return null;
}

const {
purchase,
permissions,
Expand All @@ -193,15 +183,14 @@ export const PaymentBanner = memo((props: PaymentBannerProps) => {
pendingCancelAt,
} = account;

const missingPaymentMethod = Boolean(
purchase && !purchase.paymentMethodFilled,
);
// no banner for user account
if (!me || !purchaseStatus) return null;

const { message, buttonLabel, bannerColor, action } = getBannerProps({
purchaseStatus: purchaseStatus ?? null,
const { message, buttonLabel, bannerColor, action } = getTeamBannerProps({
purchaseStatus,
trialDaysRemaining: purchase?.trialDaysRemaining ?? null,
hasGithubPurchase: Boolean(purchase && purchase.source === "github"),
missingPaymentMethod,
missingPaymentMethod: Boolean(purchase && !purchase.paymentMethodFilled),
pendingCancelAt: pendingCancelAt,
});
const userIsOwner = permissions.includes(Permission.Write);
Expand Down

0 comments on commit 6bfdadf

Please sign in to comment.