Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 66 additions & 15 deletions app/src/componentsV2/BlockScreen/components/BlockScreen.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -29,13 +35,21 @@ const BlockComponent = ({
};

export const BlockScreen: React.FC<Props> = ({ blockConfig }) => {
const dispatch = useDispatch();
const appMode = useSelector(getAppMode);
const isSharedWorkspaceMode = useSelector(isActiveWorkspaceShared);

const blockType = Object.keys(blockConfig)?.[0];
const config = blockConfig[blockType as BlockType];

useEffect(() => {
trackBlockScreenViewed(blockType);
}, [blockType]);

const handleSignOut = () => {
handleLogoutButtonOnClick(appMode, isSharedWorkspaceMode, dispatch);
};

let blockElement = (
<BlockComponent
logo={<img width={56} height={56} src={"/assets/media/grr/globe-warning.svg"} alt="Blocked" />}
Expand All @@ -45,24 +59,52 @@ export const BlockScreen: React.FC<Props> = ({ 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 = (
<a
target="_blank"
rel="noreferrer"
href={`mailto:${contactEmail}`}
className="block-screen-message-contact-mail"
>
{contactEmail}
</a>
);

blockElement = (
<BlockComponent
logo={<img width={56} height={56} src={"/assets/media/grr/globe-warning.svg"} alt="GRR warning" />}
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
<a
target="_blank"
rel="noreferrer"
href="mailto:contact@requestly.com"
className="block-screen-message-contact-mail"
>
contact@requestly.com
</a>
</>
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 = (
<BlockComponent
logo={
<img
className="block-screen-illustration"
src={"/assets/media/apiClient/file-error.svg"}
alt="Access denied"
/>
}
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."
}
/>
);
Expand Down Expand Up @@ -106,7 +148,16 @@ export const BlockScreen: React.FC<Props> = ({ blockConfig }) => {

return (
<MinimalLayout>
<div className="block-screen-screen">{blockElement}</div>
<div className="block-screen-screen">
<div className="block-screen-content-wrapper">
{blockElement}
{blockType === BlockType.ACCESS_DENIED ? (
<RQButton type="primary" onClick={handleSignOut} className="block-screen-signout-btn" icon={<MdLogout />}>
Sign out
</RQButton>
) : null}
</div>
</div>
</MinimalLayout>
);
};
21 changes: 20 additions & 1 deletion app/src/componentsV2/BlockScreen/components/blockscreen.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,40 @@
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;
align-items: center;
gap: var(--space-8, 24px);
border-radius: 8px;
width: 100%;
max-width: 600px;
max-width: 500px;

.block-screen-message-icon img {
width: 56px;
height: 56px;
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;
Expand Down
55 changes: 45 additions & 10 deletions app/src/componentsV2/BlockScreen/hooks/useIsUserBlocked.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,59 @@
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<string, any>;
};

export type BlockConfig = {
[key in BlockType]?: {
isBlocked: boolean;
reason?: string;
metadata?: Record<string, any>;
};
[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 = () => {
const user = useSelector(getUserAuthDetails);
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<BlockConfig | undefined>(undefined);
const [userBlockConfig, setUserBlockConfig] = useState<BlockConfig | undefined>(undefined);
Expand Down Expand Up @@ -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({
Expand All @@ -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,
Expand All @@ -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),
Expand Down