diff --git a/apps/staking/src/app/restricted-mode/page.tsx b/apps/staking/src/app/restricted-mode/page.tsx new file mode 100644 index 0000000000..fb09aec3bc --- /dev/null +++ b/apps/staking/src/app/restricted-mode/page.tsx @@ -0,0 +1 @@ +export { RestrictedMode as default } from "../../components/Home"; diff --git a/apps/staking/src/components/AccountSummary/index.tsx b/apps/staking/src/components/AccountSummary/index.tsx index 2ac4cca3a7..fc0328dd97 100644 --- a/apps/staking/src/components/AccountSummary/index.tsx +++ b/apps/staking/src/components/AccountSummary/index.tsx @@ -42,6 +42,7 @@ type Props = { availableRewards: bigint; expiringRewards: Date | undefined; availableToWithdraw: bigint; + restrictedMode?: boolean | undefined; }; export const AccountSummary = ({ @@ -54,6 +55,7 @@ export const AccountSummary = ({ availableToWithdraw, availableRewards, expiringRewards, + restrictedMode, }: Props) => (
)}
- + {!restrictedMode && ( + + )} {availableToWithdraw === 0n ? ( - {availableRewards === 0n || - api.type === ApiStateType.LoadedNoStakeAccount ? ( - -

You have no rewards available to be claimed

-
- ) : ( - - )} -
+ {!restrictedMode && ( + + + {availableRewards === 0n || + api.type === ApiStateType.LoadedNoStakeAccount ? ( + +

You have no rewards available to be claimed

+
+ ) : ( + + )} +
+ )}
@@ -173,35 +179,37 @@ export const AccountSummary = ({ } /> - - ) : ( - - ) - } - {...(expiringRewards !== undefined && - availableRewards > 0n && { - warning: ( - <> - Rewards expire one year from the epoch in which they were - earned. You have rewards expiring on{" "} - {expiringRewards.toLocaleDateString()}. - - ), - })} - /> + {!restrictedMode && ( + + ) : ( + + ) + } + {...(expiringRewards !== undefined && + availableRewards > 0n && { + warning: ( + <> + Rewards expire one year from the epoch in which they were + earned. You have rewards expiring on{" "} + {expiringRewards.toLocaleDateString()}. + + ), + })} + /> + )}
diff --git a/apps/staking/src/components/Dashboard/index.tsx b/apps/staking/src/components/Dashboard/index.tsx index eb4ea3e31e..4567e486c5 100644 --- a/apps/staking/src/components/Dashboard/index.tsx +++ b/apps/staking/src/components/Dashboard/index.tsx @@ -43,6 +43,7 @@ type Props = { integrityStakingPublishers: ComponentProps< typeof OracleIntegrityStaking >["publishers"]; + restrictedMode?: boolean | undefined; }; export const Dashboard = ({ @@ -57,6 +58,7 @@ export const Dashboard = ({ integrityStakingPublishers, unlockSchedule, yieldRate, + restrictedMode, }: Props) => { const [tab, setTab] = useState(TabIds.Empty); @@ -126,7 +128,11 @@ export const Dashboard = ({ }, [tab]); return ( -
+
- -

- Choose Your Journey -

- - - - Secure the Oracle -
- to Earn Rewards -
- - Gain Voting Power -
- for Governance -
-
- - - - - - - -
+ {restrictedMode ? ( + + ) : ( + +

+ Choose Your Journey +

+ + + + Secure the Oracle +
+ to Earn Rewards +
+ + Gain Voting Power +
+ for Governance +
+
+ + + + + + + +
+ )}
); }; diff --git a/apps/staking/src/components/Governance/index.tsx b/apps/staking/src/components/Governance/index.tsx index 07dd92ac24..d5fc12fa60 100644 --- a/apps/staking/src/components/Governance/index.tsx +++ b/apps/staking/src/components/Governance/index.tsx @@ -1,3 +1,5 @@ +import clsx from "clsx"; + import { type States, StateType as ApiStateType } from "../../hooks/use-api"; import { GovernanceGuide } from "../GovernanceGuide"; import { ProgramSection } from "../ProgramSection"; @@ -10,6 +12,7 @@ type Props = { staked: bigint; cooldown: bigint; cooldown2: bigint; + restrictedMode?: boolean | undefined; }; export const Governance = ({ @@ -20,8 +23,10 @@ export const Governance = ({ staked, cooldown, cooldown2, + restrictedMode, }: Props) => ( } tagline="Vote and Influence the Network" @@ -33,8 +38,6 @@ export const Governance = ({ staked, cooldown, cooldown2, - stake: api.type === ApiStateType.Loaded ? api.stakeGovernance : undefined, - stakeDescription: "Stake funds to participate in governance votes", cancelWarmup: api.type === ApiStateType.Loaded ? api.cancelWarmupGovernance @@ -44,6 +47,11 @@ export const Governance = ({ unstake: api.type === ApiStateType.Loaded ? api.unstakeGovernance : undefined, unstakeDescription: "Unstake tokens from the Governance program", + ...(!restrictedMode && { + stake: + api.type === ApiStateType.Loaded ? api.stakeGovernance : undefined, + stakeDescription: "Stake funds to participate in governance votes", + }), }} /> ); diff --git a/apps/staking/src/components/Header/current-stake-account.tsx b/apps/staking/src/components/Header/current-stake-account.tsx index 9024688f21..553be7578a 100644 --- a/apps/staking/src/components/Header/current-stake-account.tsx +++ b/apps/staking/src/components/Header/current-stake-account.tsx @@ -1,8 +1,10 @@ "use client"; import clsx from "clsx"; +import { useSelectedLayoutSegment } from "next/navigation"; import { type HTMLProps } from "react"; +import { VPN_BLOCKED_SEGMENT } from "../../config/isomorphic"; import { StateType as ApiStateType, useApi } from "../../hooks/use-api"; import { CopyButton } from "../CopyButton"; import { TruncatedKey } from "../TruncatedKey"; @@ -11,9 +13,11 @@ export const CurrentStakeAccount = ({ className, ...props }: HTMLProps) => { + const segment = useSelectedLayoutSegment(); + const isBlocked = segment === VPN_BLOCKED_SEGMENT; const api = useApi(); - return api.type === ApiStateType.Loaded ? ( + return api.type === ApiStateType.Loaded && !isBlocked ? (
{ +export const Home = () => ; +export const RestrictedMode = () => ; + +type HomeImplProps = { + restrictedMode?: boolean | undefined; +}; + +export const HomeImpl = ({ restrictedMode }: HomeImplProps) => { const isSSR = useIsSSR(); - return isSSR ? : ; + return isSSR ? : ; +}; + +type MountedHomeProps = { + restrictedMode?: boolean | undefined; }; -const MountedHome = () => { +const MountedHome = ({ restrictedMode }: MountedHomeProps) => { const api = useApi(); switch (api.type) { @@ -44,16 +55,22 @@ const MountedHome = () => { } case ApiStateType.LoadedNoStakeAccount: case ApiStateType.Loaded: { - return ; + return ( + + ); } } }; type StakeAccountLoadedHomeProps = { api: States[ApiStateType.Loaded] | States[ApiStateType.LoadedNoStakeAccount]; + restrictedMode?: boolean | undefined; }; -const StakeAccountLoadedHome = ({ api }: StakeAccountLoadedHomeProps) => { +const StakeAccountLoadedHome = ({ + api, + restrictedMode, +}: StakeAccountLoadedHomeProps) => { const data = useData(api.dashboardDataCacheKey, api.loadData, { refreshInterval: REFRESH_INTERVAL, }); @@ -69,7 +86,9 @@ const StakeAccountLoadedHome = ({ api }: StakeAccountLoadedHomeProps) => { } case DashboardDataStateType.Loaded: { - return ; + return ( + + ); } } }; diff --git a/apps/staking/src/components/OracleIntegrityStaking/index.tsx b/apps/staking/src/components/OracleIntegrityStaking/index.tsx index 702ed71d8d..ad585b11ed 100644 --- a/apps/staking/src/components/OracleIntegrityStaking/index.tsx +++ b/apps/staking/src/components/OracleIntegrityStaking/index.tsx @@ -289,7 +289,7 @@ const SelfStaking = ({ Historical APY Number of feeds Quality ranking - {availableToStake > 0n && } + @@ -690,7 +690,10 @@ const PublisherList = ({ onSelectionChange={updateSort} > - @@ -1396,7 +1399,7 @@ const YourPositionsTable = ({ currentEpoch, publisher, }: YourPositionsTableProps) => ( -
+
Your Positions diff --git a/apps/staking/src/components/ProgramSection/index.tsx b/apps/staking/src/components/ProgramSection/index.tsx index 817d2fe5a9..7e4ece04fc 100644 --- a/apps/staking/src/components/ProgramSection/index.tsx +++ b/apps/staking/src/components/ProgramSection/index.tsx @@ -32,7 +32,7 @@ export const ProgramSection = ({ }: Props) => (
( redHatMono.variable, )} > - +
+ {children} diff --git a/apps/staking/src/components/Root/restricted-region-banner.tsx b/apps/staking/src/components/Root/restricted-region-banner.tsx new file mode 100644 index 0000000000..66dde5364f --- /dev/null +++ b/apps/staking/src/components/Root/restricted-region-banner.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { useSelectedLayoutSegment } from "next/navigation"; + +import { RESTRICTED_MODE_SEGMENT } from "../../config/isomorphic"; +import { Link } from "../Link"; + +export const RestrictedRegionBanner = () => { + const segment = useSelectedLayoutSegment(); + const isRestrictedMode = segment === RESTRICTED_MODE_SEGMENT; + + return isRestrictedMode ? ( +
+

LEGAL NOTICE

+

+ Your access to this Website and its Services is restricted. +

+

+ It appears that you are located in a jurisdiction subject to + restrictions under our{" "} + + Terms of Service + + . As a result, you are not permitted to use or access certain Services + on this Website. However, you are still allowed to use the Unstake and + Withdraw functions. +

+

+ Any attempt to bypass these restrictions, including the use of VPNs or + similar technologies, is strictly prohibited. +

+
+ ) : ( +
+ ); +}; diff --git a/apps/staking/src/components/WalletButton/index.tsx b/apps/staking/src/components/WalletButton/index.tsx index 594d3871d1..74ad476d39 100644 --- a/apps/staking/src/components/WalletButton/index.tsx +++ b/apps/staking/src/components/WalletButton/index.tsx @@ -28,10 +28,7 @@ import { Collection, } from "react-aria-components"; -import { - REGION_BLOCKED_SEGMENT, - VPN_BLOCKED_SEGMENT, -} from "../../config/isomorphic"; +import { VPN_BLOCKED_SEGMENT } from "../../config/isomorphic"; import { StateType as ApiStateType, type States, @@ -52,8 +49,7 @@ type Props = Omit, "onClick" | "children">; export const WalletButton = (props: Props) => { const segment = useSelectedLayoutSegment(); - const isBlocked = - segment === REGION_BLOCKED_SEGMENT || segment === VPN_BLOCKED_SEGMENT; + const isBlocked = segment === VPN_BLOCKED_SEGMENT; // eslint-disable-next-line unicorn/no-null return isBlocked ? null : ; diff --git a/apps/staking/src/config/isomorphic.ts b/apps/staking/src/config/isomorphic.ts index ca50d0a57f..eb77bfdd81 100644 --- a/apps/staking/src/config/isomorphic.ts +++ b/apps/staking/src/config/isomorphic.ts @@ -13,18 +13,19 @@ export const IS_PRODUCTION_BUILD = process.env.NODE_ENV === "production"; /** - * Region-blocked requests will be redirected here. This is used in the + * Region or VPN-blocked requests will be redirected here if they are eligible + * for "restricted mode" (aka only allowing withdrawals). This is used in the * middleware to implement the block, and also consumed in any components that * are part of the page layout but need to know if the request is blocked from * accessing the app, such as the WalletButton in the app header. * * Don't change unless you also change the relevant app route path to match. */ -export const REGION_BLOCKED_SEGMENT = "region-blocked"; +export const RESTRICTED_MODE_SEGMENT = "restricted-mode"; /** - * Similar to `REGION_BLOCKED_SEGMENT`; this is where vpn-blocked traffic will - * be rewritten to. + * Similar to `RESTRICTED_MODE_SEGMENT`; this is where vpn-blocked traffic will + * be rewritten to if it isn't eligible for restricted mode. * * Don't change unless you also change the relevant app route path to match. */ diff --git a/apps/staking/src/config/server.ts b/apps/staking/src/config/server.ts index ef3af27bfe..a41c75da03 100644 --- a/apps/staking/src/config/server.ts +++ b/apps/staking/src/config/server.ts @@ -9,13 +9,30 @@ import "server-only"; */ const demand = (key: string): string => { const value = process.env[key]; - if (value && value !== "") { - return value; - } else { + if (value === undefined || value === "") { throw new MissingEnvironmentError(key); + } else { + return value; } }; +const fromCsv = (value: string): string[] => + value.split(",").map((entry) => entry.toLowerCase().trim()); + +const transform = (key: string, fn: (value: string | undefined) => T): T => { + const value = process.env[key]; + return fn(value === "" ? undefined : value); +}; + +const transformOr = ( + key: string, + fn: (value: string) => T, + defaultValue: T, +): T => transform(key, (value) => (value ? fn(value) : defaultValue)); + +const getOr = (key: string, defaultValue: string): string => + transform(key, (value) => value ?? defaultValue); + /** * Indicates that this server is the live customer-facing production server. */ @@ -36,15 +53,8 @@ export const WALLETCONNECT_PROJECT_ID = demandInProduction( ); export const RPC = process.env.RPC; export const IS_MAINNET = process.env.IS_MAINNET !== undefined; -export const HERMES_URL = - process.env.HERMES_URL ?? "https://hermes.pyth.network"; -export const BLOCKED_REGIONS = - process.env.BLOCKED_REGIONS === undefined || - process.env.BLOCKED_REGIONS === "" - ? [] - : process.env.BLOCKED_REGIONS.split(",").map((region) => - region.toLowerCase().trim(), - ); +export const HERMES_URL = getOr("HERMES_URL", "https://hermes.pyth.network"); +export const BLOCKED_REGIONS = transformOr("BLOCKED_REGIONS", fromCsv, []); export const PROXYCHECK_API_KEY = demandInProduction("PROXYCHECK_API_KEY"); class MissingEnvironmentError extends Error { diff --git a/apps/staking/src/middleware.ts b/apps/staking/src/middleware.ts index 2977f0e1ef..032296fa46 100644 --- a/apps/staking/src/middleware.ts +++ b/apps/staking/src/middleware.ts @@ -2,12 +2,12 @@ import { type NextRequest, NextResponse } from "next/server"; import ProxyCheck from "proxycheck-ts"; import { - REGION_BLOCKED_SEGMENT, + RESTRICTED_MODE_SEGMENT, VPN_BLOCKED_SEGMENT, } from "./config/isomorphic"; import { BLOCKED_REGIONS, PROXYCHECK_API_KEY } from "./config/server"; -const PROXY_BLOCK_PATH = `/${REGION_BLOCKED_SEGMENT}`; +const RESTRICTED_MODE_PATH = `/${RESTRICTED_MODE_SEGMENT}`; const VPN_BLOCK_PATH = `/${VPN_BLOCKED_SEGMENT}`; const proxyCheckClient = PROXYCHECK_API_KEY @@ -15,10 +15,10 @@ const proxyCheckClient = PROXYCHECK_API_KEY : undefined; export const middleware = async (request: NextRequest) => { - if (isRegionBlocked(request)) { - return rewrite(request, PROXY_BLOCK_PATH); - } else if (await isProxyBlocked(request)) { + if (await isProxyBlocked(request)) { return rewrite(request, VPN_BLOCK_PATH); + } else if (isRegionBlocked(request)) { + return rewrite(request, RESTRICTED_MODE_PATH); } else if (isBlockedSegment(request)) { return rewrite(request, "/not-found"); } else { @@ -43,12 +43,12 @@ const isProxyBlocked = async ({ ip }: NextRequest) => { }; const isBlockedSegment = ({ nextUrl: { pathname } }: NextRequest) => - pathname.startsWith(`/${REGION_BLOCKED_SEGMENT}`) || - pathname.startsWith(`/${VPN_BLOCKED_SEGMENT}`); + pathname.startsWith(`/${VPN_BLOCKED_SEGMENT}`) || + pathname.startsWith(`/${RESTRICTED_MODE_SEGMENT}`); export const config = { // Next.js requires that this is a static string and fails to read it if it's // a String.raw, so let's disable this rule // eslint-disable-next-line unicorn/prefer-string-raw - matcher: ["/((?!_next/static|_next/image|.*\\.).*)"], + matcher: ["/((?!_next/static|_next/image|api/|terms-of-service|.*\\.).*)"], };