diff --git a/cypress/e2e/userFeatureFlags.cy.ts b/cypress/e2e/userFeatureFlags.cy.ts
index 1f455096..fe18e176 100644
--- a/cypress/e2e/userFeatureFlags.cy.ts
+++ b/cypress/e2e/userFeatureFlags.cy.ts
@@ -30,6 +30,7 @@ const TEST_FEED_URL = '/feeds/gtfs/test-516';
const ALL_DEFAULTS = {
isNotificationsEnabled: false,
isSealFilterEnabled: false,
+ isSealEnabled: false,
};
interface MockFeature {
@@ -149,10 +150,7 @@ describe('User Feature Flags', () => {
cy.visit('/');
- expectResolvedFlags({
- isNotificationsEnabled: false,
- isSealFilterEnabled: true,
- });
+ expectResolvedFlags({ ...ALL_DEFAULTS, isSealFilterEnabled: true });
});
it('resolves to defaults when the profile carries no flags', () => {
diff --git a/src/app/[locale]/feeds/[feedDataType]/[feedId]/authed/seal-of-reliability/layout.tsx b/src/app/[locale]/feeds/[feedDataType]/[feedId]/authed/seal-of-reliability/layout.tsx
new file mode 100644
index 00000000..acf6ffca
--- /dev/null
+++ b/src/app/[locale]/feeds/[feedDataType]/[feedId]/authed/seal-of-reliability/layout.tsx
@@ -0,0 +1,47 @@
+'use client';
+
+import { type ReactElement, type ReactNode } from 'react';
+import { notFound } from 'next/navigation';
+import { useUserFeatureFlags } from '../../../../../../hooks/useUserFeatureFlags';
+import SealReliabilitySkeleton from '../../components/SealReliabilitySkeleton';
+
+interface Props {
+ children: ReactNode;
+}
+
+/**
+ * Entitlement gate for the Seal of Reliability breakdown page.
+ *
+ * `isSealEnabled` is a per-user flag with no server-side read by design (see
+ * docs/user-feature-flags.md), so this is the only place in the route where
+ * its value exists. The guest half of the route is blocked server-side
+ * instead — see the sibling `static/seal-of-reliability/page.tsx`.
+ *
+ * NOTE: This is a UX gate, not a security boundary. `children` is rendered on
+ * the server regardless, so an unentitled user can still read the seal data
+ * out of the RSC payload. If that matters, the feed API is the place to
+ * enforce it — it already receives `x-mdb-user-context` on these calls, and
+ * this segment's `error.tsx` handles the rejection.
+ *
+ * Once the seal feature fully rolls out, this route guard will no longer be needed
+ */
+export default function AuthedFeedReliabilityLayout({
+ children,
+}: Props): ReactElement {
+ const {
+ flags: { isSealEnabled },
+ isResolved,
+ } = useUserFeatureFlags();
+
+ // Pending is not the same as not-entitled: until the flags resolve the
+ // value is a placeholder, so render the loading state rather than a 404.
+ if (!isResolved) {
+ return ;
+ }
+
+ if (!isSealEnabled) {
+ notFound();
+ }
+
+ return <>{children}>;
+}
diff --git a/src/app/[locale]/feeds/[feedDataType]/[feedId]/static/seal-of-reliability/page.tsx b/src/app/[locale]/feeds/[feedDataType]/[feedId]/static/seal-of-reliability/page.tsx
index 8eb348e3..1a6abfd5 100644
--- a/src/app/[locale]/feeds/[feedDataType]/[feedId]/static/seal-of-reliability/page.tsx
+++ b/src/app/[locale]/feeds/[feedDataType]/[feedId]/static/seal-of-reliability/page.tsx
@@ -1,12 +1,12 @@
-import FeedReliabilityView from '../../../../../../screens/Feed/components/FeedReliabilityView';
+// import FeedReliabilityView from '../../../../../../screens/Feed/components/FeedReliabilityView';
import { type ReactElement } from 'react';
import { notFound } from 'next/navigation';
import type { Metadata, ResolvingMetadata } from 'next';
import { getTranslations } from 'next-intl/server';
import { fetchGuestFeedData } from '../../lib/guest-feed-data';
import { generateSealFeedMetadata } from '../../lib/generate-feed-metadata';
-import { fetchGuestSealAnalysisData } from '../../lib/seal-analysis-data';
-import { getLatestDataset } from '../../../../../../screens/Feed/Feed.functions';
+// import { fetchGuestSealAnalysisData } from '../../lib/seal-analysis-data';
+// import { getLatestDataset } from '../../../../../../screens/Feed/Feed.functions';
interface Props {
params: Promise<{ feedDataType: string; feedId: string }>;
@@ -52,55 +52,59 @@ export async function generateMetadata(
export default async function StaticFeedReliabilityPage({
params,
}: Props): Promise {
- const { feedId, feedDataType } = await params;
+ // Since this page is only accessible through a user feature flag
+ // It will temporarily return a not found immediately for any not
+ // authed users
+ notFound();
+ // const { feedId, feedDataType } = await params;
- // Seal of Reliability only exists for GTFS Schedule feeds (see
- // isSealAnalysisApplicable in lib/seal-analysis-data.ts).
- if (feedDataType !== 'gtfs') {
- throw new Error(
- `Seal of Reliability is not available for data type ${feedDataType}`,
- );
- }
+ // // Seal of Reliability only exists for GTFS Schedule feeds (see
+ // // isSealAnalysisApplicable in lib/seal-analysis-data.ts).
+ // if (feedDataType !== 'gtfs') {
+ // throw new Error(
+ // `Seal of Reliability is not available for data type ${feedDataType}`,
+ // );
+ // }
- // Settled rather than all-or-nothing: the two requests fail for unrelated
- // reasons and need unrelated responses. A missing feed is a 404; a seal
- // loader that can't mint a token or reach its cache is a reliability error
- // on a page that does exist.
- const [feedResult, sealResult] = await Promise.allSettled([
- fetchGuestFeedData(feedDataType, feedId),
- fetchGuestSealAnalysisData(feedDataType, feedId),
- ]);
+ // // Settled rather than all-or-nothing: the two requests fail for unrelated
+ // // reasons and need unrelated responses. A missing feed is a 404; a seal
+ // // loader that can't mint a token or reach its cache is a reliability error
+ // // on a page that does exist.
+ // const [feedResult, sealResult] = await Promise.allSettled([
+ // fetchGuestFeedData(feedDataType, feedId),
+ // fetchGuestSealAnalysisData(feedDataType, feedId),
+ // ]);
- if (feedResult.status === 'rejected') {
- // Layout should have caught non-existent feeds, but handle edge case
- console.error(
- `[StaticFeedReliabilityPage] Failed to fetch feed ${feedId}:`,
- feedResult.reason,
- );
- notFound();
- }
+ // if (feedResult.status === 'rejected') {
+ // // Layout should have caught non-existent feeds, but handle edge case
+ // console.error(
+ // `[StaticFeedReliabilityPage] Failed to fetch feed ${feedId}:`,
+ // feedResult.reason,
+ // );
+ // notFound();
+ // }
- // Rethrown as-is so this segment's error.tsx renders the full-page
- // reliability error, and the original cause keeps its stack.
- if (sealResult.status === 'rejected') {
- throw sealResult.reason;
- }
+ // // Rethrown as-is so this segment's error.tsx renders the full-page
+ // // reliability error, and the original cause keeps its stack.
+ // if (sealResult.status === 'rejected') {
+ // throw sealResult.reason;
+ // }
- const sealAnalysis = sealResult.value;
+ // const sealAnalysis = sealResult.value;
- if (sealAnalysis?.reliabilityError === true) {
- throw new Error(
- `Failed to load Seal of Reliability data for feed ${feedId}`,
- );
- }
+ // if (sealAnalysis?.reliabilityError === true) {
+ // throw new Error(
+ // `Failed to load Seal of Reliability data for feed ${feedId}`,
+ // );
+ // }
- const { feed, initialDatasets } = feedResult.value;
+ // const { feed, initialDatasets } = feedResult.value;
- return (
-
- );
+ // return (
+ //
+ // );
}
diff --git a/src/app/[locale]/feeds/components/FeedsScreen.tsx b/src/app/[locale]/feeds/components/FeedsScreen.tsx
index 42189c2b..25f25b19 100644
--- a/src/app/[locale]/feeds/components/FeedsScreen.tsx
+++ b/src/app/[locale]/feeds/components/FeedsScreen.tsx
@@ -646,9 +646,7 @@ export default function FeedsScreen(): React.ReactElement {
{searchView === 'simple' ? (
) : (
)}
diff --git a/src/app/components/Footer.tsx b/src/app/components/Footer.tsx
index c469fdc8..3f5d5ff6 100644
--- a/src/app/components/Footer.tsx
+++ b/src/app/components/Footer.tsx
@@ -9,12 +9,16 @@ import Image from 'next/image';
import { useTranslations } from 'next-intl';
import { FooterLink, FooterColumnTitle } from './FooterElements';
import { useRemoteConfig } from '../context/RemoteConfigProvider';
+import { useUserFeatureFlags } from '../hooks/useUserFeatureFlags';
const Footer: React.FC = () => {
const theme = useTheme();
const { colorScheme } = useColorScheme();
const t = useTranslations('footer');
const { config } = useRemoteConfig();
+ const {
+ flags: { isSealEnabled },
+ } = useUserFeatureFlags();
const FOOTER_COLUMN_WIDTH = '185px';
const SlackSvg = (