Merge pull request #4191 from Infisical/feat/cardDeclinedAlert

Add card declined alert
This commit is contained in:
carlosmonastyrski
2025-09-10 14:57:23 -03:00
committed by GitHub
8 changed files with 119 additions and 9 deletions
@@ -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
});
@@ -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;
@@ -54,5 +54,7 @@ export type SubscriptionPlan = {
secretScanning: boolean;
enterpriseSecretSyncs: boolean;
enterpriseAppConnections: boolean;
cardDeclined?: boolean;
cardDeclinedReason?: string;
machineIdentityAuthTemplates: boolean;
};
@@ -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 {
@@ -8,6 +8,7 @@ import {
faCaretDown,
faCheck,
faEnvelope,
faExclamationTriangle,
faInfo,
faInfoCircle,
faServer,
@@ -111,6 +112,14 @@ export const Navbar = () => {
const { subscription } = useSubscription();
const { currentOrg } = useOrganization();
const [showAdminsModal, setShowAdminsModal] = useState(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();
@@ -222,6 +231,19 @@ export const Navbar = () => {
<div className="mr-1 rounded border border-mineshaft-500 px-1 text-xs text-bunker-300 !no-underline">
{getPlan(subscription)}
</div>
{subscription.cardDeclined && (
<Tooltip
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"
>
<div className="flex items-center">
<FontAwesomeIcon
icon={faExclamationTriangle}
className="animate-pulse cursor-help text-xs text-primary-400"
/>
</div>
</Tooltip>
)}
</div>
</Link>
<DropdownMenuTrigger asChild>
@@ -428,6 +450,49 @@ export const Navbar = () => {
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Modal isOpen={showCardDeclinedModal} onOpenChange={setShowCardDeclinedModal}>
<ModalContent
title={
<div className="flex items-center gap-2">
<FontAwesomeIcon icon={faExclamationTriangle} className="text-lg text-primary-400" />
Your payment could not be processed.
</div>
}
>
<div>
<div>
<div className="mb-1">
<p>
We were unable to process your last payment
{subscription.cardDeclinedReason ? `: ${subscription.cardDeclinedReason}` : ""}.
Please update your payment information to continue using premium features.
</p>
</div>
<div className="mt-4">
<div className="flex space-x-3">
<Link to="/organization/billing" className="inline-flex">
<Button
colorSchema="primary"
variant="solid"
onClick={() => setShowCardDeclinedModal(false)}
>
Update Payment Method
</Button>
<Button
colorSchema="secondary"
variant="outline"
className="ml-2"
onClick={() => setShowCardDeclinedModal(false)}
>
Dismiss
</Button>
</Link>
</div>
</div>
</div>
</div>
</ModalContent>
</Modal>
<Modal isOpen={showAdminsModal} onOpenChange={setShowAdminsModal}>
<ModalContent title="Server Administrators" subTitle="View all server administrators">
<div className="mb-2">
@@ -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();