Skip to content

Commit 3f4f719

Browse files
committed
use server-cookies for feature-flags
1 parent d563815 commit 3f4f719

File tree

11 files changed

+145
-236
lines changed

11 files changed

+145
-236
lines changed

eslint.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const compat = new FlatCompat({
1212

1313
const eslintConfig = [
1414
{
15-
ignores: ["src/network/generated/**"],
15+
ignores: ["src/network/generated/**", ".next/**"],
1616
},
1717
...compat.extends("next/core-web-vitals", "next/typescript"),
1818
...pluginQuery.configs["flat/recommended"],

package-lock.json

Lines changed: 3 additions & 33 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"dev": "next dev --turbopack",
77
"build": "next build",
88
"start": "next start",
9-
"lint": "next lint",
9+
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --report-unused-disable-directives --max-warnings=0",
1010
"prettier": "prettier --write .",
1111
"type-check": "tsc --noEmit",
1212
"generate:profiel": "openapi-typescript ./dependencies/profiel-service/openapi.yaml -o ./src/network/profiel/generated.ts",
@@ -29,8 +29,7 @@
2929
"react": "^19.0.0",
3030
"react-dom": "^19.2.3",
3131
"tailwind-variants": "^1.0.0",
32-
"zod": "^4.3.5",
33-
"zustand": "^5.0.11"
32+
"zod": "^4.3.5"
3433
},
3534
"devDependencies": {
3635
"@eslint/eslintrc": "^3",

src/app/actions.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"use server";
2+
3+
import {
4+
defaultFlags,
5+
FeatureFlags,
6+
featureFlagsSchema,
7+
} from "@/app/instellingen/_featureFlags";
8+
import { cookies } from "next/headers";
9+
10+
// We can't use the useCookie hook here, because it's meant for client-side usage. Instead, we directly interact with the cookies API provided by Next.js in server actions.
11+
export async function setFeatureFlagsCookie(flags: FeatureFlags) {
12+
// Validate with Zod
13+
const result = featureFlagsSchema.safeParse(flags);
14+
if (!result.success) {
15+
throw new Error(
16+
"Invalid feature flags: " + JSON.stringify(result.error.format()),
17+
);
18+
}
19+
20+
const cookiesStore = await cookies();
21+
cookiesStore.set("flags", JSON.stringify(flags), {
22+
path: "/",
23+
httpOnly: true,
24+
});
25+
}
26+
27+
export const getFlagsFromServerCookie = async () => {
28+
const cookieStore = await cookies();
29+
const flagsCookie = cookieStore.get("flags")?.value;
30+
31+
let flags = {} as FeatureFlags;
32+
if (flagsCookie) {
33+
try {
34+
flags = JSON.parse(decodeURIComponent(flagsCookie));
35+
const result = featureFlagsSchema.safeParse(flags);
36+
if (!result.success) {
37+
flags = defaultFlags;
38+
}
39+
} catch {
40+
flags = defaultFlags;
41+
}
42+
}
43+
return flags;
44+
};

src/app/contactgegevens/[type]/_contactEditBox.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export const ContactEditBox = ({
7878
// setStatus("available");
7979
},
8080
onError: (error: Error) => {
81-
console.log(error);
81+
console.error(error);
8282
},
8383
},
8484
);
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { z } from "zod";
2+
3+
// Define the schema with explicit keys
4+
export const featureFlagsSchema = z.object({
5+
feature_MijnZaken: z.boolean(),
6+
feature_MijnTaken: z.boolean(),
7+
feature_MijnProducten: z.boolean(),
8+
feature_RegelRecht: z.boolean(),
9+
});
10+
11+
export type FeatureFlags = z.infer<typeof featureFlagsSchema>;
12+
export type FeatureFlagKey = keyof FeatureFlags;
13+
14+
export const defaultFlags: FeatureFlags = {
15+
feature_MijnZaken: false,
16+
feature_MijnTaken: false,
17+
feature_MijnProducten: false,
18+
feature_RegelRecht: false,
19+
};
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"use client";
2+
3+
import {
4+
defaultFlags,
5+
FeatureFlagKey,
6+
FeatureFlags,
7+
} from "@/app/instellingen/_featureFlags";
8+
import { setFeatureFlagsCookie } from "../actions";
9+
10+
export const ToggleFeature = ({
11+
flags,
12+
featureLabel,
13+
featureName,
14+
}: {
15+
flags: FeatureFlags;
16+
featureLabel: string;
17+
featureName: FeatureFlagKey;
18+
}) => {
19+
const isToggled = flags[featureName];
20+
21+
const handleToggle = async () => {
22+
setFeatureFlagsCookie({
23+
...defaultFlags,
24+
...flags,
25+
[featureName]: !flags[featureName], // toggle
26+
});
27+
};
28+
29+
return (
30+
<div className="flex items-center gap-3">
31+
<label className="text-base font-medium">{featureLabel}</label>
32+
33+
<div
34+
className={`flex h-6 w-12 cursor-pointer items-center rounded-full border border-gray-300 shadow-2xl transition-all duration-100 ${
35+
isToggled ? "bg-blue-100" : "bg-gray-100"
36+
}`}
37+
onClick={handleToggle}
38+
role="switch"
39+
aria-checked={isToggled}
40+
tabIndex={0}
41+
onKeyDown={(e) => {
42+
if (e.key === "Enter" || e.key === " ") handleToggle();
43+
}}
44+
>
45+
<div
46+
className={`flex h-6 w-6 transform items-center justify-center rounded-full shadow-lg transition-transform duration-400 ${
47+
isToggled
48+
? "bg-primary translate-x-[22px]"
49+
: "translate-x-0 bg-gray-600"
50+
}`}
51+
/>
52+
</div>
53+
</div>
54+
);
55+
};

src/app/instellingen/page.tsx

Lines changed: 9 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
"use client";
21
import Card from "@/components/card";
3-
import {
4-
useFeatureFlagsStore,
5-
useHydratedFeatureFlags,
6-
type FeatureFlagKey,
7-
} from "@/stores/featureFlags";
2+
import { getFlagsFromServerCookie } from "../actions";
3+
import { ToggleFeature } from "./_toggleFeature";
4+
5+
const Page = async () => {
6+
const flags = await getFlagsFromServerCookie();
87

9-
const Page = () => {
108
return (
119
<>
1210
<h1 className="text-4xl">Instellingen</h1>
@@ -21,18 +19,22 @@ const Page = () => {
2119

2220
<div className="space-y-5">
2321
<ToggleFeature
22+
flags={flags}
2423
featureLabel={"Mijn Zaken"}
2524
featureName="feature_MijnZaken"
2625
/>
2726
<ToggleFeature
27+
flags={flags}
2828
featureLabel={"Mijn Taken"}
2929
featureName="feature_MijnTaken"
3030
/>
3131
<ToggleFeature
32+
flags={flags}
3233
featureLabel={"Mijn Producten"}
3334
featureName="feature_MijnProducten"
3435
/>
3536
<ToggleFeature
37+
flags={flags}
3638
featureLabel={"RegelRecht"}
3739
featureName="feature_RegelRecht"
3840
/>
@@ -42,48 +44,4 @@ const Page = () => {
4244
);
4345
};
4446

45-
const ToggleFeature = ({
46-
featureLabel,
47-
featureName,
48-
}: {
49-
featureLabel: string;
50-
featureName: FeatureFlagKey;
51-
}) => {
52-
const isHydrated = useHydratedFeatureFlags();
53-
const isToggled = useFeatureFlagsStore((s) => s.flags[featureName]);
54-
console.log(isToggled);
55-
const toggleFlag = useFeatureFlagsStore((s) => s.toggleFlag);
56-
57-
const handleToggle = () => toggleFlag(featureName);
58-
59-
if (!isHydrated) return null;
60-
61-
return (
62-
<div className="flex items-center gap-3">
63-
<label className="text-base font-medium">{featureLabel}</label>
64-
65-
<div
66-
className={`flex h-6 w-12 cursor-pointer items-center rounded-full border border-gray-300 shadow-2xl transition-all duration-100 ${
67-
isToggled ? "bg-blue-100" : "bg-gray-100"
68-
}`}
69-
onClick={handleToggle}
70-
role="switch"
71-
aria-checked={isToggled}
72-
tabIndex={0}
73-
onKeyDown={(e) => {
74-
if (e.key === "Enter" || e.key === " ") handleToggle();
75-
}}
76-
>
77-
<div
78-
className={`flex h-6 w-6 transform items-center justify-center rounded-full shadow-lg transition-transform duration-400 ${
79-
isToggled
80-
? "bg-primary translate-x-[22px]"
81-
: "translate-x-0 bg-gray-600"
82-
}`}
83-
/>
84-
</div>
85-
</div>
86-
);
87-
};
88-
8947
export default Page;

src/app/layout.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { getKvkFromCookie, getKvkOptionsFromCookie } from "@/utils/kvknummer";
88
import { Footer } from "@/layouts/footer";
99
import Breadcrumb from "@/layouts/breadcrumb";
1010
import PublicRootLayout from "./publiclayout";
11+
import { getFlagsFromServerCookie } from "./actions";
1112

1213
export const metadata: Metadata = {
1314
title: "Mijn overheid zakelijk",
@@ -19,12 +20,13 @@ export default async function RootLayout({
1920
}: Readonly<{
2021
children: React.ReactNode;
2122
}>) {
23+
const flags = await getFlagsFromServerCookie();
2224
const session = await auth();
2325
const kvk = await getKvkFromCookie();
2426
const kvkOpties = await getKvkOptionsFromCookie();
2527

2628
return (
27-
<html lang="en">
29+
<html lang="en" className="">
2830
<body className="bg-[#fafafa]">
2931
{!session ? (
3032
<PublicRootLayout />
@@ -35,7 +37,7 @@ export default async function RootLayout({
3537
<div className="container mx-auto py-1.5">
3638
<div className="grid max-w-screen-xl grid-cols-[288px_1fr_1fr_1fr] justify-between gap-3">
3739
<div className="hidden md:col-span-1 md:block">
38-
<Navigation />
40+
<Navigation flags={flags} />
3941
</div>
4042
<div className="col-span-4 md:col-span-3 md:pt-[9px]">
4143
<Breadcrumb />

0 commit comments

Comments
 (0)