DevGang
Авторизоваться

Шаблон 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

Источник:

#JavaScript
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу