Skip to content

Commit b0029b4

Browse files
Merge pull request #99 from MobilityData/copilot/enable-firebase-configs-for-mobilitydata-users
Email bypass: enable all boolean Remote Config flags for @mobilitydata.org users
2 parents 79e8543 + e6861ca commit b0029b4

7 files changed

Lines changed: 433 additions & 37 deletions

File tree

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@ npx firebase use {project_name_or_alias}
8585
npx firebase hosting:channel:deploy {channel_name}
8686
```
8787

88+
# Firebase Remote Configs
89+
90+
Firebase remote configs help us toggle new features on and off. Due to the nature of static pages, there are some nuances. For static pages, the remote configs are called and set at build time and will be the same for the remainder of the static page's cache.
91+
92+
When remote configs change (they rarily do), it is recommended to redeploy the app as that will trigger a new cache for all pages, that will include the updated remote configs
93+
94+
What this also means is that client components will be able to access the firebase remote configs using the Context but server components will have to fetch them each time. This isn't a big deal as the firebase remote configs are cached (for 1 hour on the server)
95+
8896
# Component and E2E tests
8997

9098
Component and E2E tests are executed with [Cypress](https://docs.cypress.io/). Cypress tests are located in the cypress folder.

src/app/context/RemoteConfigProvider.tsx

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
'use client';
22

3-
import React, { createContext, type ReactNode, useContext } from 'react';
3+
import React, {
4+
createContext,
5+
useState,
6+
useEffect,
7+
type ReactNode,
8+
useContext,
9+
} from 'react';
410
import {
511
defaultRemoteConfigValues,
12+
matchesFeatureFlagBypass,
613
type RemoteConfigValues,
714
} from '../interface/RemoteConfig';
15+
import { useAuthSession } from '../components/AuthSessionProvider';
816

917
const RemoteConfigContext = createContext<{
1018
config: RemoteConfigValues;
@@ -17,16 +25,41 @@ interface RemoteConfigProviderProps {
1725
config: RemoteConfigValues;
1826
}
1927

28+
function applyAdminBypass(config: RemoteConfigValues): RemoteConfigValues {
29+
const overridden = { ...config };
30+
for (const key of Object.keys(overridden) as Array<
31+
keyof RemoteConfigValues
32+
>) {
33+
if (typeof overridden[key] === 'boolean') {
34+
(overridden as Record<string, unknown>)[key] = true;
35+
}
36+
}
37+
return overridden;
38+
}
39+
2040
/**
2141
* Client-side Remote Config provider that hydrates server-fetched config into React Context.
22-
* This provider does NOT fetch config - it receives pre-fetched values from the server.
42+
* Applies admin bypass for @mobilitydata.org users after client-side auth resolves,
43+
* which ensures correct flags even on statically generated pages.
2344
*/
2445
export const RemoteConfigProvider = ({
2546
children,
2647
config,
2748
}: RemoteConfigProviderProps): React.ReactElement => {
49+
const { email, isAuthReady } = useAuthSession();
50+
const [effectiveConfig, setEffectiveConfig] = useState(config);
51+
52+
useEffect(() => {
53+
if (!isAuthReady) return;
54+
setEffectiveConfig(
55+
matchesFeatureFlagBypass(email, config.featureFlagBypass)
56+
? applyAdminBypass(config)
57+
: config,
58+
);
59+
}, [email, isAuthReady, config]);
60+
2861
return (
29-
<RemoteConfigContext.Provider value={{ config }}>
62+
<RemoteConfigContext.Provider value={{ config: effectiveConfig }}>
3063
{children}
3164
</RemoteConfigContext.Provider>
3265
);

src/app/interface/RemoteConfig.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,23 @@ export const defaultRemoteConfigValues: RemoteConfigValues = {
4747
enableDetailedCoveredArea: false,
4848
gbfsValidator: false,
4949
};
50+
51+
/**
52+
* Returns true if the given email matches any regex pattern in the
53+
* featureFlagBypass config value (format: `{ "regex": [".+@example.org"] }`).
54+
*/
55+
export function matchesFeatureFlagBypass(
56+
email: string | null | undefined,
57+
featureFlagBypass: string,
58+
): boolean {
59+
if (email == null || email === '' || featureFlagBypass === '') return false;
60+
try {
61+
const parsed = JSON.parse(featureFlagBypass) as { regex?: unknown };
62+
if (!Array.isArray(parsed.regex)) return false;
63+
return (parsed.regex as string[]).some((pattern) =>
64+
new RegExp(pattern).test(email),
65+
);
66+
} catch {
67+
return false;
68+
}
69+
}

src/app/providers.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,13 @@ export function Providers({
4444

4545
return (
4646
<ContextProviders>
47-
<RemoteConfigProvider config={remoteConfig}>
48-
<LocalizationProvider dateAdapter={AdapterDayjs}>
49-
<AuthSessionProvider>{children}</AuthSessionProvider>
50-
</LocalizationProvider>
51-
</RemoteConfigProvider>
47+
<AuthSessionProvider>
48+
<RemoteConfigProvider config={remoteConfig}>
49+
<LocalizationProvider dateAdapter={AdapterDayjs}>
50+
{children}
51+
</LocalizationProvider>
52+
</RemoteConfigProvider>
53+
</AuthSessionProvider>
5254
</ContextProviders>
5355
);
5456
}

src/app/screens/Feed/components/DataQualitySummary.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { WarningContentBox } from '../../../components/WarningContentBox';
77
import { FeedStatusChip } from '../../../components/FeedStatus';
88
import OfficialChip from '../../../components/OfficialChip';
99
import { getTranslations } from 'next-intl/server';
10-
import { getRemoteConfigValues } from '../../../../lib/remote-config.server';
10+
import { getUserRemoteConfigValues } from '../../../../lib/remote-config.server';
1111

1212
export interface DataQualitySummaryProps {
1313
feedStatus: components['schemas']['Feed']['status'];
@@ -24,7 +24,7 @@ export default async function DataQualitySummary({
2424
const [t, tCommon, config] = await Promise.all([
2525
getTranslations('feeds'),
2626
getTranslations('common'),
27-
getRemoteConfigValues(),
27+
getUserRemoteConfigValues(),
2828
]);
2929

3030
return (

0 commit comments

Comments
 (0)