mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 03:27:38 +00:00
Merge pull request #4191 from Infisical/feat/cardDeclinedAlert
Add card declined alert
This commit is contained in:
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,17 @@ export const licenseServiceFactory = ({
|
|||||||
const workspacesUsed = await projectDAL.countOfOrgProjects(null);
|
const workspacesUsed = await projectDAL.countOfOrgProjects(null);
|
||||||
currentPlan.workspacesUsed = workspacesUsed;
|
currentPlan.workspacesUsed = workspacesUsed;
|
||||||
|
|
||||||
|
const usedIdentitySeats = await licenseDAL.countOrgUsersAndIdentities(null);
|
||||||
|
if (usedIdentitySeats !== currentPlan.identitiesUsed) {
|
||||||
|
const usedSeats = await licenseDAL.countOfOrgMembers(null);
|
||||||
|
await licenseServerOnPremApi.request.patch(`/api/license/v1/license`, {
|
||||||
|
usedSeats,
|
||||||
|
usedIdentitySeats
|
||||||
|
});
|
||||||
|
currentPlan.identitiesUsed = usedIdentitySeats;
|
||||||
|
currentPlan.membersUsed = usedSeats;
|
||||||
|
}
|
||||||
|
|
||||||
onPremFeatures = currentPlan;
|
onPremFeatures = currentPlan;
|
||||||
logger.info("Successfully synchronized license key features");
|
logger.info("Successfully synchronized license key features");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -226,10 +237,13 @@ export const licenseServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const refreshPlan = async (orgId: string) => {
|
const refreshPlan = async (orgId: string) => {
|
||||||
|
await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId));
|
||||||
if (instanceType === InstanceType.Cloud) {
|
if (instanceType === InstanceType.Cloud) {
|
||||||
await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId));
|
|
||||||
await getPlan(orgId);
|
await getPlan(orgId);
|
||||||
}
|
}
|
||||||
|
if (instanceType === InstanceType.EnterpriseOnPrem) {
|
||||||
|
await syncLicenseKeyOnPremFeatures(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateOrgCustomerId = async (orgName: string, email?: string | null) => {
|
const generateOrgCustomerId = async (orgName: string, email?: string | null) => {
|
||||||
@@ -296,8 +310,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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -87,6 +87,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,5 +54,7 @@ export type SubscriptionPlan = {
|
|||||||
secretScanning: boolean;
|
secretScanning: boolean;
|
||||||
enterpriseSecretSyncs: boolean;
|
enterpriseSecretSyncs: boolean;
|
||||||
enterpriseAppConnections: boolean;
|
enterpriseAppConnections: boolean;
|
||||||
|
cardDeclined?: boolean;
|
||||||
|
cardDeclinedReason?: string;
|
||||||
machineIdentityAuthTemplates: boolean;
|
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 { 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 {
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
faCaretDown,
|
faCaretDown,
|
||||||
faCheck,
|
faCheck,
|
||||||
faEnvelope,
|
faEnvelope,
|
||||||
|
faExclamationTriangle,
|
||||||
faInfo,
|
faInfo,
|
||||||
faInfoCircle,
|
faInfoCircle,
|
||||||
faServer,
|
faServer,
|
||||||
@@ -111,6 +112,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(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();
|
||||||
@@ -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">
|
<div className="mr-1 rounded border border-mineshaft-500 px-1 text-xs text-bunker-300 !no-underline">
|
||||||
{getPlan(subscription)}
|
{getPlan(subscription)}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
@@ -428,6 +450,49 @@ export const Navbar = () => {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</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}>
|
<Modal isOpen={showAdminsModal} onOpenChange={setShowAdminsModal}>
|
||||||
<ModalContent title="Server Administrators" subTitle="View all server administrators">
|
<ModalContent title="Server Administrators" subTitle="View all server administrators">
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
|
|||||||
+11
-1
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user