busybar-ui / src / core / auth / index.tsx
index.tsx
Raw
import * as WebBrowser from 'expo-web-browser'
import * as AuthSession from 'expo-auth-session'
import { create } from 'zustand'
import { createSelectors } from '../utils'
import { fetchUserSession, getAuthUrl, logout } from './api'
import { type OAuthProvider, type TokenType, type UserSession } from './types'
import { getToken, removeToken, setToken } from './utils'
import { router } from 'expo-router'

interface AuthState {
  token: TokenType | null
  status: 'unauthenticated' | 'loading' | 'authenticated'
  userSession: UserSession | null
  provider: OAuthProvider | null
  initiateOAuth: (provider: OAuthProvider) => Promise<void>
  signOut: () => void
  hydrate: () => Promise<void>
  refreshUserSession: () => Promise<void>
}

const _useAuth = create<AuthState>((set, get) => ({
  status: 'unauthenticated',
  token: null,
  userSession: null,
  provider: null,

  initiateOAuth: async (provider: OAuthProvider) => {
    try {
      set({ status: 'loading' })

      const redirectUrl = AuthSession.makeRedirectUri({
        path: 'auth',
      })
      const authUrl = await getAuthUrl(provider, redirectUrl)
      const result = await WebBrowser.openAuthSessionAsync(authUrl, redirectUrl)

      if (result.type === 'success') {
        const url = new URL(result.url)
        const accessToken = url.searchParams.get('access_token')
        const refreshToken = url.searchParams.get('refresh_token')

        if (accessToken && refreshToken) {
          set({ token: { accessToken, refreshToken } })
          setToken({ accessToken, refreshToken })

          await get().refreshUserSession()

          const user = get().userSession
          console.log('user', user)

          if (!user?.isRegistered) {
            router.replace('/register')
          } else {
            router.replace('/')
          }
        }
      }
    } catch (error) {
      console.error(`Error during ${provider} auth:`, error)
    }
  },

  signOut: async () => {
    const token = get().token
    if (token) {
      await logout(token.accessToken)
    }

    removeToken()
    set({
      status: 'unauthenticated',
      token: null,
      userSession: null,
    })

    router.replace('/login')
  },

  hydrate: async () => {
    try {
      const userToken = getToken()
      if (userToken !== null) {
        set({ token: userToken })
        await get().refreshUserSession()
      } else {
        get().signOut()
      }
    } catch (e) {
      console.error('Error during hydration:', e)
      get().signOut()
    }
  },

  refreshUserSession: async () => {
    try {
      const token = get().token
      if (!token) throw new Error('No token available')
      const userSession = await fetchUserSession(token.accessToken)

      if (userSession) {
        set({ userSession, status: 'authenticated' })
      }
    } catch (e) {
      console.error('Error refreshing user session:', e)
      get().signOut()
    }
  },
}))

export const useAuth = createSelectors(_useAuth)

export const signOut = () => _useAuth.getState().signOut()
export const initiateOAuth = (provider: OAuthProvider) =>
  _useAuth.getState().initiateOAuth(provider)
export const hydrateAuth = () => _useAuth.getState().hydrate()
export const refreshUserSession = () => _useAuth.getState().refreshUserSession()