Repository: OpenBeta/open-tacos

Issue:
Add test area somewhere on website landing page
Noticed someone making a test area in Uganda instead of in the designated "test area" in the USA that serves this purpose. 

Might be a good improvement to direct new users to use the test area to learn the site, so we don't end up with "test" boulders in a bunch of random locations. 

Discussion:
@icebelly29: I would like to work on this issue. @mikeschen can you assign this to me?@icebelly29: Hi @mikeschen 

I came across two possible area URLs:

- `/area/18c5dd5c-8186-50b6-8a60-ae2948c548d1/whole-hazelnuts`
- `/area/fedcd484-cf47-5263-ab2a-52dc27c738e0/utah-upload-test`

Could you please confirm which one is the official Test Area we should be linking to from the homepage?@mikeschen: We would want to delete those areas. We would want a "secret" url, one that is not linked to anything, that we can direct devs to. something like: /area/18c5dd5c-8186-50b6-8a60-ae2948c548d1/test-area @mikeschen: > the designated "test area" in the USA that serves this purpose.
@emilyg1709 where is this test area ?

@emilyg1709: @mikeschen @icebelly29 this is the link to the test area where people can play with editing modes and such. Might be helpful if there were a button somewhere on the home page (maybe near latest contributions or something) that says something like "Learn to use the site here" and people can go to the test area where they aren't adding bad beta on existing climbs or places. 

I only added this because I noticed somebody adding areas called test in random places and imo that's not really something we want happening. 

https://openbeta.io/area/18c5dd5c-8186-50b6-8a60-ae2948c548d1/test-area@mikeschen: <img width="307" height="257" alt="Image" src="https://github.com/user-attachments/assets/b6d1d9b3-e5eb-4924-bbdd-e0f390d2b6e9" />

Maybe this , I posted to the Discord channel for opinions.
@mikeschen: Ah ok so we can swap out the banner when a user is logged in:
When not logged in, show this:
```
'use client'
import Cookies from 'js-cookie'
import { useState, useEffect } from 'react'
import { useSession } from 'next-auth/react'
import { SuppressButton } from './SuppressButton'

const STORAGE_KEY = 'suppress-main-banner'

export interface AppAlertProps {
  message: JSX.Element
}

/**
 * Main alert to be displayed under the nav bar.  Users can snooze the alert.
 * @param message alert content
 */
export const AppAlert: React.FC<AppAlertProps> = ({ message }) => {
  const { status } = useSession()
  const [showAlert, setShowAlert] = useState(false)
  useEffect(() => {
    const suppressed = Cookies.get(STORAGE_KEY)
    setShowAlert(suppressed == null)
  }, [])

  // Hide alert if user is logged in
  return showAlert && status !== 'authenticated'
    ? (
      <div className='z-40 w-fit alert alert-info flex flex-wrap justify-center xl:p-4 gap-4'>
        <div className='flex flex-col gap-2 items-start'>
          {message}
        </div>
        <div className='inline-flex w-[210px] grow-1'>
          <SuppressButton
            onClick={() => {
              setShowAlert(false)
              Cookies.set(STORAGE_KEY, '1', { strict: true, expires: 30 })
            }}
          />
        </div>
      </div>
      )
    : null
}

```

when they are logged in show different text asking them to use test area.@mikeschen: **NM, comment below, I have updated on the actual PR**
Something like this:
~~export const AppAlert: React.FC<AppAlertProps> = ({ message, testAreaMessage }) => {~~
  const { status } = useSession()
  const [showAlert, setShowAlert] = useState(false)
  useEffect(() => {
    const suppressed = Cookies.get(STORAGE_KEY)
    setShowAlert(suppressed == null)
  }, [])

  // Hide alert if user is logged in
  return showAlert &&
    (
      <div className='z-40 w-fit alert alert-info flex flex-wrap justify-center xl:p-4 gap-4'>
        <div className='flex flex-col gap-2 items-start'>
          {status !== 'authenticated' ? message : testAreaMessage}
        </div>
        <div className='inline-flex w-[210px] grow-1'>
          <SuppressButton
            onClick={() => {
              setShowAlert(false)
              Cookies.set(STORAGE_KEY, status !== 'authenticated' ? '1' : '2', { strict: true, expires: 30 })
            }}
          />
        </div>
      </div>
    )
}~~


Solution patch:
diff --git a/src/app/(default)/components/LandingHero.tsx b/src/app/(default)/components/LandingHero.tsx
index 9c6b0c8..e22bce0 100644
--- a/src/app/(default)/components/LandingHero.tsx
+++ b/src/app/(default)/components/LandingHero.tsx
@@ -1,20 +1,52 @@
+'use client'
+import { useSession } from 'next-auth/react'
+import Link from 'next/link'
 import { AppAlert } from '@/components/broadcast/AppAlert'
 import { SignupButton } from './DesktopHeader'
 
+const STORAGE_KEY_JOIN_US = 'suppress-join-us-banner'
+const STORAGE_KEY_TEST_AREA = 'suppress-test-area-banner'
+
+const TEST_AREA_ID = process.env.NEXT_PUBLIC_TEST_AREA_ID ?? '18c5dd5c-8186-50b6-8a60-ae2948c548d1'
+const TEST_AREA_URL = `/area/${TEST_AREA_ID}/test-area`
+
 export const LandingHero: React.FC = () => {
-  return (
-    <section className='mt-4'>
+  const { status } = useSession()
+
+  if (status === 'authenticated') {
+    return (
       <AppAlert
+        cookieStorageKey={STORAGE_KEY_TEST_AREA}
         message={
           <>
-            <h1 className='text-xl tracking-tighter font-bold'>Share your climbing route knowledge!</h1>
+            <h1 className='text-xl tracking-tighter font-bold'>Try out adding a route!</h1>
             <div className='font-medium text-neutral/80'>
-              <p>Join us to help improve this comprehensive <br /> climbing resource for the community.</p>
+              <p>
+                You're in the test area — perfect for exploring how everything works.
+                <br />Add a route and see it in action!
+              </p>
             </div>
-            <SignupButton />
+            <Link href={TEST_AREA_URL} className='btn btn-accent border-b-2 border-b-neutral'>
+              Test Area
+            </Link>
           </>
         }
       />
-    </section>
+    )
+  }
+
+  return (
+    <AppAlert
+      cookieStorageKey={STORAGE_KEY_JOIN_US}
+      message={
+        <>
+          <h1 className='text-xl tracking-tighter font-bold'>Share your climbing route knowledge!</h1>
+          <div className='font-medium text-neutral/80'>
+            <p>Join us to help improve this comprehensive <br /> climbing resource for the community.</p>
+          </div>
+          <SignupButton />
+        </>
+      }
+    />
   )
 }
diff --git a/src/app/api/mobile/login/route.ts b/src/app/api/mobile/login/route.ts
index ba48c3e..db1cec8 100644
--- a/src/app/api/mobile/login/route.ts
+++ b/src/app/api/mobile/login/route.ts
@@ -18,7 +18,8 @@ async function postHandler (request: NextRequest): Promise<NextResponse> {
       console.error('Empty username/password!')
       throw new Error('Invalid payload')
     }
-  } catch (error) {
+  } catch (err) {
+    console.error('POST /login error:', err)
     return NextResponse.json({ error: 'Unexpected error' }, { status: 400 })
   }
 
@@ -27,7 +28,7 @@ async function postHandler (request: NextRequest): Promise<NextResponse> {
     response = await auth0Client.oauth.passwordGrant({
       username,
       password,
-      scope: 'openid profile email offline_access',
+      scope: 'offline_access access_token_authz openid email profile read:current_user create:current_user_metadata update:current_user_metadata read:stats update:area_attrs',
       audience: 'https://api.openbeta.io',
       realm: 'Username-Password-Authentication'
     })
diff --git a/src/app/api/mobile/refreshToken/route.ts b/src/app/api/mobile/refreshToken/route.ts
index b516d3b..3f531cd 100644
--- a/src/app/api/mobile/refreshToken/route.ts
+++ b/src/app/api/mobile/refreshToken/route.ts
@@ -28,6 +28,7 @@ async function postHandler (request: NextRequest): Promise<any> {
   try {
     response = await auth0Client.oauth.refreshTokenGrant({
       refresh_token: refreshToken,
+      scope: 'offline_access access_token_authz openid email profile read:current_user create:current_user_metadata update:current_user_metadata read:stats update:area_attrs',
       audience: 'https://api.openbeta.io'
     })
 
diff --git a/src/components/broadcast/AppAlert.tsx b/src/components/broadcast/AppAlert.tsx
index 099b51f..ed4580f 100644
--- a/src/components/broadcast/AppAlert.tsx
+++ b/src/components/broadcast/AppAlert.tsx
@@ -1,43 +1,42 @@
 'use client'
 import Cookies from 'js-cookie'
 import { useState, useEffect } from 'react'
-import { useSession } from 'next-auth/react'
 import { SuppressButton } from './SuppressButton'
 
-const STORAGE_KEY = 'suppress-main-banner'
-
 export interface AppAlertProps {
   message: JSX.Element
+  cookieStorageKey: string
 }
 
 /**
  * Main alert to be displayed under the nav bar.  Users can snooze the alert.
  * @param message alert content
  */
-export const AppAlert: React.FC<AppAlertProps> = ({ message }) => {
-  const { status } = useSession()
+export const AppAlert: React.FC<AppAlertProps> = ({
+  message,
+  cookieStorageKey
+}) => {
   const [showAlert, setShowAlert] = useState(false)
+
   useEffect(() => {
-    const suppressed = Cookies.get(STORAGE_KEY)
+    const suppressed = Cookies.get(cookieStorageKey)
+
     setShowAlert(suppressed == null)
-  }, [])
+  }, [cookieStorageKey])
 
-  // Hide alert if user is logged in
-  return showAlert && status !== 'authenticated'
-    ? (
+  return (
+    showAlert && (
       <div className='z-40 w-fit alert alert-info flex flex-wrap justify-center xl:p-4 gap-4'>
-        <div className='flex flex-col gap-2 items-start'>
-          {message}
-        </div>
+        <div className='flex flex-col gap-2 items-start'>{message}</div>
         <div className='inline-flex w-[210px] grow-1'>
           <SuppressButton
             onClick={() => {
               setShowAlert(false)
-              Cookies.set(STORAGE_KEY, '1', { strict: true, expires: 30 })
+              Cookies.set(cookieStorageKey, '1', { strict: true, expires: 30 })
             }}
           />
         </div>
       </div>
-      )
-    : null
+    )
+  )
 }



Test patch:
diff --git a/src/components/broadcast/__tests__/AppAlert.tsx b/src/components/broadcast/__tests__/AppAlert.tsx
index 6206b97..43807f0 100644
--- a/src/components/broadcast/__tests__/AppAlert.tsx
+++ b/src/components/broadcast/__tests__/AppAlert.tsx
@@ -23,47 +23,52 @@ jest.mock('next-auth/react', () => ({
 
 let AppAlertComponent: React.FC<AppAlertProps>
 
-describe('Banner suppression', () => {
+describe('AppAlert', () => {
   beforeAll(async () => {
     // why async import?  see https://github.com/facebook/jest/issues/10025#issuecomment-716789840
     const module = await import('../AppAlert')
     AppAlertComponent = module.AppAlert
   })
 
-  it('doesn\'t show alert when cookie exists', async () => {
-    // cookie exists
-    cookieGetter.mockReturnValueOnce('foo')
+  beforeEach(() => {
+    cookieGetter.mockClear()
+    cookieSetter.mockClear()
+  })
+
+  it('renders alert with message', () => {
     render(
       <AppAlertComponent
+        cookieStorageKey='test'
         message={
           <div>
             important message
           </div>
-      }
-      />)
+        }
+      />
+    )
 
-    expect(screen.queryAllByRole('button').length).toEqual(0)
-    cookieGetter.mockClear()
+    expect(screen.getByText('important message')).toBeInTheDocument()
+    expect(screen.getByRole('button', { name: /Don't show this again/i })).toBeInTheDocument()
   })
 
-  it('shows alert', async () => {
-    // Clear previous cookie setting if any
-    // cookieGetter.mockClear()
-    // cookieGetter.mockRejectedValueOnce(null)
+  it('sets cookie when suppress button is clicked', async () => {
     const user = userEvent.setup({ skipHover: true })
+
     render(
       <AppAlertComponent
+        cookieStorageKey='test'
         message={
           <div>
             important message 2
           </div>
-      }
-      />)
-    screen.debug()
+        }
+      />
+    )
+
     // click the Suppress button
     await user.click(screen.getByRole('button', { name: /Don't show this again/i }))
 
-    // alert dismissed
-    expect(screen.queryAllByRole('button')).toHaveLength(0)
+    // verify cookie was set
+    expect(cookieSetter).toHaveBeenCalledWith('test', '1', { strict: true, expires: 30 })
   })
 })