diff --git a/app/src/componentsV2/BlockScreen/components/BlockScreen.tsx b/app/src/componentsV2/BlockScreen/components/BlockScreen.tsx index a5bda7581e..5a83b00af5 100644 --- a/app/src/componentsV2/BlockScreen/components/BlockScreen.tsx +++ b/app/src/componentsV2/BlockScreen/components/BlockScreen.tsx @@ -1,6 +1,12 @@ import React, { ReactElement, useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { MdLogout } from "@react-icons/all-files/md/MdLogout"; import "./blockscreen.scss"; import MinimalLayout from "layouts/MinimalLayout"; +import { RQButton } from "lib/design-system-v2/components"; +import { getAppMode } from "store/selectors"; +import { isActiveWorkspaceShared } from "store/slices/workspaces/selectors"; +import { handleLogoutButtonOnClick } from "features/onboarding/components/auth/components/Form/actions"; import { BlockConfig, BlockType } from "../hooks/useIsUserBlocked"; import { trackBlockScreenViewed } from "../analytics"; @@ -29,6 +35,10 @@ const BlockComponent = ({ }; export const BlockScreen: React.FC = ({ blockConfig }) => { + const dispatch = useDispatch(); + const appMode = useSelector(getAppMode); + const isSharedWorkspaceMode = useSelector(isActiveWorkspaceShared); + const blockType = Object.keys(blockConfig)?.[0]; const config = blockConfig[blockType as BlockType]; @@ -36,6 +46,10 @@ export const BlockScreen: React.FC = ({ blockConfig }) => { trackBlockScreenViewed(blockType); }, [blockType]); + const handleSignOut = () => { + handleLogoutButtonOnClick(appMode, isSharedWorkspaceMode, dispatch); + }; + let blockElement = ( } @@ -45,24 +59,52 @@ export const BlockScreen: React.FC = ({ blockConfig }) => { ); if (blockType === BlockType.GRR) { + const contactEmail = config?.metadata?.contactEmail || "contact@requestly.com"; + const title = config?.metadata?.title || "Important Update on Requestly Usage"; + const contactLink = ( + + {contactEmail} + + ); + blockElement = ( } - title={"Important Update on Requestly Usage"} + title={title} subtitle={ - <> - Welcome to Requestly, now part of BrowserStack! Your organization requires Data Residency, and Requestly is - currently being updated for full compliance. For guidance on using Requestly, please reach out to your - BrowserStack Customer Success Manager or email us at - - contact@requestly.com - - + config?.metadata?.message ? ( + <> + {config.metadata.message} {contactLink} + + ) : ( + <> + Welcome to Requestly, now part of BrowserStack! Your organization requires Data Residency, and Requestly + is currently being updated for full compliance. For guidance on using Requestly, please reach out to your + BrowserStack Customer Success Manager or email us at {contactLink} + + ) + } + /> + ); + } else if (blockType === BlockType.ACCESS_DENIED) { + blockElement = ( + + } + title={config?.metadata?.title || "You don't have access to Requestly"} + subtitle={ + config?.metadata?.message || + "Your account hasn't been granted access to this product. Contact your administrator to request access, then sign in again." } /> ); @@ -106,7 +148,16 @@ export const BlockScreen: React.FC = ({ blockConfig }) => { return ( -
{blockElement}
+
+
+ {blockElement} + {blockType === BlockType.ACCESS_DENIED ? ( + }> + Sign out + + ) : null} +
+
); }; diff --git a/app/src/componentsV2/BlockScreen/components/blockscreen.scss b/app/src/componentsV2/BlockScreen/components/blockscreen.scss index c30a871743..9e9604ce7d 100644 --- a/app/src/componentsV2/BlockScreen/components/blockscreen.scss +++ b/app/src/componentsV2/BlockScreen/components/blockscreen.scss @@ -4,6 +4,19 @@ padding: var(--space-15, 80px) 10px; } +.block-screen-content-wrapper { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-8, 24px); + width: 100%; + max-width: 500px; + + .block-screen-signout-btn { + margin-top: var(--space-2, 4px); + } +} + .block-screen-message-container { display: flex; flex-direction: column; @@ -11,7 +24,7 @@ gap: var(--space-8, 24px); border-radius: 8px; width: 100%; - max-width: 600px; + max-width: 500px; .block-screen-message-icon img { width: 56px; @@ -19,6 +32,12 @@ aspect-ratio: 1/1; } + .block-screen-message-icon img.block-screen-illustration { + width: auto; + height: 72px; + aspect-ratio: auto; + } + .block-screen-content { display: flex; flex-direction: column; diff --git a/app/src/componentsV2/BlockScreen/hooks/useIsUserBlocked.ts b/app/src/componentsV2/BlockScreen/hooks/useIsUserBlocked.ts index 52e5c305d5..4dc01b72f6 100644 --- a/app/src/componentsV2/BlockScreen/hooks/useIsUserBlocked.ts +++ b/app/src/componentsV2/BlockScreen/hooks/useIsUserBlocked.ts @@ -1,21 +1,45 @@ import firebaseApp from "firebase"; import { getDatabase, onValue, ref } from "firebase/database"; import { doc, getFirestore, onSnapshot } from "firebase/firestore"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useSelector } from "react-redux"; import { getUserAuthDetails } from "store/slices/global/user/selectors"; +import { isPremiumUser } from "utils/PremiumUtils"; export enum BlockType { GRR = "grr", COMPLIANCE_ISSUE = "compliance-issue", + // Free / unlicensed users at a gated domain (e.g. aon.com). Used with metadata.exemptBrowserstackUsers + // so users holding an active BrowserStack seat pass through while everyone else sees the access screen. + ACCESS_DENIED = "access-denied", } +type BlockConfigValue = { + isBlocked: boolean; + reason?: string; + // metadata.exemptBrowserstackUsers (boolean): when true, users who hold an active BrowserStack-provisioned + // seat are NOT blocked (e.g. block free users at a domain but let BrowserStack-licensed users through). + // Omit for a full domain block (everyone at the domain is blocked). + metadata?: Record; +}; + export type BlockConfig = { - [key in BlockType]?: { - isBlocked: boolean; - reason?: string; - metadata?: Record; - }; + [key in BlockType]?: BlockConfigValue; +}; + +/** + * A user has an active BrowserStack seat when their subscription is BrowserStack-provisioned and currently active. + */ +const hasActiveBrowserstackSeat = (planDetails: any): boolean => { + return Boolean(planDetails?.subscription?.isBrowserstackSubscription) && isPremiumUser(planDetails); +}; + +/** + * A domain block is skipped for BrowserStack-licensed users when metadata.exemptBrowserstackUsers is set. + * Used only for domain-level blocks; explicit per-user blocks always apply. + */ +const isUserExemptFromBlock = (value: BlockConfigValue | undefined, hasBrowserstackSeat: boolean): boolean => { + return Boolean(value?.metadata?.exemptBrowserstackUsers) && hasBrowserstackSeat; }; export const useIsUserBlocked = () => { @@ -23,6 +47,13 @@ export const useIsUserBlocked = () => { const isLoggedIn = user?.loggedIn; const uid = user?.details?.profile?.uid; const email = user?.details?.profile?.email; + const planDetails = user?.details?.planDetails; + + const hasBrowserstackSeat = useMemo( + () => hasActiveBrowserstackSeat(planDetails), + // eslint-disable-next-line react-hooks/exhaustive-deps + [planDetails?.planId, planDetails?.status, planDetails?.subscription?.endDate, planDetails?.subscription?.isBrowserstackSubscription] + ); const [domainBlockConfig, setDomainBlockConfig] = useState(undefined); const [userBlockConfig, setUserBlockConfig] = useState(undefined); @@ -77,8 +108,7 @@ export const useIsUserBlocked = () => { return; } - // console.log({ userBlockConfig, domainBlockConfig }); - + // Explicit per-user blocks always apply, regardless of plan. for (const [key, value] of Object.entries(userBlockConfig || {})) { if (value?.isBlocked) { setFinalBlockConfig({ @@ -90,8 +120,10 @@ export const useIsUserBlocked = () => { } } + // Domain-level blocks can exempt BrowserStack-licensed users via metadata.exemptBrowserstackUsers, + // so free users at a domain are blocked while users with a BrowserStack seat pass through. for (const [key, value] of Object.entries(domainBlockConfig || {})) { - if (value?.isBlocked) { + if (value?.isBlocked && !isUserExemptFromBlock(value, hasBrowserstackSeat)) { setFinalBlockConfig({ [key]: { ...value, @@ -100,7 +132,10 @@ export const useIsUserBlocked = () => { return; } } - }, [domainBlockConfig, isLoggedIn, uid, userBlockConfig]); + + // No applicable block (or user is exempt) — clear any stale block config. + setFinalBlockConfig(undefined); + }, [domainBlockConfig, isLoggedIn, uid, userBlockConfig, hasBrowserstackSeat]); return { isBlocked: !!finalBlockConfig && Object.values(finalBlockConfig).some((value) => value.isBlocked),