Skip to content

Commit bf9e1bf

Browse files
privy login on web app (#698)
Co-authored-by: The Technocrat <josh.mcmenemy@openzyme.bio>
1 parent 91450e5 commit bf9e1bf

File tree

20 files changed

+12893
-1886
lines changed

20 files changed

+12893
-1886
lines changed

.github/workflows/ci.yml

+8-2
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ jobs:
135135
env:
136136
# Setting it at workflow level to be used by all the steps
137137
BACALHAU_API_HOST: "127.0.0.1"
138+
NEXT_PUBLIC_PRIVY_APP_ID: "{{ secrets.NEXT_PUBLIC_PRIVY_APP_ID }}"
138139
steps:
139140
- name: Checkout code
140141
uses: actions/checkout@v4
@@ -186,7 +187,9 @@ jobs:
186187
- name: docker compose build
187188
run: |
188189
# Build in parallel
189-
docker compose build --parallel
190+
docker compose build --build-arg NEXT_PUBLIC_PRIVY_APP_ID=${{ secrets.NEXT_PUBLIC_PRIVY_APP_ID }} --parallel
191+
env:
192+
NEXT_PUBLIC_PRIVY_APP_ID: ${{ secrets.NEXT_PUBLIC_PRIVY_APP_ID }}
190193

191194
- name: Bring up the stack
192195
run: |
@@ -249,6 +252,7 @@ jobs:
249252
env:
250253
# Setting it at workflow level to be used by all the steps
251254
BACALHAU_API_HOST: "127.0.0.1"
255+
NEXT_PUBLIC_PRIVY_APP_ID: "{{ secrets.NEXT_PUBLIC_PRIVY_APP_ID }}"
252256
steps:
253257
- name: Checkout code
254258
uses: actions/checkout@v4
@@ -305,7 +309,9 @@ jobs:
305309
- name: docker compose build
306310
run: |
307311
# Build in parallel
308-
docker compose build --parallel
312+
docker compose build --build-arg NEXT_PUBLIC_PRIVY_APP_ID=${{ secrets.NEXT_PUBLIC_PRIVY_APP_ID }} --parallel
313+
env:
314+
NEXT_PUBLIC_PRIVY_APP_ID: ${{ secrets.NEXT_PUBLIC_PRIVY_APP_ID }}
309315

310316
- name: Bring up the stack
311317
run: |

docker-compose.yml

+1
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ services:
129129
- quay.io/labdao/frontend:latest
130130
args:
131131
NEXT_PUBLIC_BACKEND_URL: http://localhost:8080
132+
NEXT_PUBLIC_PRIVY_APP_ID: $NEXT_PUBLIC_PRIVY_APP_ID
132133
environment:
133134
NODE_ENV: 'production'
134135
ports:

frontend/Dockerfile

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
FROM node:18-alpine AS base
22

3+
# Include NEXT_PUBLIC_BACKEND_URL and NEXT_PUBLIC_PRIVY_APP_ID
4+
ARG NEXT_PUBLIC_BACKEND_URL=http://localhost:8080
5+
ARG NEXT_PUBLIC_PRIVY_APP_ID
6+
7+
ENV NEXT_PUBLIC_BACKEND_URL=$NEXT_PUBLIC_BACKEND_URL
8+
ENV NEXT_PUBLIC_PRIVY_APP_ID=$NEXT_PUBLIC_PRIVY_APP_ID
9+
310
# Install dependencies only when needed
411
FROM base AS deps
512
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
@@ -15,13 +22,9 @@ RUN \
1522
else echo "Lockfile not found." && exit 1; \
1623
fi
1724

18-
1925
# Rebuild the source code only when needed
2026
FROM base AS builder
2127

22-
# Include NEXT_PUBLIC_BACKEND_URL
23-
ARG NEXT_PUBLIC_BACKEND_URL http://localhost:8080
24-
2528
WORKDIR /app
2629
COPY --from=deps /app/node_modules ./node_modules
2730
COPY . .

frontend/app/components/HomeMenu/HomeMenu.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import ListItem from '@mui/material/ListItem'
66
import ListItemButton from '@mui/material/ListItemButton'
77
import ListItemText from '@mui/material/ListItemText'
88
import { useRouter } from 'next/navigation'
9+
import { usePrivy } from '@privy-io/react-auth';
910

1011
export const HomeMenu = () => {
1112
const router = useRouter()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import React, { useContext } from 'react';
2+
import { useDispatch, useSelector } from 'react-redux';
3+
import { setIsLoggedIn, selectIsLoggedIn, setWalletAddress, AppDispatch } from '@/lib/redux';
4+
import Button from '@mui/material/Button';
5+
import Box from '@mui/material/Box';
6+
import { useLogin, useWallets } from '@privy-io/react-auth';
7+
import { PrivyAuthContext } from '../../../lib/PrivyContext';
8+
import { saveUserAsync } from '@/lib/redux/slices/userSlice/thunks';
9+
import { useRouter } from 'next/navigation'
10+
11+
const PrivyLoginComponent: React.FC = () => {
12+
const dispatch: AppDispatch = useDispatch();
13+
const { user } = useContext(PrivyAuthContext);
14+
const router = useRouter()
15+
16+
const { login } = useLogin({
17+
onComplete: async (user, isNewUser, wasAlreadyAuthenticated) => {
18+
const walletAddress = await getWalletAddress();
19+
if (wasAlreadyAuthenticated) {
20+
console.log('User was already authenticated');
21+
dispatch(setIsLoggedIn(true));
22+
router.push('/');
23+
} else if (isNewUser) {
24+
console.log('New user');
25+
dispatch(saveUserAsync({ walletAddress }));
26+
dispatch(setIsLoggedIn(true));
27+
router.push('/');
28+
} else if (user) {
29+
console.log('User authenticated');
30+
dispatch(setIsLoggedIn(true));
31+
router.push('/');
32+
}
33+
},
34+
onError: (error) => {
35+
console.log('onError callback triggered', error);
36+
}
37+
})
38+
39+
const handleLogin = async () => {
40+
if (!user) {
41+
try {
42+
login();
43+
} catch (error) {
44+
console.log('Error calling login function:', error);
45+
}
46+
}
47+
}
48+
49+
const getWalletAddress = async () => {
50+
let counter = 0;
51+
let wallets = JSON.parse(localStorage.getItem('privy:connections') || '[]');
52+
53+
while (wallets.length === 0 || (wallets[0].walletClientType !== 'privy' && counter < 5)) {
54+
// Wait for 1 second before checking again
55+
await new Promise(resolve => setTimeout(resolve, 1000));
56+
counter++;
57+
wallets = JSON.parse(localStorage.getItem('privy:connections') || '[]');
58+
}
59+
60+
if (wallets.length > 0) {
61+
const walletAddress = wallets[0].address;
62+
localStorage.setItem('walletAddress', walletAddress);
63+
dispatch(setWalletAddress(walletAddress));
64+
return walletAddress;
65+
}
66+
}
67+
68+
return (
69+
<Box
70+
display="flex"
71+
justifyContent="center"
72+
mt={2}
73+
>
74+
<Button
75+
variant="contained"
76+
onClick={handleLogin}
77+
sx={{ backgroundColor: '#333333', '&:hover': { backgroundColor: '#6bdaad' } }}
78+
>
79+
Login
80+
</Button>
81+
</Box>
82+
)
83+
}
84+
85+
export default PrivyLoginComponent;
+30-18
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import React from 'react'
3+
import React, { useContext, useEffect } from 'react'
44

55
import MenuIcon from '@mui/icons-material/Menu'
66
import Menu from '@mui/material/Menu'
@@ -14,19 +14,20 @@ import {
1414
useSelector,
1515
selectWalletAddress,
1616
selectIsLoggedIn,
17-
selectUsername,
18-
setUsername,
1917
setWalletAddress,
2018
setIsLoggedIn,
2119
} from '@/lib/redux'
20+
import { usePrivy } from '@privy-io/react-auth';
21+
import { PrivyAuthContext } from '../../../lib/PrivyContext';
2222

2323
export const TopNav = () => {
2424
const dispatch = useDispatch()
2525
const router = useRouter()
26-
const isLoggedIn = useSelector(selectIsLoggedIn)
27-
const username = useSelector(selectUsername)
26+
const { ready, authenticated, user, exportWallet } = usePrivy();
2827
const walletAddress = useSelector(selectWalletAddress)
2928

29+
const { logout } = usePrivy();
30+
3031
// State and handlers for the dropdown menu
3132
const [anchorEl, setAnchorEl] = React.useState<null | SVGSVGElement>(null)
3233

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

45-
const handleLogout = () => {
46-
// Clear data from localStorage
47-
localStorage.removeItem('username')
48-
localStorage.removeItem('walletAddress')
49-
dispatch(setUsername(''))
50-
dispatch(setWalletAddress(''))
51-
dispatch(setIsLoggedIn(false))
52-
handleClose()
53-
router.push('/login')
46+
const hasEmbeddedWallet = ready && authenticated && !!user?.linkedAccounts.find((account: any) => account.type === 'wallet' && account.walletClient === 'privy');
47+
48+
const handleExportWallet = async () => {
49+
if (hasEmbeddedWallet) {
50+
exportWallet();
51+
}
5452
}
5553

54+
const handleLogout = async () => {
55+
logout();
56+
localStorage.removeItem('walletAddress');
57+
dispatch(setWalletAddress(''));
58+
dispatch(setIsLoggedIn(false));
59+
handleClose();
60+
router.push('/login');
61+
}
5662

5763
return (
5864
<nav className={styles.navbar}>
5965
<span className={styles.link} onClick={() => handleNavigation('/')}>
6066
plex
6167
</span>
62-
{isLoggedIn && (
68+
{ready && authenticated && (
6369
<div className={styles.userContainer}>
64-
<span className={styles.username}>{username}</span>
6570
<MenuIcon style={{ color: 'white', marginLeft: '10px' }} onClick={(e: any) => handleClick(e)} />
6671
<Menu
6772
anchorEl={anchorEl}
@@ -70,11 +75,18 @@ export const TopNav = () => {
7075
onClose={handleClose}
7176
>
7277
<MenuItem onClick={handleClose}>Wallet: { walletAddress }</MenuItem>
78+
<div title={!hasEmbeddedWallet ? 'Export wallet only available for embedded wallets.' : ''}>
79+
<MenuItem
80+
onClick={handleExportWallet}
81+
disabled={!hasEmbeddedWallet}
82+
>
83+
Export Wallet
84+
</MenuItem>
85+
</div>
7386
<MenuItem onClick={handleLogout}>Logout</MenuItem>
7487
</Menu>
7588
</div>
7689
)}
77-
{/* Other links or elements can be added here if required */}
7890
</nav>
7991
)
80-
}
92+
}

frontend/app/components/UserLoader/UserLoader.jsx

+11-16
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,41 @@
11
'use client'
22

3-
import { useState, useEffect } from 'react';
3+
import { useState, useEffect, useContext } from 'react';
44

55
import {
66
useDispatch,
77
useSelector,
8-
selectUsername,
98
selectWalletAddress,
10-
setUsername,
119
setWalletAddress,
1210
setIsLoggedIn,
1311
} from '@/lib/redux'
14-
12+
import { usePrivy } from '@privy-io/react-auth'
1513
import { useRouter } from 'next/navigation'
1614

1715
export const UserLoader = ({ children }) => {
1816
const dispatch = useDispatch()
1917
const router = useRouter();
2018
const [isLoaded, setIsLoaded] = useState(false);
21-
const userNameFromRedux = useSelector(selectUsername)
19+
const { ready, authenticated } = usePrivy();
20+
2221
const walletAddressFromRedux = useSelector(selectWalletAddress)
2322

2423
useEffect(() => {
25-
const usernameFromLocalStorage = localStorage.getItem('username')
2624
const walletAddressFromLocalStorage = localStorage.getItem('walletAddress')
2725

28-
if (!userNameFromRedux && usernameFromLocalStorage) {
29-
dispatch(setUsername(usernameFromLocalStorage));
30-
}
31-
3226
if (!walletAddressFromRedux && walletAddressFromLocalStorage) {
3327
dispatch(setWalletAddress(walletAddressFromLocalStorage))
3428
}
3529

36-
if (!usernameFromLocalStorage || !walletAddressFromLocalStorage) {
37-
router.push('/login')
38-
} else {
39-
dispatch(setIsLoggedIn(true))
30+
if (ready) {
31+
if (!authenticated) {
32+
router.push('/login')
33+
} else {
34+
dispatch(setIsLoggedIn(true))
35+
}
4036
}
41-
4237
setIsLoaded(true)
43-
}, [dispatch])
38+
}, [dispatch, ready, authenticated])
4439

4540
if (!isLoaded) return null
4641

frontend/app/layout.tsx

+16-18
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,26 @@ import './styles/globals.css'
99

1010
export default function RootLayout(props: React.PropsWithChildren) {
1111
return (
12-
<Providers>
1312
<html lang="en">
1413
<body>
15-
<Box display="flex" flexDirection="column" height="100vh"> {/* Fill entire view height and set up for flex */}
16-
<TopNav />
17-
18-
<Container maxWidth="lg" style={{paddingBottom: "40px"}}>
19-
<Box mt={5} mb={5} flexGrow={1} display="flex" alignItems="center" justifyContent="center"> {/* Center content */}
20-
<Grid container direction="column" spacing={3}>
21-
<Grid item xs={12}>
22-
<UserLoader>
23-
<main>{props.children}</main>
24-
</UserLoader>
14+
<Providers>
15+
<Box display="flex" flexDirection="column" height="100vh"> {/* Fill entire view height and set up for flex */}
16+
<TopNav />
17+
<Container maxWidth="lg" style={{paddingBottom: "40px"}}>
18+
<Box mt={5} mb={5} flexGrow={1} display="flex" alignItems="center" justifyContent="center"> {/* Center content */}
19+
<Grid container direction="column" spacing={3}>
20+
<Grid item xs={12}>
21+
<UserLoader>
22+
<main>{props.children}</main>
23+
</UserLoader>
24+
</Grid>
2525
</Grid>
26-
</Grid>
27-
</Box>
28-
</Container>
29-
30-
<FootBar />
31-
</Box>
26+
</Box>
27+
</Container>
28+
<FootBar />
29+
</Box>
30+
</Providers>
3231
</body>
3332
</html>
34-
</Providers>
3533
)
3634
}

0 commit comments

Comments
 (0)