Skip to content

feat: update Next.js app router example (please close in favor of updated PR #14) #13

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Draft
7 changes: 7 additions & 0 deletions nextjs-approuter/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# This is the Flagsmith environment ID used in the example from YouTube.
# You can change it to a different ID for testing different flags in your
# own account.
#
# If following the blog tutorial, copy this file to a new file ".env" and update
# with the server-side key you create from your own account.
FLAGSMITH_ENVIRONMENT_ID=5zsj2BaedF6BcBHXLNGqUj
1 change: 1 addition & 0 deletions nextjs-approuter/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ yarn-error.log*

# local env files
.env*.local
.env

# vercel
.vercel
Expand Down
2 changes: 1 addition & 1 deletion nextjs-approuter/.nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v18.17.0
v22.14.0
19 changes: 15 additions & 4 deletions nextjs-approuter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,32 @@

## Next.js and Flagsmith Tutorial

The repository for the [live stream tutorial](https://www.youtube.com/watch?v=u9TjbtZX4Zg) with [@eddiejaoude](https://twitter.com/eddiejaoude).
The repository was originally for the [live stream tutorial](https://www.youtube.com/watch?v=u9TjbtZX4Zg) with [@eddiejaoude](https://twitter.com/eddiejaoude), and has since been updated with the latest dependencies.

## Getting started

### Setup Flagsmith

**Install**
First, set up Flagsmith and add two feature flags. These flags are documented in `app/lib/flags.ts`.

### Setup `.env`

Copy `.env.example` to `.env` and use your Flagsmith server-only key.

### Install Dependencies

```
npm i
```

**Run**
### Run the App

```
npm run dev
```

### (Optional) Build and Run


```
npm run build && npm run start
```
20 changes: 14 additions & 6 deletions nextjs-approuter/app/api/login/route.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
'use server'

import { cookies } from 'next/headers'
import { LoginRequest } from '@/app/types'
import { NextResponse } from 'next/server'
import { cookies } from 'next/headers'

export async function POST(request: Request) {
const body: LoginRequest = await request.json()
//Mock a user id by formatting the email address
const id = body.email.split('@').join('_').replace(/\./, '_')
// Create a mock user object and store it in cookie

const { email } = body

// Mock a user id by formatting the email address.
const id = email.split('@').join('_').replace(/\./, '_')

// Create a mock user object and store it in cookie.
const userJSON = {
id,
email: body.email,
email,
}

const user = JSON.stringify(userJSON)
cookies().set('user', user)

const cookieStore = await cookies()
cookieStore.set('user', user)

return NextResponse.json(userJSON)
}
5 changes: 4 additions & 1 deletion nextjs-approuter/app/api/logout/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { cookies } from 'next/headers'

export async function POST() {
// Mock login API
cookies().delete('user')
const cookieStore = await cookies()

cookieStore.delete('user')

return new Response('{}', {
status: 200,
})
Expand Down
34 changes: 34 additions & 0 deletions nextjs-approuter/app/beta/BetaPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use client'

import { HomePageLink } from './components/HomePageLink'
import WelcomeMessage from '../components/WelcomeMessage'

/**
* This page is a client component that displays a welcome message to beta users.
*
* @returns {React.ReactNode}
*/
export const BetaPage = () => {
return (
<main className='main'>
<div>
<h1>Welcome to the Beta! 🎉</h1>
<p>
This page is restricted to beta users only.
<br />
Looks like you have access!
<br />
<br />
</p>
{/**
* Since this is a client component, we can include the WelcomeMessage
* component here. Otherwise, we would encounter an error because the
* WelcomeMessage component's use of the useFlags hook is a client-side
* hook.
*/}
<WelcomeMessage />
<HomePageLink />
</div>
</main>
)
}
20 changes: 20 additions & 0 deletions nextjs-approuter/app/beta/RestrictedPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { HomePageLink } from './components/HomePageLink'

/**
* This page is a server component that displays a message to non-beta users.
* Since there's no need for client-side interactivity, we can simply return
* the HTML.
*
* @returns {React.ReactNode}
*/
export const RestrictedPage = () => {
return (
<main className='main'>
<div>
<h1>Restricted Beta Page</h1>
<p>You are not a beta user.</p>
<HomePageLink />
</div>
</main>
)
}
20 changes: 20 additions & 0 deletions nextjs-approuter/app/beta/UnauthorizedPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { HomePageLink } from './components/HomePageLink'

/**
* This page is a server component that displays a message to non-logged in users.
* Since there's no need for client-side interactivity, we can simply return
* the HTML.
*
* @returns {React.ReactNode}
*/
export const UnauthorizedPage = () => {
return (
<main className='main'>
<div>
<h1>Restricted Beta Page</h1>
<p>Must be logged in to access this page.</p>
<HomePageLink />
</div>
</main>
)
}
7 changes: 7 additions & 0 deletions nextjs-approuter/app/beta/components/HomePageLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Link from 'next/link'

export const HomePageLink = () => (
<p className='mt-4'>
<Link href='/'>Click here to go back to the home page.</Link>
</p>
)
38 changes: 38 additions & 0 deletions nextjs-approuter/app/beta/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { BetaPage } from './BetaPage'
import { RestrictedPage } from './RestrictedPage'
import { UnauthorizedPage } from './UnauthorizedPage'
import { getDefaultUser } from '@/app/utils/getDefaultUser'
import { isFeatureEnabledForUser } from '@/app/lib/flagsmith'

/**
* This page is a server component that checks the user's login status and
* serves the appropriate page based on their status. Each page can have its
* own server and client components. Since the React Context API is not available
* on the server, we need to use the server component to check the user's
* login status instead of the context provider defined in the layout.tsx file.
*
* The general way to determine which way to check feature flags is:
*
* - Rendering the page on the server? Use the checkFeatureFlag() or associated helper.
* - Dynamically changing the UI based on the feature flag? Use the useFlags() hook.
*
* @returns {Promise<React.ReactNode>}
*/
export default async function RestrictedBeta() {
const defaultUser = await getDefaultUser()

// Unauthorized users are served the UnauthorizedPage because
// they are not logged in.
if (!defaultUser) return <UnauthorizedPage />

// Check if the user is a beta user.
const isBetaUser = await isFeatureEnabledForUser('beta_users', defaultUser)

// Regular users are served the RestrictedPage because
// they are not beta users.
if (!isBetaUser) return <RestrictedPage />

// Beta users are served the BetaPage because
// they are beta users.
return <BetaPage />
}
5 changes: 4 additions & 1 deletion nextjs-approuter/app/components/FeatureFlagProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
'use client'

import { FC, ReactNode, useRef } from 'react'
import { IState } from 'flagsmith/types'

import { FlagsmithProvider } from 'flagsmith/react'
import { IState } from 'flagsmith/types'
import { createFlagsmithInstance } from 'flagsmith/isomorphic'

type FeatureFlagProviderType = {
Expand All @@ -14,6 +16,7 @@ const FeatureFlagProvider: FC<FeatureFlagProviderType> = ({
children,
}) => {
const flagsmithInstance = useRef(createFlagsmithInstance())

return (
<FlagsmithProvider
flagsmith={flagsmithInstance.current}
Expand Down
18 changes: 15 additions & 3 deletions nextjs-approuter/app/components/Nav.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
'use client'

import React, { ChangeEvent, FC, FormEvent, useState } from 'react'

import { User } from '@/app/types'
import useUser from '@/app/hooks/useUser'
import { useRouter } from 'next/navigation'
import { useUser } from '@/app/hooks/useUser'

const LoginForm: FC<{ defaultUser: User | undefined }> = ({ defaultUser }) => {
// Router is used to refresh the page after a login or logout.
const router = useRouter()

const { login, logout, user } = useUser(defaultUser)

const [formData, setFormData] = useState({
email: '',
password: '',
})

// Disable the login button if the form is not filled out.
const disableLogin = !formData.email || !formData.password

const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
Expand All @@ -23,12 +31,16 @@ const LoginForm: FC<{ defaultUser: User | undefined }> = ({ defaultUser }) => {
const handleLogin = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (!disableLogin) {
login(formData)
login(formData).then(() => {
router.refresh()
})
}
}

const handleLogout = () => {
logout()
logout().then(() => {
router.refresh()
})
}

return (
Expand Down
2 changes: 2 additions & 0 deletions nextjs-approuter/app/components/WelcomeMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ type WelcomeMessageType = {}

const WelcomeMessage: FC<WelcomeMessageType> = ({}) => {
const { welcome_message } = useFlags(['welcome_message'])

if (!welcome_message.enabled) {
return null
}

return (
<div className='border border-1 rounded border-secondary p-2'>
<code>{welcome_message.value}</code>
Expand Down
53 changes: 34 additions & 19 deletions nextjs-approuter/app/hooks/useUser.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,55 @@
import { useCallback, useState } from 'react'

import { User } from '@/app/types'
import { getTraits } from '@/app/utils/getTraits'
import { useFlagsmith } from 'flagsmith/react'
import getTraits from '@/app/utils/getTraits'

export interface LoginRequest {
email: string
password: string
}
export default function (defaultUser: User | null = null) {

export function useUser(defaultUser: User | null = null) {
const [user, setUser] = useState(defaultUser)
const flagsmith = useFlagsmith()
const login = useCallback((data: LoginRequest) => {
return fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
.then((res) => res.json())
.then((res: User | null) => {
if (res) {

const login = useCallback(
(data: LoginRequest) => {
return fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
.then((res) => res.json())
.then((res: User | { error: string } | null) => {
console.log('login', { res })
if (!res) return

if ('error' in res) {
throw new Error(res.error)
}

flagsmith.identify(res.id, getTraits(res))
setUser(res)
}
})
}, [])
})
.catch((err) => {
alert(err.message)
})
},
[flagsmith],
)

const logout = useCallback(() => {
setUser(null)
flagsmith.logout()
return fetch('/api/logout', {
method: 'POST',
body: '{}',
}).then(() => {
setUser(null)
flagsmith.logout()
})
}, [])
}, [flagsmith])

return { login, logout, user }
}
Loading