diff --git a/backend/src/ee/routes/v1/license-router.ts b/backend/src/ee/routes/v1/license-router.ts index 0a59fa7b5..17923975d 100644 --- a/backend/src/ee/routes/v1/license-router.ts +++ b/backend/src/ee/routes/v1/license-router.ts @@ -43,6 +43,12 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { }, schema: { params: z.object({ organizationId: z.string().trim() }), + querystring: z.object({ + refreshCache: z + .enum(["true", "false"]) + .default("false") + .transform((value) => value === "true") + }), response: { 200: z.object({ plan: z.any() }) } @@ -54,7 +60,8 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, - orgId: req.params.organizationId + orgId: req.params.organizationId, + refreshCache: req.query.refreshCache }); return { plan }; } diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index 7e784d9ad..bb208ab2e 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -295,8 +295,19 @@ export const licenseServiceFactory = ({ 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); + if (refreshCache) { + await refreshPlan(orgId); + } const plan = await getPlan(orgId, projectId); return plan; }; diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index a3412f574..ea94f6172 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -84,6 +84,7 @@ export type TOrgPlansTableDTO = { export type TOrgPlanDTO = { projectId?: string; + refreshCache?: boolean; } & TOrgPermission; export type TStartOrgTrialDTO = { diff --git a/frontend/src/context/SubscriptionContext/SubscriptionContext.tsx b/frontend/src/context/SubscriptionContext/SubscriptionContext.tsx index de52e5f18..95da6bb01 100644 --- a/frontend/src/context/SubscriptionContext/SubscriptionContext.tsx +++ b/frontend/src/context/SubscriptionContext/SubscriptionContext.tsx @@ -3,7 +3,7 @@ import { useRouteContext } from "@tanstack/react-router"; import { fetchOrgSubscription, subscriptionQueryKeys } from "@app/hooks/api/subscriptions/queries"; -export const useSubscription = () => { +export const useSubscription = (refreshCache?: boolean) => { const organizationId = useRouteContext({ from: "/_authenticate/_inject-org-details", select: (el) => el.organizationId @@ -11,7 +11,7 @@ export const useSubscription = () => { const { data: subscription } = useSuspenseQuery({ queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId), - queryFn: () => fetchOrgSubscription(organizationId), + queryFn: () => fetchOrgSubscription(organizationId, refreshCache), staleTime: Infinity }); diff --git a/frontend/src/hooks/api/subscriptions/queries.tsx b/frontend/src/hooks/api/subscriptions/queries.tsx index 99b1f4486..f545565eb 100644 --- a/frontend/src/hooks/api/subscriptions/queries.tsx +++ b/frontend/src/hooks/api/subscriptions/queries.tsx @@ -10,9 +10,9 @@ export const subscriptionQueryKeys = { 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 }>( - `/api/v1/organizations/${orgID}/plan` + `/api/v1/organizations/${orgID}/plan${refreshCache ? "?refreshCache=true" : ""}` ); return data.plan; diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index 0893d3bd1..6042df67d 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -54,4 +54,5 @@ export type SubscriptionPlan = { enterpriseSecretSyncs: boolean; enterpriseAppConnections: boolean; cardDeclined?: boolean; + cardDeclinedReason?: string; }; diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx index 8af8a24da..96da887b1 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons"; import { faCircleQuestion, faUserCircle } from "@fortawesome/free-regular-svg-icons"; import { @@ -110,9 +110,14 @@ export const Navbar = () => { const { subscription } = useSubscription(); const { currentOrg } = useOrganization(); const [showAdminsModal, setShowAdminsModal] = useState(false); - const [showCardDeclinedModal, setShowCardDeclinedModal] = useState( - subscription?.cardDeclined || false - ); + const [showCardDeclinedModal, setShowCardDeclinedModal] = useState(false); + + useEffect(() => { + if (subscription?.cardDeclined && !sessionStorage.getItem("paymentFailed")) { + sessionStorage.setItem("paymentFailed", "true"); + setShowCardDeclinedModal(true); + } + }, [subscription]); const { data: orgs } = useGetOrganizations(); const navigate = useNavigate(); @@ -205,7 +210,7 @@ export const Navbar = () => { {subscription.cardDeclined && (
@@ -418,7 +423,7 @@ export const Navbar = () => { title={
- Your payment method has been declined + Your payment could not be processed.
} > @@ -426,8 +431,9 @@ export const Navbar = () => {

- Your payment method was declined and your subscription may be at risk. Please - update your payment information to continue using premium features. + We were unable to process your last payment + {subscription.cardDeclinedReason ? `: ${subscription.cardDeclinedReason}` : ""}. + Please update your payment information to continue using premium features.

diff --git a/frontend/src/pages/organization/BillingPage/components/BillingCloudTab/PreviewSection.tsx b/frontend/src/pages/organization/BillingPage/components/BillingCloudTab/PreviewSection.tsx index eaa90450e..6ab0429ee 100644 --- a/frontend/src/pages/organization/BillingPage/components/BillingCloudTab/PreviewSection.tsx +++ b/frontend/src/pages/organization/BillingPage/components/BillingCloudTab/PreviewSection.tsx @@ -1,5 +1,7 @@ +import { useEffect } from "react"; import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useQueryClient } from "@tanstack/react-query"; import { OrgPermissionCan } from "@app/components/permissions"; import { Button } from "@app/components/v2"; @@ -15,13 +17,15 @@ import { useGetOrgPlanBillingInfo, useGetOrgTrialUrl } from "@app/hooks/api"; +import { subscriptionQueryKeys } from "@app/hooks/api/subscriptions/queries"; import { usePopUp } from "@app/hooks/usePopUp"; import { ManagePlansModal } from "./ManagePlansModal"; export const PreviewSection = () => { const { currentOrg } = useOrganization(); - const { subscription } = useSubscription(); + const { subscription } = useSubscription(true); + const queryClient = useQueryClient(); const { data, isPending } = useGetOrgPlanBillingInfo(currentOrg?.id ?? ""); const getOrgTrialUrl = useGetOrgTrialUrl(); const createCustomerPortalSession = useCreateCustomerPortalSession(); @@ -37,6 +41,12 @@ export const PreviewSection = () => { return formattedTotal; }; + useEffect(() => { + queryClient.invalidateQueries({ + queryKey: subscriptionQueryKeys.getOrgSubsription(currentOrg?.id ?? "") + }); + }, []); + const formatDate = (date: number) => { const createdDate = new Date(date * 1000); const day: number = createdDate.getDate();