Chrome Extension MV3 Template : Supabase Auth, Plasmo, Tailwinds CSS & Shadcn UI
remusris
Posted on August 13, 2023
Setting Up Plasmo, Tailwinds & Shadcn-UI
Setting Up Plasmo
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
Adding Tailwinds CSS Manually — Can be Skipped
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
pnpm i -D tailwindcss postcss autoprefixer
npx tailwindcss init
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: {}
}
}
Create a file called postcss.config.js
Paste the code above into that file.
@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 ~
.
{
"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
pnpm dlx shadcn-ui@latest init
run this command
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"
Supabase Auth for MV3 Chrome Extension
Install SupabaseJS client
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
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
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
?
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
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
Command + click (control + click on Windows) on GoTrueClient
imported function
Command + click (control + click on Windows) on SupportedStorage
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
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
)
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"
}
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 }
}
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])
}
})
})
}
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()
}
})
})
}
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()
}
})
})
}
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
}
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")
}
}
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
})
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" })
}
}
}
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 });
}
}
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
Add the built-in form component from the Shadcn-UI library.
pnpm dlx shadcn-ui@latest add input
Also, add in the input component
pnpm dlx shadcn-ui@latest add toast
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"
Add all the imports
// 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.
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
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
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
// 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
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
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()
}, [])
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>
)
}
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
Posted on August 13, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
August 13, 2023