DEV Community

remusris
remusris

Posted on

Chrome Extension MV3 Template : Supabase Auth, Plasmo, Tailwinds CSS & Shadcn UI

Setting Up Plasmo, Tailwinds & Shadcn-UI

Setting Up Plasmo

pnpm create plasmo
# OR
yarn create plasmo
# OR
npm create plasmo
Enter fullscreen mode Exit fullscreen mode

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

Adding Tailwinds CSS Manually — Can be Skipped

pnpm create plasmo --with-tailwindcss
Enter fullscreen mode Exit fullscreen mode

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

pnpm i -D tailwindcss postcss autoprefixer
Enter fullscreen mode Exit fullscreen mode
npx tailwindcss init
Enter fullscreen mode Exit fullscreen mode

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

/**
 * @type {import('postcss').ProcessOptions}
 */
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {}
  }
}
Enter fullscreen mode Exit fullscreen mode

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

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

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 ~.

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

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

pnpm dlx shadcn-ui@latest init
Enter fullscreen mode Exit fullscreen mode

run this command

Image description

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

import "~style.css"
Enter fullscreen mode Exit fullscreen mode

Supabase Auth for MV3 Chrome Extension

Install SupabaseJS client

pnpm add @supabase/supabase-js
Enter fullscreen mode Exit fullscreen mode

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

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
)
Enter fullscreen mode Exit fullscreen mode

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

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

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

Path
Go to the @supabase/supabase-js folder in the node_modules folder, go to types.ts

node_modules@supabase/supabase-jssrclibtypes.ts

GoTrueClient
Command + click (control + click on Windows) on GoTrueClient imported function

Supported Storage
Command + click (control + click on Windows) on SupportedStorage

GetItem, RemoveItem, SetItem
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

pnpm add @plasmohq/storage
Enter fullscreen mode Exit fullscreen mode

Add the plasmo storage api to your project

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
)
Enter fullscreen mode Exit fullscreen mode

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.

// init chrome storage keys
const chromeStorageKeys = {
  supabaseAccessToken: "supabaseAccessToken",
  supabaseRefreshToken: "supabaseRefreshToken",
  supabaseUserData: "supabaseUserData",
  supabaseExpiration: "supabaseExpiration",
  supabaseUserId: "supabaseUserId"
}
Enter fullscreen mode Exit fullscreen mode

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

// 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 }
}
Enter fullscreen mode Exit fullscreen mode

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

// 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])
      }
    })
  })
}
Enter fullscreen mode Exit fullscreen mode

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.

//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()
      }
    })
  })
}
Enter fullscreen mode Exit fullscreen mode

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

//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()
      }
    })
  })
}
Enter fullscreen mode Exit fullscreen mode

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.

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
}
Enter fullscreen mode Exit fullscreen mode

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.

// 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")
  }
}
Enter fullscreen mode Exit fullscreen mode

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

Message Handlers for supabaseAuto & supabaseManual

// the event listener
chrome.runtime.onMessage.addListener((message, sender, response) => {
//   handleMessageManual(message, sender, response)
  handleMessageAutomatic(message, sender, response)
  return true
})
Enter fullscreen mode Exit fullscreen mode

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

// 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" })
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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

// 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 });
    }
}
Enter fullscreen mode Exit fullscreen mode

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

pnpm dlx shadcn-ui@latest add form
Enter fullscreen mode Exit fullscreen mode

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

pnpm dlx shadcn-ui@latest add input
Enter fullscreen mode Exit fullscreen mode

Also, add in the input component

pnpm dlx shadcn-ui@latest add toast
Enter fullscreen mode Exit fullscreen mode

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

// 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"
Enter fullscreen mode Exit fullscreen mode

Add all the imports

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

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.

const { toast } = useToast();

const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      username: "",
      password: "",
    },
  });
Enter fullscreen mode Exit fullscreen mode

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

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
        }
      }
    )
  }, [])
Enter fullscreen mode Exit fullscreen mode

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

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")
      }
    })
  }, [])
Enter fullscreen mode Exit fullscreen mode

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

// 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")
        }
      }
    )
  }
Enter fullscreen mode Exit fullscreen mode

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

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>
  )
Enter fullscreen mode Exit fullscreen mode

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

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

useEffect() for supabasePlasmo

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()
  }, [])
Enter fullscreen mode Exit fullscreen mode

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

 // 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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

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. Check out the github repo below if you want to skip past the tutorial.

https://github.com/remusris/plasmo_tailwinds_supabase_scaffold

Top comments (0)