Шаблон MV3 Chrome Extension: Supabase Auth, Plasmo, Tailwinds CSS & Shadcn UI
Для начала работы мы настроим Plasmo, Tailwinds и Shadcn-UI.
Настройка Plasmo
pnpm create plasmo
# OR
yarn create plasmo
# OR
npm create plasmo
Начните работу с Plasmo с помощью базовой команды, приведенной ниже. В этом руководстве мы будем использовать pnpm.
https://docs.plasmo.com/framework
Добавление Tailwinds CSS вручную - допускается пропуск
pnpm create plasmo --with-tailwindcss
Вы можете перейти к настройке Tailwinds с помощью команды, приведенной выше, но если вы хотите узнать, как вручную добавить Tailwinds CSS в ваш проект Plasmo, продолжайте читать дальше. Приведенная ниже ссылка также является хорошим руководством по этому вопросу.
https://docs.plasmo.com/quickstarts/with-tailwindcss
pnpm i -D tailwindcss postcss autoprefixer
npx tailwindcss init
Установив проект с помощью команды create plasmo
, мы можем приступить к настройке tailwinds css.
/**
* @type {import('postcss').ProcessOptions}
*/
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
}
Создайте файл с именем postcss.config.js
. Вставьте в этот файл приведенный выше код.
@tailwind base;
@tailwind components;
@tailwind utilities;
Создайте файл style.css
, он эквивалентен файлу globals.css
, который часто встречается в других проектах tailwind.
Создайте папку src
и поместите в нее файлы popup.tsx
и style.css
.
Измените путь в файле tsconfig.ts
к исходной папке для псевдонима импорта ~
.
{
"extends": "plasmo/templates/tsconfig.base",
"exclude": [
"node_modules"
],
"include": [
".plasmo/index.d.ts",
"./**/*.ts",
"./**/*.tsx"
],
**"compilerOptions": {
"paths": {
"~*": [
"./src/*"
]
},**
"baseUrl": "."
}
}
Настройка Shadcn-UI
Теперь, когда tailwinds добавлен, мы добавим Shadcn-UI с помощью cli. Следуйте указаниям или воспользуйтесь приведенным ниже руководством.
https://ui.shadcn.com/docs/installation/next
pnpm dlx shadcn-ui@latest init
Выполните эту команду
Выберите следующие опции, показанные на скриншоте.
Укажите требование globals.css
к файлу style.css
, расположенному в директории src
.
Для импорта компонентов из каталога src
используйте псевдоним импорта ~
, это необязательно, но рекомендуется. Аналогично поступите с lib/util
, только не используйте ~~lib/utils.ts~~
, так как это автоматически добавит окончание файла ts
.
Как использовать tailwinds в файле popup.tsx
Вы должны импортировать style.css
, используя псевдоним импорта, который мы настроили
import "~style.css"
Расширение Supabase Auth для MV3 Chrome
Установка клиента SupabaseJS
pnpm add @supabase/supabase-js
Введение в Auth в расширении Chrome
Аутентификация - это, по сути, JWT, который хранится в виде маркера доступа и маркера обновления. Когда срок действия маркера доступа истекает, маркер обновления обменивается на новый маркер доступа. В следующем разделе мы расскажем о том, как маркеры доступа и маркеры обновления получают доступ и хранятся в расширении chrome MV3.
Хранение токенов доступа и обновления - три метода
Мы рассмотрим три способа хранения и доступа к токенам обновления. Первый метод использует chrome.storage
api без изменения каких-либо опций в функции createClient
в supabase. По сути, вы вручную создаете логику, проверяющую, не истек ли срок действия токена, что функция createClient
библиотеки supabaseJS обычно делает в фоновом режиме веб-приложения. Этот "ручной" метод может работать с любым api, который предоставляет вам токен доступа и токен обновления. Второй метод модифицирует функцию supabase createClient
для использования api chrome.storage
в фоновом режиме. Третий метод направляет это хранилище к хранилищу plasmo storage api, работающему только во фреймворке plasmo.
Первый метод будем называть supabaseManual
, второй - supabaseAuto
и третий - supabasePlasmo
.
Настройка клиента Supabase
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
)
В данном примере мы экспортируем две разные переменные supabase: supabaseManual
и supabaseAuto
. Первая используется для ручного метода хранения, а вторая - для модификации переменной storage в конфигурации supabase createClient()
с целью использования либо chrome.storage
, либо plasmo storage api. В обычном рабочем проекте переменная должна называться просто supabase
, а не supabaseAuto
или supabaseManual
, но в данном примере мы делаем это для пояснения.
Остальная функциональность supabaseManual
раскрывается в разделе, посвященном BGSW, в файле background.ts
.
Вдохновением для создания supabaseAuto
послужила приведенная ниже ветка github.
https://gist.github.com/lcmchris/da979cbbf56c9452b6e5847ece7ee6ca
В optionsForMethod2
была создана функция для каждого из методов, используемых в chrome.storage
api, - getItem
, setItem
и removeItem
.
Зачем нужны getItem, setItem и removeItem?
Приведенный ниже метод работает только в том случае, если библиотека установлена на локальном устройстве, мне не удалось найти эти типы в размещенном ниже репозитории github.
https://github.com/supabase/supabase-js/blob/master/src/SupabaseClient.ts
Перейдите в папку @supabase/supabase-js
в папке node_modules
, затем перейдите к файлу types.ts
.
node_modules
→ @supabase/supabase-js
→ src
→ lib
→ types.ts
Command + click (control + click в Windows) на импортируемой функции GoTrueClient
Command + click (control + click в Windows) на SupportedStorage
В результате вы перейдете к выделенной строке.
Метод API Plasmo Storage
Метод plasmo storage api является наиболее простым решением, но ограничен только рамками расширения plasmo. Используйте приведенную ниже ссылку в качестве справочника, однако инструкции в ней менее понятны.
https://docs.plasmo.com/quickstarts/with-supabase
pnpm add @plasmohq/storage
Добавьте plasmo storage api в свой проект.
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
)
Создайте переменную storage
, связывающую с plasmo storage api, и на этом все. Осталось еще немного настроить страницу popup.tsx
, но об этом мы расскажем в следующих разделах.
Работник фоновой службы BGSW - Background.ts
Куда поместить файл background.ts?
Файл background.ts
может быть помещен в папку background
с именем index.ts
в папке background. Причина, по которой вы можете захотеть использовать метод папок, заключается в том, что вы можете вложить папку сообщений в папку background. В противном случае можно просто поместить файл background.ts
в папку src
, а не в папку background.
Добавление и удаление токенов доступа вручную
В методе supabaseManual
имеется несколько базовых операций для доступа к токенам доступа и их модификации: getSupabaseKeys()
, validateToken()
, getKeyFromStorage()
, setKeyInStorage()
и removeKeysFromStorage()
. Укажите на accessToken внутри объектной переменной, которая хранит строковое значение для конкретного места chrome.storage
. Хотя технически это не требуется, пытаться каждый раз запоминать точное значение строки будет утомительно.
// init chrome storage keys
const chromeStorageKeys = {
supabaseAccessToken: "supabaseAccessToken",
supabaseRefreshToken: "supabaseRefreshToken",
supabaseUserData: "supabaseUserData",
supabaseExpiration: "supabaseExpiration",
supabaseUserId: "supabaseUserId"
}
К объекту chromeStorageKeys
можно добавить любое количество нужных свойств по своему усмотрению.
// 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 }
}
Функция getSupabaseKeys()
, по сути, является посредником для более основного метода getKeyFromStorage()
.
// 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])
}
})
})
}
Метод getKeyFromStorage()
помещает promise chrome.storage.local.get
в асинхронную функцию, она довольно ограничена в своей функциональности, но мы абстрагируемся от всего остального в функции getSupabaseKeys()
.
//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()
}
})
})
}
Функция setKeyInStorage()
, по сути, загружает ключ в хранилище, поскольку при использовании 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()
}
})
})
}
Функция removeKeysFromStorage()
может пригодиться, когда пользователь вышел из системы или истек срок действия его маркера доступа, что требует от него повторного входа в систему.
После того как метод getSupabaseKeys
вернул supabaseAccessToken
, supabaseExpiration
и userId
, можно приступать к проверке запроса.
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
}
Приведенный выше блок кода является шаблоном для примера функции, в которой из функции getSupabaseKeys()
в функцию validateToken()
выводится токен доступа и время его действия. С подтвержденным маркером доступа можно отправить запрос обратно на сервер или сообщение в 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")
}
}
Это функция validateToken()
, которая используется в функции handleMessage()
для метода supabaseManual
.
Обработчики сообщений для supabaseAuto и supabaseManual
// the event listener
chrome.runtime.onMessage.addListener((message, sender, response) => {
// handleMessageManual(message, sender, response)
handleMessageAutomatic(message, sender, response)
return true
})
В фоновом режиме должен быть запущен слушатель события chrome.runtime.onMessage
, в котором будет активна соответствующая функция handleMessage()
в зависимости от того, какой метод используется - supabaseAuto
или supabaseManual
. Метод supabasePlasmo
не использует обмен сообщениями для аутентификации между popup.tsx
и BGSW, поэтому в нем нет функции обработчика сообщений.
handleMessageManual() для 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" })
}
}
}
BGSW является связующим звеном между accessTokens, расположенным в chrome.storage
api, и файлом popup.tsx
. Он охватывает базовые операции входа, выхода, регистрации и обновления.
handleMessageAutomatic() для 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 });
}
}
Обработчик сообщений для метода supabaseAuto
не является обязательным, поскольку мы взаимодействуем со сторонним api, лучше всего вызывать функции supabase из BGSW, так как именно там в расширении chrome лучше всего работают любые внешние api. В отличие от supabaseManual
, здесь не используются функции chrome.storage
, поскольку все это происходит в фоновом режиме.
Создание компонентов Popup.tsx с передачей сообщений в BGSW
pnpm dlx shadcn-ui@latest add form
Добавьте встроенный компонент формы из библиотеки Shadcn-UI.
pnpm dlx shadcn-ui@latest add input
Также добавьте компонент input
pnpm dlx shadcn-ui@latest add toast
Добавьте компонент toast, который будет сигнализировать пользователю о неправильном входе в систему
// 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"
Добавьте весь импорт
// creating a form schema
const formSchema = z.object({
username: z.string().min(2).max(50),
password: z.string().min(8)
})
Создайте компонент схемы формы, минимальное и максимальное количество символов для имени пользователя и пароля зависит от ваших требований, они не обязательны. Это будет единственная функция вне компонента React.
const { toast } = useToast();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
password: "",
},
});
Установите функцию useToast()
, чтобы toaster работал, и создайте переменную form
для хука useForm
из библиотеки react-hook-forms.
useEffect() для 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
}
}
)
}, [])
После установки компонента для получения ключей из хранилища необходимо запустить метод useEffect()
, который будет отличаться для методов supabaseAuto
и supabaseManual
. В основном проверяется наличие или отсутствие активной сессии, если срок действия токена истек, то сессия обновляется с помощью метода refreshAndUpdate()
. Функцию refreshAndUpdate()
можно было бы вынести за пределы useEffect()
и поместить в react-компонент, чтобы затем вызывать ее внутри useEffect()
, но это лишь семантика.
useEffect() для 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")
}
})
}, [])
Функция useEffect()
для supabaseAuto
гораздо менее многословна, чем для метода supabaseManual
, поскольку операции с chrome.storage.local
не извлекаются из chrome.storage.local
вручную, а происходят в фоновом режиме.
Методы handleSignin(), handleSignout() и refreshSession()
// 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")
}
}
)
}
Это основные методы, используемые для передачи данных из popup.tsx
в BGSW в файле background.ts
. Функции handleSignin()
и handleSignout()
привязываются к кнопкам в операторе возврата. Все эти функции находятся внутри компонента react.
Складываем всё вместе в отчёт о возврате
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>
)
Есть чекер, который проверяет, доступна ли переменная для пользователя, если нет, то показывается форма входа в приложение, иначе пользователь попадает в приложение.
Используйте приведенную ниже ссылку на формы Shadcn-UI в качестве справочника для изучения работы с библиотекой react-hook-forms.
https://ui.shadcn.com/docs/components/form
Версия Popup.tsx Plasmo
В этой версии не будет использоваться передача сообщений, поэтому функциональность входа/регистрации будет работать по-другому. Используйте приведенную ниже ссылку для ознакомления с функциональностью входа в систему с помощью supabase auth.
https://docs.plasmo.com/quickstarts/with-supabase
useEffect() для 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()
}, [])
Поскольку обмена сообщениями не происходит, функция useEffect()
для версии supabasePlasmo
гораздо более лаконична.
// 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>
)
}
В остальном она очень похожа на supabaseAuto
и supabaseManual
, однако функции login, signin и signout немного отличаются, поскольку изменяют переменную supabase напрямую, не используя функциональность обмена сообщениями.
Прочие ресурсы
Ниже приведена хорошая статья о том, как выполнить аутентификацию oauth в расширении MV3 chrome.
https://gourav.io/blog/supabase-auth-chrome-extension
Заключение
Существует множество способов хранения токенов доступа для аутентификации, я рассмотрел три метода. Все три метода будут работать для MV3, и это должно быть разумным шаблоном для создания остальной части вашего расширения chrome, я надеюсь, что это было полезно для всех, кто это читает. Если вы хотите пропустить это руководство, загляните в репозиторий github, расположенный ниже.
https://github.com/remusris/plasmo_tailwinds_supabase_scaffold