Skip to content

Commit

Permalink
privy login on web app (#698)
Browse files Browse the repository at this point in the history
Co-authored-by: The Technocrat <josh.mcmenemy@openzyme.bio>
  • Loading branch information
acashmoney and thetechnocrat-dev committed Oct 12, 2023
1 parent 91450e5 commit bf9e1bf
Show file tree
Hide file tree
Showing 20 changed files with 12,893 additions and 1,886 deletions.
10 changes: 8 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ jobs:
env:
# Setting it at workflow level to be used by all the steps
BACALHAU_API_HOST: "127.0.0.1"
NEXT_PUBLIC_PRIVY_APP_ID: "{{ secrets.NEXT_PUBLIC_PRIVY_APP_ID }}"
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand Down Expand Up @@ -186,7 +187,9 @@ jobs:
- name: docker compose build
run: |
# Build in parallel
docker compose build --parallel
docker compose build --build-arg NEXT_PUBLIC_PRIVY_APP_ID=${{ secrets.NEXT_PUBLIC_PRIVY_APP_ID }} --parallel
env:
NEXT_PUBLIC_PRIVY_APP_ID: ${{ secrets.NEXT_PUBLIC_PRIVY_APP_ID }}

- name: Bring up the stack
run: |
Expand Down Expand Up @@ -249,6 +252,7 @@ jobs:
env:
# Setting it at workflow level to be used by all the steps
BACALHAU_API_HOST: "127.0.0.1"
NEXT_PUBLIC_PRIVY_APP_ID: "{{ secrets.NEXT_PUBLIC_PRIVY_APP_ID }}"
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand Down Expand Up @@ -305,7 +309,9 @@ jobs:
- name: docker compose build
run: |
# Build in parallel
docker compose build --parallel
docker compose build --build-arg NEXT_PUBLIC_PRIVY_APP_ID=${{ secrets.NEXT_PUBLIC_PRIVY_APP_ID }} --parallel
env:
NEXT_PUBLIC_PRIVY_APP_ID: ${{ secrets.NEXT_PUBLIC_PRIVY_APP_ID }}

- name: Bring up the stack
run: |
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ services:
- quay.io/labdao/frontend:latest
args:
NEXT_PUBLIC_BACKEND_URL: http://localhost:8080
NEXT_PUBLIC_PRIVY_APP_ID: $NEXT_PUBLIC_PRIVY_APP_ID
environment:
NODE_ENV: 'production'
ports:
Expand Down
11 changes: 7 additions & 4 deletions frontend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
FROM node:18-alpine AS base

# Include NEXT_PUBLIC_BACKEND_URL and NEXT_PUBLIC_PRIVY_APP_ID
ARG NEXT_PUBLIC_BACKEND_URL=http://localhost:8080
ARG NEXT_PUBLIC_PRIVY_APP_ID

ENV NEXT_PUBLIC_BACKEND_URL=$NEXT_PUBLIC_BACKEND_URL
ENV NEXT_PUBLIC_PRIVY_APP_ID=$NEXT_PUBLIC_PRIVY_APP_ID

# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
Expand All @@ -15,13 +22,9 @@ RUN \
else echo "Lockfile not found." && exit 1; \
fi


# Rebuild the source code only when needed
FROM base AS builder

# Include NEXT_PUBLIC_BACKEND_URL
ARG NEXT_PUBLIC_BACKEND_URL http://localhost:8080

WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
Expand Down
1 change: 1 addition & 0 deletions frontend/app/components/HomeMenu/HomeMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import ListItem from '@mui/material/ListItem'
import ListItemButton from '@mui/material/ListItemButton'
import ListItemText from '@mui/material/ListItemText'
import { useRouter } from 'next/navigation'
import { usePrivy } from '@privy-io/react-auth';

export const HomeMenu = () => {
const router = useRouter()
Expand Down
85 changes: 85 additions & 0 deletions frontend/app/components/PrivyLogin/PrivyLoginComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React, { useContext } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { setIsLoggedIn, selectIsLoggedIn, setWalletAddress, AppDispatch } from '@/lib/redux';
import Button from '@mui/material/Button';
import Box from '@mui/material/Box';
import { useLogin, useWallets } from '@privy-io/react-auth';
import { PrivyAuthContext } from '../../../lib/PrivyContext';
import { saveUserAsync } from '@/lib/redux/slices/userSlice/thunks';
import { useRouter } from 'next/navigation'

const PrivyLoginComponent: React.FC = () => {
const dispatch: AppDispatch = useDispatch();
const { user } = useContext(PrivyAuthContext);
const router = useRouter()

const { login } = useLogin({
onComplete: async (user, isNewUser, wasAlreadyAuthenticated) => {
const walletAddress = await getWalletAddress();
if (wasAlreadyAuthenticated) {
console.log('User was already authenticated');
dispatch(setIsLoggedIn(true));
router.push('/');
} else if (isNewUser) {
console.log('New user');
dispatch(saveUserAsync({ walletAddress }));
dispatch(setIsLoggedIn(true));
router.push('/');
} else if (user) {
console.log('User authenticated');
dispatch(setIsLoggedIn(true));
router.push('/');
}
},
onError: (error) => {
console.log('onError callback triggered', error);
}
})

const handleLogin = async () => {
if (!user) {
try {
login();
} catch (error) {
console.log('Error calling login function:', error);
}
}
}

const getWalletAddress = async () => {
let counter = 0;
let wallets = JSON.parse(localStorage.getItem('privy:connections') || '[]');

while (wallets.length === 0 || (wallets[0].walletClientType !== 'privy' && counter < 5)) {
// Wait for 1 second before checking again
await new Promise(resolve => setTimeout(resolve, 1000));
counter++;
wallets = JSON.parse(localStorage.getItem('privy:connections') || '[]');
}

if (wallets.length > 0) {
const walletAddress = wallets[0].address;
localStorage.setItem('walletAddress', walletAddress);
dispatch(setWalletAddress(walletAddress));
return walletAddress;
}
}

return (
<Box
display="flex"
justifyContent="center"
mt={2}
>
<Button
variant="contained"
onClick={handleLogin}
sx={{ backgroundColor: '#333333', '&:hover': { backgroundColor: '#6bdaad' } }}
>
Login
</Button>
</Box>
)
}

export default PrivyLoginComponent;
48 changes: 30 additions & 18 deletions frontend/app/components/TopNav/TopNav.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import React from 'react'
import React, { useContext, useEffect } from 'react'

import MenuIcon from '@mui/icons-material/Menu'
import Menu from '@mui/material/Menu'
Expand All @@ -14,19 +14,20 @@ import {
useSelector,
selectWalletAddress,
selectIsLoggedIn,
selectUsername,
setUsername,
setWalletAddress,
setIsLoggedIn,
} from '@/lib/redux'
import { usePrivy } from '@privy-io/react-auth';
import { PrivyAuthContext } from '../../../lib/PrivyContext';

export const TopNav = () => {
const dispatch = useDispatch()
const router = useRouter()
const isLoggedIn = useSelector(selectIsLoggedIn)
const username = useSelector(selectUsername)
const { ready, authenticated, user, exportWallet } = usePrivy();
const walletAddress = useSelector(selectWalletAddress)

const { logout } = usePrivy();

// State and handlers for the dropdown menu
const [anchorEl, setAnchorEl] = React.useState<null | SVGSVGElement>(null)

Expand All @@ -42,26 +43,30 @@ export const TopNav = () => {
router.push(path)
}

const handleLogout = () => {
// Clear data from localStorage
localStorage.removeItem('username')
localStorage.removeItem('walletAddress')
dispatch(setUsername(''))
dispatch(setWalletAddress(''))
dispatch(setIsLoggedIn(false))
handleClose()
router.push('/login')
const hasEmbeddedWallet = ready && authenticated && !!user?.linkedAccounts.find((account: any) => account.type === 'wallet' && account.walletClient === 'privy');

const handleExportWallet = async () => {
if (hasEmbeddedWallet) {
exportWallet();
}
}

const handleLogout = async () => {
logout();
localStorage.removeItem('walletAddress');
dispatch(setWalletAddress(''));
dispatch(setIsLoggedIn(false));
handleClose();
router.push('/login');
}

return (
<nav className={styles.navbar}>
<span className={styles.link} onClick={() => handleNavigation('/')}>
plex
</span>
{isLoggedIn && (
{ready && authenticated && (
<div className={styles.userContainer}>
<span className={styles.username}>{username}</span>
<MenuIcon style={{ color: 'white', marginLeft: '10px' }} onClick={(e: any) => handleClick(e)} />
<Menu
anchorEl={anchorEl}
Expand All @@ -70,11 +75,18 @@ export const TopNav = () => {
onClose={handleClose}
>
<MenuItem onClick={handleClose}>Wallet: { walletAddress }</MenuItem>
<div title={!hasEmbeddedWallet ? 'Export wallet only available for embedded wallets.' : ''}>
<MenuItem
onClick={handleExportWallet}
disabled={!hasEmbeddedWallet}
>
Export Wallet
</MenuItem>
</div>
<MenuItem onClick={handleLogout}>Logout</MenuItem>
</Menu>
</div>
)}
{/* Other links or elements can be added here if required */}
</nav>
)
}
}
27 changes: 11 additions & 16 deletions frontend/app/components/UserLoader/UserLoader.jsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,41 @@
'use client'

import { useState, useEffect } from 'react';
import { useState, useEffect, useContext } from 'react';

import {
useDispatch,
useSelector,
selectUsername,
selectWalletAddress,
setUsername,
setWalletAddress,
setIsLoggedIn,
} from '@/lib/redux'

import { usePrivy } from '@privy-io/react-auth'
import { useRouter } from 'next/navigation'

export const UserLoader = ({ children }) => {
const dispatch = useDispatch()
const router = useRouter();
const [isLoaded, setIsLoaded] = useState(false);
const userNameFromRedux = useSelector(selectUsername)
const { ready, authenticated } = usePrivy();

const walletAddressFromRedux = useSelector(selectWalletAddress)

useEffect(() => {
const usernameFromLocalStorage = localStorage.getItem('username')
const walletAddressFromLocalStorage = localStorage.getItem('walletAddress')

if (!userNameFromRedux && usernameFromLocalStorage) {
dispatch(setUsername(usernameFromLocalStorage));
}

if (!walletAddressFromRedux && walletAddressFromLocalStorage) {
dispatch(setWalletAddress(walletAddressFromLocalStorage))
}

if (!usernameFromLocalStorage || !walletAddressFromLocalStorage) {
router.push('/login')
} else {
dispatch(setIsLoggedIn(true))
if (ready) {
if (!authenticated) {
router.push('/login')
} else {
dispatch(setIsLoggedIn(true))
}
}

setIsLoaded(true)
}, [dispatch])
}, [dispatch, ready, authenticated])

if (!isLoaded) return null

Expand Down
34 changes: 16 additions & 18 deletions frontend/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,26 @@ import './styles/globals.css'

export default function RootLayout(props: React.PropsWithChildren) {
return (
<Providers>
<html lang="en">
<body>
<Box display="flex" flexDirection="column" height="100vh"> {/* Fill entire view height and set up for flex */}
<TopNav />

<Container maxWidth="lg" style={{paddingBottom: "40px"}}>
<Box mt={5} mb={5} flexGrow={1} display="flex" alignItems="center" justifyContent="center"> {/* Center content */}
<Grid container direction="column" spacing={3}>
<Grid item xs={12}>
<UserLoader>
<main>{props.children}</main>
</UserLoader>
<Providers>
<Box display="flex" flexDirection="column" height="100vh"> {/* Fill entire view height and set up for flex */}
<TopNav />
<Container maxWidth="lg" style={{paddingBottom: "40px"}}>
<Box mt={5} mb={5} flexGrow={1} display="flex" alignItems="center" justifyContent="center"> {/* Center content */}
<Grid container direction="column" spacing={3}>
<Grid item xs={12}>
<UserLoader>
<main>{props.children}</main>
</UserLoader>
</Grid>
</Grid>
</Grid>
</Box>
</Container>

<FootBar />
</Box>
</Box>
</Container>
<FootBar />
</Box>
</Providers>
</body>
</html>
</Providers>
)
}

0 comments on commit bf9e1bf

Please sign in to comment.