From 6bfdadf1f6b7db1d750c8673d657536a087dece9 Mon Sep 17 00:00:00 2001 From: Jeremy Sfez Date: Thu, 23 Nov 2023 12:27:34 +0100 Subject: [PATCH] feat: add past due to payment banner --- .../20231122143018_add-purchase-status.js | 6 +- apps/backend/db/structure.sql | 12 +- .../src/synchronize/github/updatePurchase.ts | 3 +- .../frontend/src/containers/PaymentBanner.tsx | 133 ++++++++---------- 4 files changed, 67 insertions(+), 87 deletions(-) diff --git a/apps/backend/db/migrations/20231122143018_add-purchase-status.js b/apps/backend/db/migrations/20231122143018_add-purchase-status.js index 9528eb34d..b486fdaf1 100644 --- a/apps/backend/db/migrations/20231122143018_add-purchase-status.js +++ b/apps/backend/db/migrations/20231122143018_add-purchase-status.js @@ -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"); }); @@ -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(); }); }; @@ -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"); }); }; diff --git a/apps/backend/db/structure.sql b/apps/backend/db/structure.sql index c8e6d0ba5..8b858b9ba 100644 --- a/apps/backend/db/structure.sql +++ b/apps/backend/db/structure.sql @@ -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]))) ); @@ -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, @@ -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 -- diff --git a/apps/backend/src/synchronize/github/updatePurchase.ts b/apps/backend/src/synchronize/github/updatePurchase.ts index c79a1fce1..ea686a5b2 100644 --- a/apps/backend/src/synchronize/github/updatePurchase.ts +++ b/apps/backend/src/synchronize/github/updatePurchase.ts @@ -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({ diff --git a/apps/frontend/src/containers/PaymentBanner.tsx b/apps/frontend/src/containers/PaymentBanner.tsx index d73a418e9..5b2966778 100644 --- a/apps/frontend/src/containers/PaymentBanner.tsx +++ b/apps/frontend/src/containers/PaymentBanner.tsx @@ -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 @@ -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; @@ -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 = { @@ -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, @@ -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);