Skip to content

Commit eafb919

Browse files
Merge branch 'master' into production
2 parents f4fdb56 + 704739f commit eafb919

3 files changed

Lines changed: 131 additions & 26 deletions

File tree

app/src/componentsV2/BlockScreen/components/BlockScreen.tsx

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import React, { ReactElement, useEffect } from "react";
2+
import { useDispatch, useSelector } from "react-redux";
3+
import { MdLogout } from "@react-icons/all-files/md/MdLogout";
24
import "./blockscreen.scss";
35
import MinimalLayout from "layouts/MinimalLayout";
6+
import { RQButton } from "lib/design-system-v2/components";
7+
import { getAppMode } from "store/selectors";
8+
import { isActiveWorkspaceShared } from "store/slices/workspaces/selectors";
9+
import { handleLogoutButtonOnClick } from "features/onboarding/components/auth/components/Form/actions";
410
import { BlockConfig, BlockType } from "../hooks/useIsUserBlocked";
511
import { trackBlockScreenViewed } from "../analytics";
612

@@ -29,13 +35,21 @@ const BlockComponent = ({
2935
};
3036

3137
export const BlockScreen: React.FC<Props> = ({ blockConfig }) => {
38+
const dispatch = useDispatch();
39+
const appMode = useSelector(getAppMode);
40+
const isSharedWorkspaceMode = useSelector(isActiveWorkspaceShared);
41+
3242
const blockType = Object.keys(blockConfig)?.[0];
3343
const config = blockConfig[blockType as BlockType];
3444

3545
useEffect(() => {
3646
trackBlockScreenViewed(blockType);
3747
}, [blockType]);
3848

49+
const handleSignOut = () => {
50+
handleLogoutButtonOnClick(appMode, isSharedWorkspaceMode, dispatch);
51+
};
52+
3953
let blockElement = (
4054
<BlockComponent
4155
logo={<img width={56} height={56} src={"/assets/media/grr/globe-warning.svg"} alt="Blocked" />}
@@ -45,24 +59,52 @@ export const BlockScreen: React.FC<Props> = ({ blockConfig }) => {
4559
);
4660

4761
if (blockType === BlockType.GRR) {
62+
const contactEmail = config?.metadata?.contactEmail || "contact@requestly.com";
63+
const title = config?.metadata?.title || "Important Update on Requestly Usage";
64+
const contactLink = (
65+
<a
66+
target="_blank"
67+
rel="noreferrer"
68+
href={`mailto:${contactEmail}`}
69+
className="block-screen-message-contact-mail"
70+
>
71+
{contactEmail}
72+
</a>
73+
);
74+
4875
blockElement = (
4976
<BlockComponent
5077
logo={<img width={56} height={56} src={"/assets/media/grr/globe-warning.svg"} alt="GRR warning" />}
51-
title={"Important Update on Requestly Usage"}
78+
title={title}
5279
subtitle={
53-
<>
54-
Welcome to Requestly, now part of BrowserStack! Your organization requires Data Residency, and Requestly is
55-
currently being updated for full compliance. For guidance on using Requestly, please reach out to your
56-
BrowserStack Customer Success Manager or email us at
57-
<a
58-
target="_blank"
59-
rel="noreferrer"
60-
href="mailto:contact@requestly.com"
61-
className="block-screen-message-contact-mail"
62-
>
63-
contact@requestly.com
64-
</a>
65-
</>
80+
config?.metadata?.message ? (
81+
<>
82+
{config.metadata.message} {contactLink}
83+
</>
84+
) : (
85+
<>
86+
Welcome to Requestly, now part of BrowserStack! Your organization requires Data Residency, and Requestly
87+
is currently being updated for full compliance. For guidance on using Requestly, please reach out to your
88+
BrowserStack Customer Success Manager or email us at {contactLink}
89+
</>
90+
)
91+
}
92+
/>
93+
);
94+
} else if (blockType === BlockType.ACCESS_DENIED) {
95+
blockElement = (
96+
<BlockComponent
97+
logo={
98+
<img
99+
className="block-screen-illustration"
100+
src={"/assets/media/apiClient/file-error.svg"}
101+
alt="Access denied"
102+
/>
103+
}
104+
title={config?.metadata?.title || "You don't have access to Requestly"}
105+
subtitle={
106+
config?.metadata?.message ||
107+
"Your account hasn't been granted access to this product. Contact your administrator to request access, then sign in again."
66108
}
67109
/>
68110
);
@@ -106,7 +148,16 @@ export const BlockScreen: React.FC<Props> = ({ blockConfig }) => {
106148

107149
return (
108150
<MinimalLayout>
109-
<div className="block-screen-screen">{blockElement}</div>
151+
<div className="block-screen-screen">
152+
<div className="block-screen-content-wrapper">
153+
{blockElement}
154+
{blockType === BlockType.ACCESS_DENIED ? (
155+
<RQButton type="primary" onClick={handleSignOut} className="block-screen-signout-btn" icon={<MdLogout />}>
156+
Sign out
157+
</RQButton>
158+
) : null}
159+
</div>
160+
</div>
110161
</MinimalLayout>
111162
);
112163
};

app/src/componentsV2/BlockScreen/components/blockscreen.scss

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,40 @@
44
padding: var(--space-15, 80px) 10px;
55
}
66

7+
.block-screen-content-wrapper {
8+
display: flex;
9+
flex-direction: column;
10+
align-items: center;
11+
gap: var(--space-8, 24px);
12+
width: 100%;
13+
max-width: 500px;
14+
15+
.block-screen-signout-btn {
16+
margin-top: var(--space-2, 4px);
17+
}
18+
}
19+
720
.block-screen-message-container {
821
display: flex;
922
flex-direction: column;
1023
align-items: center;
1124
gap: var(--space-8, 24px);
1225
border-radius: 8px;
1326
width: 100%;
14-
max-width: 600px;
27+
max-width: 500px;
1528

1629
.block-screen-message-icon img {
1730
width: 56px;
1831
height: 56px;
1932
aspect-ratio: 1/1;
2033
}
2134

35+
.block-screen-message-icon img.block-screen-illustration {
36+
width: auto;
37+
height: 72px;
38+
aspect-ratio: auto;
39+
}
40+
2241
.block-screen-content {
2342
display: flex;
2443
flex-direction: column;

app/src/componentsV2/BlockScreen/hooks/useIsUserBlocked.ts

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,59 @@
11
import firebaseApp from "firebase";
22
import { getDatabase, onValue, ref } from "firebase/database";
33
import { doc, getFirestore, onSnapshot } from "firebase/firestore";
4-
import { useEffect, useState } from "react";
4+
import { useEffect, useMemo, useState } from "react";
55
import { useSelector } from "react-redux";
66
import { getUserAuthDetails } from "store/slices/global/user/selectors";
7+
import { isPremiumUser } from "utils/PremiumUtils";
78

89
export enum BlockType {
910
GRR = "grr",
1011
COMPLIANCE_ISSUE = "compliance-issue",
12+
// Free / unlicensed users at a gated domain (e.g. aon.com). Used with metadata.exemptBrowserstackUsers
13+
// so users holding an active BrowserStack seat pass through while everyone else sees the access screen.
14+
ACCESS_DENIED = "access-denied",
1115
}
1216

17+
type BlockConfigValue = {
18+
isBlocked: boolean;
19+
reason?: string;
20+
// metadata.exemptBrowserstackUsers (boolean): when true, users who hold an active BrowserStack-provisioned
21+
// seat are NOT blocked (e.g. block free users at a domain but let BrowserStack-licensed users through).
22+
// Omit for a full domain block (everyone at the domain is blocked).
23+
metadata?: Record<string, any>;
24+
};
25+
1326
export type BlockConfig = {
14-
[key in BlockType]?: {
15-
isBlocked: boolean;
16-
reason?: string;
17-
metadata?: Record<string, any>;
18-
};
27+
[key in BlockType]?: BlockConfigValue;
28+
};
29+
30+
/**
31+
* A user has an active BrowserStack seat when their subscription is BrowserStack-provisioned and currently active.
32+
*/
33+
const hasActiveBrowserstackSeat = (planDetails: any): boolean => {
34+
return Boolean(planDetails?.subscription?.isBrowserstackSubscription) && isPremiumUser(planDetails);
35+
};
36+
37+
/**
38+
* A domain block is skipped for BrowserStack-licensed users when metadata.exemptBrowserstackUsers is set.
39+
* Used only for domain-level blocks; explicit per-user blocks always apply.
40+
*/
41+
const isUserExemptFromBlock = (value: BlockConfigValue | undefined, hasBrowserstackSeat: boolean): boolean => {
42+
return Boolean(value?.metadata?.exemptBrowserstackUsers) && hasBrowserstackSeat;
1943
};
2044

2145
export const useIsUserBlocked = () => {
2246
const user = useSelector(getUserAuthDetails);
2347
const isLoggedIn = user?.loggedIn;
2448
const uid = user?.details?.profile?.uid;
2549
const email = user?.details?.profile?.email;
50+
const planDetails = user?.details?.planDetails;
51+
52+
const hasBrowserstackSeat = useMemo(
53+
() => hasActiveBrowserstackSeat(planDetails),
54+
// eslint-disable-next-line react-hooks/exhaustive-deps
55+
[planDetails?.planId, planDetails?.status, planDetails?.subscription?.endDate, planDetails?.subscription?.isBrowserstackSubscription]
56+
);
2657

2758
const [domainBlockConfig, setDomainBlockConfig] = useState<BlockConfig | undefined>(undefined);
2859
const [userBlockConfig, setUserBlockConfig] = useState<BlockConfig | undefined>(undefined);
@@ -77,8 +108,7 @@ export const useIsUserBlocked = () => {
77108
return;
78109
}
79110

80-
// console.log({ userBlockConfig, domainBlockConfig });
81-
111+
// Explicit per-user blocks always apply, regardless of plan.
82112
for (const [key, value] of Object.entries(userBlockConfig || {})) {
83113
if (value?.isBlocked) {
84114
setFinalBlockConfig({
@@ -90,8 +120,10 @@ export const useIsUserBlocked = () => {
90120
}
91121
}
92122

123+
// Domain-level blocks can exempt BrowserStack-licensed users via metadata.exemptBrowserstackUsers,
124+
// so free users at a domain are blocked while users with a BrowserStack seat pass through.
93125
for (const [key, value] of Object.entries(domainBlockConfig || {})) {
94-
if (value?.isBlocked) {
126+
if (value?.isBlocked && !isUserExemptFromBlock(value, hasBrowserstackSeat)) {
95127
setFinalBlockConfig({
96128
[key]: {
97129
...value,
@@ -100,7 +132,10 @@ export const useIsUserBlocked = () => {
100132
return;
101133
}
102134
}
103-
}, [domainBlockConfig, isLoggedIn, uid, userBlockConfig]);
135+
136+
// No applicable block (or user is exempt) — clear any stale block config.
137+
setFinalBlockConfig(undefined);
138+
}, [domainBlockConfig, isLoggedIn, uid, userBlockConfig, hasBrowserstackSeat]);
104139

105140
return {
106141
isBlocked: !!finalBlockConfig && Object.values(finalBlockConfig).some((value) => value.isBlocked),

0 commit comments

Comments
 (0)