Chrome Extension MV3 Template : Supabase Auth, Plasmo, Tailwinds CSS & Shadcn UI
Tags: webdev, extensions, tailwindcss, javascript
Published: 2023-08-13 23:50:02

# Setting Up Plasmo, Tailwinds & Shadcn-UI

### Setting Up Plasmo

```jsx
pnpm create plasmo
# OR
yarn create plasmo
# OR
npm create plasmo
```

Get started with plasmo with the base command below, stick to pnpm in this guide as that’s what we’re going to use.   

[https://docs.plasmo.com/framework](https://docs.plasmo.com/framework)

### Adding Tailwinds CSS Manually — Can be Skipped

```jsx
pnpm create plasmo --with-tailwindcss
```

You can jump past setting up tailwinds by using the command above but if you want to know how to manually add tailwinds css to your plasmo project, continue reading on.  The link below is also another good reference for this.  

[https://docs.plasmo.com/quickstarts/with-tailwindcss](https://docs.plasmo.com/quickstarts/with-tailwindcss)

```jsx
pnpm i -D tailwindcss postcss autoprefixer
```

```jsx
npx tailwindcss init
```

 With your project set up using the `create plasmo` command we can get started with setting up tailwinds css.  

```jsx
/**
 * @type {import('postcss').ProcessOptions}
 */
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {}
  }
}
```

Create a file called `postcss.config.js` Paste the code above into that file.  

```jsx
@tailwind base;
@tailwind components;
@tailwind utilities;
```

Create a file called `style.css`, this is equivalent to a `globals.css` file that you’ll commonly find in other tailwind projects.  

Create a `src` folder and place the `popup.tsx` and `style.css` files into it 

Change the path in the `tsconfig.ts` file to the source folder for the import alias `~`. 

```jsx
{
  "extends": "plasmo/templates/tsconfig.base",
  "exclude": [
    "node_modules"
  ],
  "include": [
    ".plasmo/index.d.ts",
    "./**/*.ts",
    "./**/*.tsx"
  ],
  **"compilerOptions": {
    "paths": {
      "~*": [
        "./src/*"
      ]
    },**
    "baseUrl": "."
  }
}
```

### Setting Up Shadcn-UI

Now that tailwinds has been added we’re going to add Shadcn-UI via the cli.  Follow along or use the guide below as a reference.

[https://ui.shadcn.com/docs/installation/next](https://ui.shadcn.com/docs/installation/next)

```jsx
pnpm dlx shadcn-ui@latest init
```

run this command


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wiqrq9rjtk3x275qbhbb.png)

Select the following options shown in the screenshot. 

Point the `globals.css` requirement to the `style.css` file located inside the `src` directory.

Use the import alias `~` for importing components within the `src` directory, this is optional but recommended.  Do the same for the `lib/util`, make sure **NOT** to do `~~lib/utils.ts~~` as it automatically adds the `ts` file ending.  

### How to use tailwinds in the `popup.tsx` file

You must import the style.css, use the import alias that we set up 

```jsx
import "~style.css"
```

# Supabase Auth for MV3 Chrome Extension

### Install SupabaseJS client

```jsx
pnpm add @supabase/supabase-js
```

### Introduction to Auth in a Chrome Extension

Authentication is essentially a JWT that gets stored in the form of an access token and refresh token.  When the access token expires, the refresh token gets exchanged for a new access token.  In the next section, we talk about how access tokens and refresh tokens get accessed and stored in a chrome MV3 chrome extension.  

### Access Token & Refresh Token Storage — Three Methods

There are three methods we’ll cover regarding how to store and access and a refresh tokens.  The first method leverages the `chrome.storage` api without modifying any of the options in the supabase `createClient` function.  Basically you manually create the logic verifying whether the token has expired, something that the `createClient` functionpart of the supabaseJS library typically does in the background of a webapp.  This “manual” method can work with any api that delivers you an access token & refresh token.  The second method modifies the supabase `createClient` function to leverage the `chrome.storage` api in the background.  The third method points that storage to the plasmo storage api, working only in the plasmo framework.  

Method one will be referred to as `supabaseManual`, method two will be referred to as `supabaseAuto` and the third method will be referred to as `supabasePlasmo`.  

### Setting Up the Supabase Client

```jsx
import { createClient } from "@supabase/supabase-js"

// this is the default for method 1, we leave all the options on by default
export const supabaseManualStorage = createClient(
  process.env.PLASMO_PUBLIC_SUPABASE_URL,
  process.env.PLASMO_PUBLIC_SUPABASE_KEY
)

// this options is for method 2, method 3 also has it's own custom options
const optionsForMethod2 = {
  auth: {
      autoRefreshToken: true,
      persistSession: true,
      detectSessionInUrl: true,
      storage: {
          async getItem(key: string): Promise<string | null> {
              // @ts-ignore
              const storage = await chrome.storage.local.get(key);
              return storage?.[key];
          },
          async setItem(key: string, value: string): Promise<void> {
              // @ts-ignore
              await chrome.storage.local.set({
                  [key]: JSON.parse(value)
              });
          },
          async removeItem(key: string): Promise<void> {
              // @ts-ignore
              await chrome.storage.local.remove(key);
          }
      }
  }
}

export const supabaseAuto = createClient(
  process.env.PLASMO_PUBLIC_SUPABASE_URL,
  process.env.PLASMO_PUBLIC_SUPABASE_KEY,
  optionsForMethod2
)
```

For the case of this example, we have two different supabase variables to export, one in the form of `supabaseManual` and `supabaseAuto` the former being used for the manual storage method and the ladder being used for modifying the storage variable in supabase `createClient()` config to use either `chrome.storage` or the plasmo storage api.  In a typical working project the variable should just be called `supabase` not `supabaseAuto` or `supabaseManual` but for this demo we’re doing it for clarification. 

The rest of the `supabaseManual` functionality gets explained in the section covering the BGSW in the `background.ts` file.  

The inspiration for `supabaseAuto` came from this github thread below.  

[https://gist.github.com/lcmchris/da979cbbf56c9452b6e5847ece7ee6ca](https://gist.github.com/lcmchris/da979cbbf56c9452b6e5847ece7ee6ca)

In `optionsForMethod2` a function was created for each of the methods used in the `chrome.storage` api, `getItem`, `setItem` and `removeItem`.  

### Why `getItem`, `setItem` and `removeItem` ?


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5o4ikprl17bwbc6teoqw.png)

The method below only works if you have the library installed on your local device, I was not able to find these types in the posted github repo below.    

[https://github.com/supabase/supabase-js/blob/master/src/SupabaseClient.ts](https://github.com/supabase/supabase-js/blob/master/src/SupabaseClient.ts)

![Path](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8plxf0ir7xvw6auyze26.png)
Go to the `@supabase/supabase-js` folder in the `node_modules` folder, go to `types.ts`

`node_modules` → `@supabase/supabase-js` → `src` → `lib` → `types.ts`

![GoTrueClient](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l3nqkyeyx3t0p7xctlhl.png)
Command + click (control + click on Windows) on `GoTrueClient` imported function

![Supported Storage](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/86q9gdhokoexlt4v8tup.png)
Command + click (control + click on Windows) on `SupportedStorage`

![GetItem, RemoveItem, SetItem](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qri4093am7n1xxqg38ul.png)
That should take you to the highlighted line.  

### Plasmo Storage API Method

The plasmo storage api method is the easiest solution but limited only to the plasmo extension framework.  Use the link below as a reference, however, the instructions are less clear.  

[https://docs.plasmo.com/quickstarts/with-supabase](https://docs.plasmo.com/quickstarts/with-supabase)

```jsx
pnpm add @plasmohq/storage
```

Add the plasmo storage api to your project  

```jsx
import { createClient } from "@supabase/supabase-js"
import { Storage } from "@plasmohq/storage"
 
const storage = new Storage({
  area: "local"
})

const options = {
	auth: {
		storage,
    autoRefreshToken: true,
    persistSession: true,
    detectSessionInUrl: true
	}
}

// this is for method 2 & 3
export const supabasePlasmo = createClient(
  process.env.PLASMO_PUBLIC_SUPABASE_URL,
  process.env.PLASMO_PUBLIC_SUPABASE_KEY, 
	options
)
```

Create a variable called `storage` that bridges to the plasmo storage api and that should be all.  There’s still some more setup for the `popup.tsx` page but that will be covered in the following sections.  

# Background Service Worker BGSW — `Background.ts`

### Where to put `background.ts`?

The `background.ts` file can be put in a folder called `background` named as `index.ts` in the background folder.  The reason why you might want to use the folder method is that you can nest a messaging folder within the background folder.  Otherwise, you can just stick to `background.ts` inside the `src` folder not inside a background folder.    

### Adding & Removing Access Tokens Manually

In the `supabaseManual` method there a few base operations for accessing and modifying access tokens, `getSupabaseKeys()` , `validateToken()` , `getKeyFromStorage()` , `setKeyInStorage()` and `removeKeysFromStorage()` .  Point to the accessToken inside an object variable that stores the string value for the specific `chrome.storage` location.  While technically this is not required trying to remember the exact string each time will get tedious.  

```jsx
// init chrome storage keys
const chromeStorageKeys = {
  supabaseAccessToken: "supabaseAccessToken",
  supabaseRefreshToken: "supabaseRefreshToken",
  supabaseUserData: "supabaseUserData",
  supabaseExpiration: "supabaseExpiration",
  supabaseUserId: "supabaseUserId"
}
```

You can add any number of desired properties to the `chromeStorageKeys` object as one sees fit for your needs.  

```jsx
// get the supabase keys
async function getSupabaseKeys() {
  const supabaseAccessToken = await getKeyFromStorage(
    chromeStorageKeys.supabaseAccessToken
  )
  const supabaseExpiration = (await getKeyFromStorage(
    chromeStorageKeys.supabaseExpiration
  )) as number
  const userId = await getKeyFromStorage(chromeStorageKeys.supabaseUserId)

  return { supabaseAccessToken, supabaseExpiration, userId }
}
```

The `getSupabaseKeys()` function is basically an intermediary for the more basic `getKeyFromStorage()` method.

```jsx
// get the key from storage
async function getKeyFromStorage(key) {
  return new Promise((resolve, reject) => {
    chrome.storage.local.get(key, (result) => {
      if (chrome.runtime.lastError) {
        reject(chrome.runtime.lastError)
      } else {
        resolve(result[key])
      }
    })
  })
}
```

The `getKeyFromStorage()` method puts the `chrome.storage.local.get` promise into an async function, it’s quite limited in its functionality but we abstract away everything else in the `getSupabaseKeys()` function.  

```tsx
//setting keys in local storage
async function setKeyInStorage(
  keyValuePairs: Record<string, any>
): Promise<void> {
  return new Promise<void>((resolve, reject) => {
    chrome.storage.local.set(keyValuePairs, () => {
      if (chrome.runtime.lastError) {
        reject(chrome.runtime.lastError)
      } else {
        resolve()
      }
    })
  })
}
```

The `setKeyInStorage()` basically uploads the key into storage since there’s no “update” functionality when using `chrome.storage` api.  

```tsx
//removing keys from local storage
async function removeKeysFromStorage(keys: string[]): Promise<void> {
  return new Promise<void>((resolve, reject) => {
    chrome.storage.local.remove(keys, () => {
      if (chrome.runtime.lastError) {
        reject(chrome.runtime.lastError)
      } else {
        resolve()
      }
    })
  })
}
```

The `removeKeysFromStorage()` comes in handy when the user signs out and or their access token expired, thus requiring the user to sign back in again.  

With the `getSupabaseKeys` method returning a `supabaseAccessToken`, `supabaseExpiration` and `userId`, the next step can be taken for validating a request. 

```jsx
async uploadFunction() {
	const { supabaseAccessToken, supabaseExpiration, userId } = await getSupabaseKeys()
  await validateToken(supabaseAccessToken, supabaseExpiration)

	// do something here
	// either send a message to the bgsw or upload data to the server
}
```

The code block above is a template for an example function, where the accessToken and expiration time is being outputted from the `getSupabaseKeys()` function into the `validateToken()` function.  With the validated access token a request can be sent back to the server and or a message sent to the BGSW.  

```jsx
// validate the token
async function validateToken(supabaseAccessToken, supabaseExpiration) {
  const currentTime = Math.floor(Date.now() / 1000)
  if (!supabaseAccessToken) {
    throw new Error("No Supabase access token found")
  }
  if (currentTime > supabaseExpiration) {
    handleMessage({ action: "refresh", value: null })
    throw new Error("Supabase access token is expired")
  }
}
```

This is the `validateToken()` function, that gets leveraged in the `handleMessage()` function for the `supabaseManual` method. 

### Message Handlers for `supabaseAuto` & `supabaseManual`

```tsx
// the event listener
chrome.runtime.onMessage.addListener((message, sender, response) => {
//   handleMessageManual(message, sender, response)
  handleMessageAutomatic(message, sender, response)
  return true
})
```

A `chrome.runtime.onMessage` event listener needs to be running in the background, where the appropriate `handleMessage()` function will be active contingent on whether you’re using the `supabaseAuto` or `supabaseManual` method.  The `supabasePlasmo` method does not use messaging for authentication between the `popup.tsx` and BGSW, thus it has no message handler function.  

### `handleMessageManual()` for `supabaseManual`

```tsx
// handle message functionality for manual keys
async function handleMessageManual({ action, value }, sender?, response?) {
  if (action === "signin") {
    console.log("requesting auth")

    const { data, error } = await supabase.auth.signInWithPassword(value)
    if (data && data.session) {
      await setKeyInStorage({
        [chromeStorageKeys.supabaseAccessToken]: data.session.access_token,
        [chromeStorageKeys.supabaseRefreshToken]: data.session.refresh_token,
        [chromeStorageKeys.supabaseUserData]: data.user,
        [chromeStorageKeys.supabaseExpiration]: data.session.expires_at,
        [chromeStorageKeys.supabaseUserId]: data.user.id
      })
      console.log("User data stored in chrome.storage.sync")
      response({ data, error })
    } else {
      console.log("failed login attempt", error)
      response({ data: null, error: error })
    }
  } else if (action === "signup") {
    const { data, error } = await supabase.auth.signUp(value)
    if (data) {
      await setKeyInStorage({
        [chromeStorageKeys.supabaseAccessToken]: data.session.access_token,
        [chromeStorageKeys.supabaseRefreshToken]: data.session.refresh_token,
        [chromeStorageKeys.supabaseUserData]: data.user,
        [chromeStorageKeys.supabaseExpiration]: data.session.expires_at,
        [chromeStorageKeys.supabaseUserId]: data.user.id
      })
      console.log("User data stored in chrome.storage.sync")
      response({ message: "Successfully signed up!", data: data })
    } else {
      response({ data: null, error: error?.message || "Signup failed" })
    }
  } else if (action === "signout") {
    const { error } = await supabase.auth.signOut()
    if (!error) {
      await removeKeysFromStorage([
        chromeStorageKeys.supabaseAccessToken,
        chromeStorageKeys.supabaseRefreshToken,
        chromeStorageKeys.supabaseUserData,
        chromeStorageKeys.supabaseExpiration,
        chromeStorageKeys.supabaseUserId
      ])
      console.log("User data removed from chrome.storage.sync")
      response({ message: "Successfully signed out!" })
    } else {
      response({ error: error?.message || "Signout failed" })
    }
  } else if (action === "refresh") {
    const refreshToken = (await getKeyFromStorage(
      chromeStorageKeys.supabaseRefreshToken
    )) as string
    if (refreshToken) {
      const { data, error } = await supabase.auth.refreshSession({
        refresh_token: refreshToken
      })

      if (error) {
        response({ data: null, error: error.message })
        return
      }

      console.log("token data", data)

      if (!data || !data.session || !data.user) {
        await handleMessageManual(
          { action: "signout", value: null },
          sender,
          console.log
        )
        response({
          data: null,
          error: "Session expired. Please log in again."
        })
      } else {
        await setKeyInStorage({
          [chromeStorageKeys.supabaseAccessToken]: data.session.access_token,
          [chromeStorageKeys.supabaseRefreshToken]: data.session.refresh_token,
          [chromeStorageKeys.supabaseUserData]: data.user,
          [chromeStorageKeys.supabaseExpiration]: data.session.expires_at,
          [chromeStorageKeys.supabaseUserId]: data.user.id
        })

        console.log("User data refreshed in chrome.storage.sync")
        response({ data: data })
      }
    } else {
      response({ data: null, error: "No refresh token available" })
    }
  }
}
```

The BGSW is the bridge between the accessTokens located in the `chrome.storage` api and the `popup.tsx` file.  It covers the base operations of signin, signout, signup, and refresh.    

### `handleMessageAutomatic()` for `supabaseAuto`

```tsx
// handler for automatic messages
async function handleMessageAutomatic({ action, value }, sender?, response?) {
    if (action === "signin") {

        const { data, error } = await supabaseAuto.auth.signInWithPassword(value);

        if (data && data.session) {
            response({ data, error });
        } else {
            console.log("failed login attempt", error);
            response({ data: null, error: error });
        }

    } else if (action === "signup") {
        const { data, error } = await supabaseAuto.auth.signUp(value);

        if (data) {
            response({ message: "Successfully signed up!", data: data });
        } else {
            response({ data: null, error: error?.message || "Signup failed" });
        }

    } else if (action === "signout") {
        const { error } = await supabaseAuto.auth.signOut();
        if (!error) {
            response({ message: "Successfully signed out!" });
        } else {
            response({ error: error?.message || "Signout failed" });
        }

    } else if (action === "getsession") {
        console.log("inside get session")
        const { data, error } = await supabaseAuto.auth.getSession();

        console.log("data inside getSession", data)

        if (data && data.session) {
            const sessionExpiration = data.session.expires_at;
            const currentTime = Math.floor(Date.now() / 1000); // Convert to seconds

            if (sessionExpiration <= currentTime) {
                response({ error: "Session has expired" });
            } else {
                console.log("going to send data")
                response({ data: data });
            }

        } else {
            response({ error: "No session available" });
        }

    } else if (action === "refreshsession") {
        const { data, error } = await supabaseAuto.auth.refreshSession();

        response({ data: data, error: error });
    }
}
```

A message handler for the `supabaseAuto` method is not 100% required, because we’re interacting with a third-party api it’s best to call the supabase functions from the BGSW as that is where any external apis function best in a chrome extension.  Unlike `supabaseManual` there’s `chrome.storage` functions being leveraged as all of that is happening in the background.  

# Creating `Popup.tsx` Components w/Messaging to BGSW

```jsx
pnpm dlx shadcn-ui@latest add form
```

Add the built-in form component from the Shadcn-UI library.  

```jsx
pnpm dlx shadcn-ui@latest add input
```

Also, add in the input component

```jsx
pnpm dlx shadcn-ui@latest add toast
```

Add the toast component to signify to the user if an incorrect login took place

```jsx
// react stuff
import { useEffect, useState } from "react"

// supabase stuff
import type { Provider, User } from "@supabase/supabase-js"
import { supabase } from "./core/supabase"

// react-hook-form stuff
import { z } from "zod"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"

// shadcn-ui form components imports
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage
} from "~components/ui/form"
import { Toaster } from "~components/ui/toaster"
import { useToast } from "~components/ui/use-toast"

// tailwind stuff
import "~style.css"

// the supabase variable inputs
import { supabaseAuto, supabaseManual, supabasePlasmo } from "./core/supabase"
```

Add all the imports 

```jsx
// creating a form schema 
const formSchema = z.object({
  username: z.string().min(2).max(50),
  password: z.string().min(8)
})
```

Create a form schema component, the min and max characters for username and password are contingent on your requirements, they’re not necessary.  This will be the only function outside the React component.    

```tsx
const { toast } = useToast();
  
const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      username: "",
      password: "",
    },
  });
```

Instantiate the `useToast()` function so that the toaster will work and create a variable called `form` for the `useForm` hook coming from the react-hook-forms library.

### `useEffect()` for `supabaseManual`

```tsx
useEffect(() => {
    setLoadingUser(true)

    chrome.storage.local.get(
      [
        chromeStorageKeys.supabaseAccessToken,
        chromeStorageKeys.supabaseExpiration,
        chromeStorageKeys.supabaseUserData
      ],
      (result) => {
        if (result && result[chromeStorageKeys.supabaseAccessToken]) {
          const currentTime = Math.floor(Date.now() / 1000) // convert to seconds
          const timeUntilExpiration =
            result[chromeStorageKeys.supabaseExpiration] - currentTime

          const refreshAndUpdate = () => {
            chrome.runtime.sendMessage({ action: "refresh" }, (response) => {
              if (response.error) {
                console.log("Error refreshing token: " + response.error)
              } else {
                if (response.data && response.data.session) {
                  console.log("Token refreshed successfully")
                  setUser(response.data.user)
                  setExpiration(response.data.session.expires_at)
                } else {
                  console.log("Error: session data is not available")
                }
              }
              setLoadingUser(false)
            })
          }

          if (timeUntilExpiration <= 0) {
            // Token is expired, request a refresh and update user and expiration
            console.log("Session expired, refreshing token")
            refreshAndUpdate()
          } else {
            // Token is not expired, set user data and expiration
            setUser(result[chromeStorageKeys.supabaseUserData])
            setExpiration(result[chromeStorageKeys.supabaseExpiration])

            if (timeUntilExpiration < 24 * 60 * 60) {
              // less than 24 hours left, request a refresh and update user and expiration
              console.log("Token is about to expire, refreshing token")
              refreshAndUpdate()
            } else {
              setLoadingUser(false) //Add this line
            }
          }
        } else {
          setLoadingUser(false) //Add this line
        }
      }
    )
  }, [])
```

Once the component is mounted a `useEffect()` needs to run to get keys from storage, this `useEffect()` will differ for the `supabaseAuto` and `supabaseManual` methods.  Basically, it checks whether or not there’s an active session, if the token expired then it refreshes the session using the `refreshAndUpdate()` method.  The `refreshAndUpdate()` function could be moved outside of the `useEffect()` and into the react component to then be called inside the `useEffect()` but that’s just semantics. 

### `useEffect()` for `supabaseAuto`

```tsx
useEffect(() => {
    chrome.runtime.sendMessage({ action: "getsession" }, (response) => {
      // console.log('sending getsession from popup')
      console.log("response", response)

      if (response.error) {
        // If session has expired, attempt to refresh it
        if (response.error === "Session has expired") {
          console.log("Session has expired, attempting to refresh...")
          refreshSession()
        } else {
          console.log("Error getting session: " + response.error)
        }
      } else if (response.data && response.data.session) {
        console.log("Session retrieved successfully")
        console.log("Session data: ", response.data.session)
        console.log("User data: ", response.data.session.user)
        setUser(response.data.session.user)
      } else {
        console.log("Error: session data is not available")
      }
    })
  }, [])
```

The `useEffect()` for `supabaseAuto` is far less verbose than for `supabaseManual` method because the `chrome.storage.local` is not being pulled from `chrome.storage.local` manually as those operations are happening in the background.  

### `handleSignin()`, `handleSignout()` and `refreshSession()` Methods

```jsx
// create a function to handle login
  async function handleLogin(username: string, password: string) {
    try {
      // Send a message to the background script to initiate the login
      chrome.runtime.sendMessage(
        { action: "signin", value: { email: username, password: password } },
        (response) => {
          if (response.error) {
            // alert("Error with auth: " + response.error.message);

            toast({
              description: `Error with auth: ${response.error.message}`
            })

            console.log("Error with auth: " + response.error.message)
          } else if (response.data?.user) {
            setUser(response.data.user)
            setExpiration(response.data.session.expires_at)
          }
        }
      )
    } catch (error) {
      console.log("Error with auth: " + error.error.message)
      // alert(error.error_description || error);
    }
  }

  async function handleSignOut() {
    try {
        // Send a message to the background script to initiate the sign out
        chrome.runtime.sendMessage({ action: "signout" }, (response) => {
            if (response.error) {
                toast({ description: `Error signing out: ${response.error}` });
                console.log("Error signing out: ", response.error);
            } else {
                // Clear the user and session data upon successful sign-out
                setUser(null);
                setExpiration(0);
            }
        });
    } catch (error) {
        console.log("Error signing out: ", error.message);
    }
}

  const refreshSession = () => {
    chrome.runtime.sendMessage(
      { action: "refreshsession" },
      (refreshResponse) => {
        if (refreshResponse.error) {
          console.log("Error refreshing session: " + refreshResponse.error)
        } else if (refreshResponse.data && refreshResponse.data.session) {
          console.log("Session refreshed successfully")
          setUser(refreshResponse.data.user)
        } else {
          console.log("Error: refreshed session data is not available")
        }
      }
    )
  }
```

These are the primary methods used to send the data from the `popup.tsx` to the BGSW in the `background.ts` file.  The `handleSignin()` and the `handleSignout()` gets mapped to buttons in the return statement.  These functions are all inside the react component.  

### Putting it all together in the return statement

```tsx
return (
    <div className="w-96 px-5 py-4">
      <Toaster></Toaster>
      {user ? (
        // If user is logged in
        <div>
          <h1 className="text-xl font-bold mb-4">User Info</h1>
          <p>User ID: {user.id}</p>{" "}
          <Button onClick={handleSignOut}></Button>
        </div>
      ) : (
        <Form {...form}>
          <h1 className="text-xl font-bold mb-4">Basic Auth</h1>
          <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
            <FormField
              control={form.control}
              name="username"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Password</FormLabel>
                  <FormControl>
                    <Input placeholder="username" {...field} />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />
            <FormField
              control={form.control}
              name="password"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Password</FormLabel>
                  <FormControl>
                    <Input placeholder="password" {...field} />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />
            <Button type="submit">Login</Button>
          </form>
        </Form>
      )}
    </div>
  )
```

There’s a checker that checks whether or not there’s a variable available for the user, if there’s no user the login-in form is shown otherwise the user gets into the app.  

Use the Shadcn-UI forms link below as a reference for learning how to work with the react-hook-forms library.  

[https://ui.shadcn.com/docs/components/form](https://ui.shadcn.com/docs/components/form)

### `Popup.tsx` Plasmo version

This version will not be leveraging message passing so the login/signup functionality will not operate the same way.  Use the link below as a reference for the supabase auth sign-in functionality.  

[https://docs.plasmo.com/quickstarts/with-supabase](https://docs.plasmo.com/quickstarts/with-supabase)

### `useEffect()` for `supabasePlasmo`

```jsx
useEffect(() => {
    async function init() {
      const { data, error } = await supabasePlasmo.auth.getSession()

      if (error) {
        console.error(error)
        return
      }
      if (!!data.session) {
        setUser(data.session.user)
      }
    }

    init()
  }, [])
```

Since there’s no messaging happening, the `useEffect()` is far more succinct for the `supabasePlasmo` version.  

```tsx
 // plasmo method
export default function LoginAuthFormPlasmo() {
  const [user, setUser] = useStorage<User>({
    key: "user",
    instance: new Storage({
      area: "local"
    })
  })

  const { toast } = useToast()
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      username: "",
      password: ""
    }
  })

  useEffect(() => {
    async function init() {
      const { data, error } = await supabasePlasmo.auth.getSession()

      if (error) {
        console.error(error)
        return
      }
      if (!!data.session) {
        setUser(data.session.user)
      }
    }

    init()
  }, [])

  const handleEmailLogin = async (type: "LOGIN" | "SIGNUP") => {
    const { username, password } = form.getValues()

    try {
      const { error, data } =
        type === "LOGIN"
          ? await supabasePlasmo.auth.signInWithPassword({
              email: username,
              password
            })
          : await supabasePlasmo.auth.signUp({
              email: username,
              password
            })

      if (error) {
        toast({
          description: `Error with auth: ${error.message}`
        })
      } else if (!data.user) {
        toast({
          description:
            "Signup successful, confirmation mail should be sent soon!"
        })
      } else {
        setUser(data.user)
      }
    } catch (error) {
      console.log("error", error)
      toast({
        description: error.error_description || error
      })
    }
  }

  return (
    <div className="w-96 px-5 py-4">
      <Toaster />
      {user ? (
        <>
          <h3>
            {user.email} - {user.id}
          </h3>
          <h1>this is plasmo </h1>
          <button
            onClick={() => {
              supabasePlasmo.auth.signOut()
              setUser(null)
            }}>
            Logout
          </button>
        </>
      ) : (
        <Form {...form}>
          <h1 className="text-xl font-bold mb-4">Login</h1>
          <form
            onSubmit={form.handleSubmit((data) => handleEmailLogin("LOGIN"))}
            className="space-y-8">
            <FormField
              control={form.control}
              name="username"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Email</FormLabel>
                  <FormControl>
                    <Input placeholder="Your Username" {...field} />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />
            <FormField
              control={form.control}
              name="password"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>Password</FormLabel>
                  <FormControl>
                    <Input
                      type="password"
                      placeholder="Your Password"
                      {...field}
                    />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />
            <Button type="submit">Login</Button>
            <Button onClick={() => handleEmailLogin("SIGNUP")}>Sign up</Button>
          </form>
        </Form>
      )}
    </div>
  )
}
```

Otherwise, it’s very similar to the `supabaseAuto` and `supabaseManual` however, the login, signin, and signout functions are a bit different since it’s modifying the supabase variable directly without leveraging messaging functionality.   

### Other Resources

Below is a good article on how doing oauth authentication in a MV3 chrome extension.  
[https://gourav.io/blog/supabase-auth-chrome-extension](https://gourav.io/blog/supabase-auth-chrome-extension)

# Conclusion

There’s lots of ways for storing access tokens for auth, I covered three methods.  All three methods will work for MV3, and this should be a reasonable template to build the rest of your chrome extension on, I hope this was helpful to anyone reading it.  If you liked my work give me a follow on [twitter](http://twitter.com/remusrisnov).   Check out the github repo below if you want to skip past the tutorial.  

[https://github.com/remusris/plasmo_tailwinds_supabase_scaffold](https://github.com/remusris/plasmo_tailwinds_supabase_scaffold)