Add declined payment reason and force refresh on billing page load

This commit is contained in:
Carlos Monastyrski
2025-07-30 11:58:35 -03:00
parent b9406e7f55
commit 8666f328e2
8 changed files with 51 additions and 15 deletions
+8 -1
View File
@@ -43,6 +43,12 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
}, },
schema: { schema: {
params: z.object({ organizationId: z.string().trim() }), params: z.object({ organizationId: z.string().trim() }),
querystring: z.object({
refreshCache: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
}),
response: { response: {
200: z.object({ plan: z.any() }) 200: z.object({ plan: z.any() })
} }
@@ -54,7 +60,8 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
orgId: req.params.organizationId orgId: req.params.organizationId,
refreshCache: req.query.refreshCache
}); });
return { plan }; return { plan };
} }
@@ -295,8 +295,19 @@ export const licenseServiceFactory = ({
return data; return data;
}; };
const getOrgPlan = async ({ orgId, actor, actorId, actorOrgId, actorAuthMethod, projectId }: TOrgPlanDTO) => { const getOrgPlan = async ({
orgId,
actor,
actorId,
actorOrgId,
actorAuthMethod,
projectId,
refreshCache
}: TOrgPlanDTO) => {
await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
if (refreshCache) {
await refreshPlan(orgId);
}
const plan = await getPlan(orgId, projectId); const plan = await getPlan(orgId, projectId);
return plan; return plan;
}; };
@@ -84,6 +84,7 @@ export type TOrgPlansTableDTO = {
export type TOrgPlanDTO = { export type TOrgPlanDTO = {
projectId?: string; projectId?: string;
refreshCache?: boolean;
} & TOrgPermission; } & TOrgPermission;
export type TStartOrgTrialDTO = { export type TStartOrgTrialDTO = {
@@ -3,7 +3,7 @@ import { useRouteContext } from "@tanstack/react-router";
import { fetchOrgSubscription, subscriptionQueryKeys } from "@app/hooks/api/subscriptions/queries"; import { fetchOrgSubscription, subscriptionQueryKeys } from "@app/hooks/api/subscriptions/queries";
export const useSubscription = () => { export const useSubscription = (refreshCache?: boolean) => {
const organizationId = useRouteContext({ const organizationId = useRouteContext({
from: "/_authenticate/_inject-org-details", from: "/_authenticate/_inject-org-details",
select: (el) => el.organizationId select: (el) => el.organizationId
@@ -11,7 +11,7 @@ export const useSubscription = () => {
const { data: subscription } = useSuspenseQuery({ const { data: subscription } = useSuspenseQuery({
queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId), queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId),
queryFn: () => fetchOrgSubscription(organizationId), queryFn: () => fetchOrgSubscription(organizationId, refreshCache),
staleTime: Infinity staleTime: Infinity
}); });
@@ -10,9 +10,9 @@ export const subscriptionQueryKeys = {
getOrgSubsription: (orgID: string) => ["plan", { orgID }] as const getOrgSubsription: (orgID: string) => ["plan", { orgID }] as const
}; };
export const fetchOrgSubscription = async (orgID: string) => { export const fetchOrgSubscription = async (orgID: string, refreshCache: boolean = false) => {
const { data } = await apiRequest.get<{ plan: SubscriptionPlan }>( const { data } = await apiRequest.get<{ plan: SubscriptionPlan }>(
`/api/v1/organizations/${orgID}/plan` `/api/v1/organizations/${orgID}/plan${refreshCache ? "?refreshCache=true" : ""}`
); );
return data.plan; return data.plan;
@@ -54,4 +54,5 @@ export type SubscriptionPlan = {
enterpriseSecretSyncs: boolean; enterpriseSecretSyncs: boolean;
enterpriseAppConnections: boolean; enterpriseAppConnections: boolean;
cardDeclined?: boolean; cardDeclined?: boolean;
cardDeclinedReason?: string;
}; };
@@ -1,4 +1,4 @@
import { useState } from "react"; import { useEffect, useState } from "react";
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons"; import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
import { faCircleQuestion, faUserCircle } from "@fortawesome/free-regular-svg-icons"; import { faCircleQuestion, faUserCircle } from "@fortawesome/free-regular-svg-icons";
import { import {
@@ -110,9 +110,14 @@ export const Navbar = () => {
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const [showAdminsModal, setShowAdminsModal] = useState(false); const [showAdminsModal, setShowAdminsModal] = useState(false);
const [showCardDeclinedModal, setShowCardDeclinedModal] = useState( const [showCardDeclinedModal, setShowCardDeclinedModal] = useState(false);
subscription?.cardDeclined || false
); useEffect(() => {
if (subscription?.cardDeclined && !sessionStorage.getItem("paymentFailed")) {
sessionStorage.setItem("paymentFailed", "true");
setShowCardDeclinedModal(true);
}
}, [subscription]);
const { data: orgs } = useGetOrganizations(); const { data: orgs } = useGetOrganizations();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -205,7 +210,7 @@ export const Navbar = () => {
</div> </div>
{subscription.cardDeclined && ( {subscription.cardDeclined && (
<Tooltip <Tooltip
content="Payment failed. Please update your payment method to continue using premium features." content={`Your payment could not be processed${subscription.cardDeclinedReason ? `: ${subscription.cardDeclinedReason}` : ""}. Please update your payment method to continue enjoying premium features.`}
className="max-w-xs" className="max-w-xs"
> >
<div className="flex items-center"> <div className="flex items-center">
@@ -418,7 +423,7 @@ export const Navbar = () => {
title={ title={
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FontAwesomeIcon icon={faExclamationTriangle} className="text-lg text-primary-400" /> <FontAwesomeIcon icon={faExclamationTriangle} className="text-lg text-primary-400" />
Your payment method has been declined Your payment could not be processed.
</div> </div>
} }
> >
@@ -426,8 +431,9 @@ export const Navbar = () => {
<div> <div>
<div className="mb-1"> <div className="mb-1">
<p> <p>
Your payment method was declined and your subscription may be at risk. Please We were unable to process your last payment
update your payment information to continue using premium features. {subscription.cardDeclinedReason ? `: ${subscription.cardDeclinedReason}` : ""}.
Please update your payment information to continue using premium features.
</p> </p>
</div> </div>
<div className="mt-4"> <div className="mt-4">
@@ -1,5 +1,7 @@
import { useEffect } from "react";
import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQueryClient } from "@tanstack/react-query";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2"; import { Button } from "@app/components/v2";
@@ -15,13 +17,15 @@ import {
useGetOrgPlanBillingInfo, useGetOrgPlanBillingInfo,
useGetOrgTrialUrl useGetOrgTrialUrl
} from "@app/hooks/api"; } from "@app/hooks/api";
import { subscriptionQueryKeys } from "@app/hooks/api/subscriptions/queries";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { ManagePlansModal } from "./ManagePlansModal"; import { ManagePlansModal } from "./ManagePlansModal";
export const PreviewSection = () => { export const PreviewSection = () => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { subscription } = useSubscription(); const { subscription } = useSubscription(true);
const queryClient = useQueryClient();
const { data, isPending } = useGetOrgPlanBillingInfo(currentOrg?.id ?? ""); const { data, isPending } = useGetOrgPlanBillingInfo(currentOrg?.id ?? "");
const getOrgTrialUrl = useGetOrgTrialUrl(); const getOrgTrialUrl = useGetOrgTrialUrl();
const createCustomerPortalSession = useCreateCustomerPortalSession(); const createCustomerPortalSession = useCreateCustomerPortalSession();
@@ -37,6 +41,12 @@ export const PreviewSection = () => {
return formattedTotal; return formattedTotal;
}; };
useEffect(() => {
queryClient.invalidateQueries({
queryKey: subscriptionQueryKeys.getOrgSubsription(currentOrg?.id ?? "")
});
}, []);
const formatDate = (date: number) => { const formatDate = (date: number) => {
const createdDate = new Date(date * 1000); const createdDate = new Date(date * 1000);
const day: number = createdDate.getDate(); const day: number = createdDate.getDate();