diff --git a/packages/client/package.json b/packages/client/package.json index 86e949b..de791af 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -19,6 +19,8 @@ "dependencies": { "@emotion/babel-plugin": "^11.11.0", "@emotion/react": "^11.11.4", + "@farcaster/auth-client": "^0.1.1", + "@farcaster/auth-kit": "^0.3.1", "@phosphor-icons/react": "^2.1.4", "@svgr/rollup": "^8.1.0", "axios": "^1.6.8", @@ -28,6 +30,8 @@ "react-markdown": "^9.0.1", "react-router-dom": "^6.22.3", "react-syntax-highlighter": "^15.5.0", + "viem": "^2.17.0", + "vite-plugin-node-polyfills": "^0.22.0", "vite-plugin-top-level-await": "^1.4.1", "vite-tsconfig-paths": "^4.3.2" }, diff --git a/packages/client/src/components/FarcasterModal.tsx b/packages/client/src/components/FarcasterModal.tsx new file mode 100644 index 0000000..b072687 --- /dev/null +++ b/packages/client/src/components/FarcasterModal.tsx @@ -0,0 +1,69 @@ +import { useVoteManagementContext } from '@/context/voteManagement' +import useLocalStorage from '@/hooks/generic/useLocalStorage' +import { AuthClientError, QRCode, StatusAPIResponse } from '@farcaster/auth-kit' +import { DeviceMobileCamera } from '@phosphor-icons/react' +import React, { useEffect } from 'react' +import { Link } from 'react-router-dom' + +interface FarcasterModalProps { + url?: string + data: StatusAPIResponse | undefined + error: AuthClientError | undefined + onClose: () => void +} + +const FarcasterModal: React.FC = ({ url, data, error, onClose }) => { + const { setUser } = useVoteManagementContext() + const [farcasterAuth, setFarcasterUser] = useLocalStorage('farcasterAuth', null) + + useEffect(() => { + if (data && data.state === 'completed' && !farcasterAuth) { + setUser(data) + setFarcasterUser(data) + onClose() + } + }, [data, setFarcasterUser]) + + return ( +
+
+
+

Verify your account with farcaster

+ {!error && ( + <> +

Scan with your phone's camera to continue.

+ +

Need to create an account?

+ + + )} +
+ {!error && ( + <> + {url && ( +
+ +
+ )} + {url && ( +
+ + +

I'm using my phone

+ +
+ )} + + )} + {error && ( +

+ Your polling request has timed out after 5 minutes. This may occur if you haven't scanned the QR code using Farcaster, or if + there was an issue during the process. Please ensure you have completed the QR scan and try again. +

+ )} +
+
+ ) +} + +export default FarcasterModal diff --git a/packages/client/src/components/Modal.tsx b/packages/client/src/components/Modal.tsx index 949ba4a..c712890 100644 --- a/packages/client/src/components/Modal.tsx +++ b/packages/client/src/components/Modal.tsx @@ -5,9 +5,10 @@ interface ModalProps { show: boolean onClose: () => void children: React.ReactNode + className?: string | undefined } -const Modal: FC = ({ show, onClose, children }) => { +const Modal: FC = ({ show, onClose, children, className }) => { const modalRef = useRef(null) const closeModal = (e: React.MouseEvent) => { @@ -34,7 +35,9 @@ const Modal: FC = ({ show, onClose, children }) => { return (
-
+
{children} diff --git a/packages/client/src/context/voteManagement/VoteManagement.context.tsx b/packages/client/src/context/voteManagement/VoteManagement.context.tsx index 3891562..9f0ceca 100644 --- a/packages/client/src/context/voteManagement/VoteManagement.context.tsx +++ b/packages/client/src/context/voteManagement/VoteManagement.context.tsx @@ -2,7 +2,6 @@ import { createGenericContext } from '@/utils/create-generic-context' import { VoteManagementContextType, VoteManagementProviderProps } from '@/context/voteManagement' import { useWebAssemblyHook } from '@/hooks/wasm/useWebAssembly' import { useEffect, useState } from 'react' -import { SocialAuth } from '@/model/twitter.model' import useLocalStorage from '@/hooks/generic/useLocalStorage' import { VoteStateLite, VotingRound } from '@/model/vote.model' import { useEnclaveServer } from '@/hooks/enclave/useEnclaveServer' @@ -10,6 +9,7 @@ import { convertPollData, convertTimestampToDate } from '@/utils/methods' import { Poll, PollResult } from '@/model/poll.model' import { generatePoll } from '@/utils/generate-random-poll' import { handleGenericError } from '@/utils/handle-generic-error' +import { StatusAPIResponse } from '@farcaster/auth-client' const [useVoteManagementContext, VoteManagementContextProvider] = createGenericContext() @@ -17,8 +17,8 @@ const VoteManagementProvider = ({ children }: VoteManagementProviderProps) => { /** * Voting Management States **/ - const [socialAuth, setSocialAuth] = useLocalStorage('socialAuth', null) - const [user, setUser] = useState(socialAuth) + const [farcasterAuth, setFarcasterUser] = useLocalStorage('farcasterAuth', null) + const [user, setUser] = useState(farcasterAuth) const [roundState, setRoundState] = useState(null) const [votingRound, setVotingRound] = useState(null) const [roundEndDate, setRoundEndDate] = useState(null) @@ -59,7 +59,7 @@ const VoteManagementProvider = ({ children }: VoteManagementProviderProps) => { const logout = () => { setUser(null) - setSocialAuth(null) + setFarcasterUser(null) } const getRoundStateLite = async (roundCount: number) => { diff --git a/packages/client/src/context/voteManagement/VoteManagement.types.ts b/packages/client/src/context/voteManagement/VoteManagement.types.ts index be49614..f195882 100644 --- a/packages/client/src/context/voteManagement/VoteManagement.types.ts +++ b/packages/client/src/context/voteManagement/VoteManagement.types.ts @@ -1,14 +1,16 @@ import { ReactNode } from 'react' import * as WasmInstance from 'libs/wasm/pkg/crisp_web' -import { Auth, SocialAuth } from '@/model/twitter.model' + import { BroadcastVoteRequest, BroadcastVoteResponse, VoteStateLite, VotingRound } from '@/model/vote.model' import { Poll, PollRequestResult, PollResult } from '@/model/poll.model' +import { StatusAPIResponse } from '@farcaster/auth-client' +import { Auth } from '@/model/auth.model' export type VoteManagementContextType = { isLoading: boolean wasmInstance: WasmInstance.InitOutput | null encryptInstance: WasmInstance.Encrypt | null - user: SocialAuth | null + user: StatusAPIResponse | null votingRound: VotingRound | null roundEndDate: Date | null pollOptions: Poll[] @@ -25,7 +27,7 @@ export type VoteManagementContextType = { existNewRound: () => Promise getPastPolls: () => Promise setVotingRound: React.Dispatch> - setUser: (value: SocialAuth | null) => void + setUser: (value: StatusAPIResponse | null) => void initWebAssembly: () => Promise encryptVote: (voteId: bigint, publicKey: Uint8Array) => Promise broadcastVote: (vote: BroadcastVoteRequest) => Promise diff --git a/packages/client/src/globals.css b/packages/client/src/globals.css index 6db4da6..f51899e 100644 --- a/packages/client/src/globals.css +++ b/packages/client/src/globals.css @@ -264,6 +264,10 @@ footer { } } +._1n3pr306 svg g { + fill: #65a30d !important; +} + /* Custom Scrollbar */ /* Firefox */ /* \* { diff --git a/packages/client/src/hooks/enclave/useEnclaveServer.ts b/packages/client/src/hooks/enclave/useEnclaveServer.ts index fb33f32..c81f9f7 100644 --- a/packages/client/src/hooks/enclave/useEnclaveServer.ts +++ b/packages/client/src/hooks/enclave/useEnclaveServer.ts @@ -3,7 +3,8 @@ import { BroadcastVoteRequest, BroadcastVoteResponse, RoundCount, VoteStateLite import { useApi } from '../generic/useFetchApi' import { PollRequestResult } from '@/model/poll.model' import { fixPollResult, fixResult } from '@/utils/methods' -import { Auth } from '@/model/twitter.model' +import { Auth } from '@/model/auth.model' + const ENCLAVE_API = import.meta.env.VITE_ENCLAVE_API diff --git a/packages/client/src/hooks/twitter/useTwitter.ts b/packages/client/src/hooks/twitter/useTwitter.ts deleted file mode 100644 index 9ac4f7a..0000000 --- a/packages/client/src/hooks/twitter/useTwitter.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { handleGenericError } from '@/utils/handle-generic-error' -import { Twitter, SocialAuth } from '@/model/twitter.model' -import useLocalStorage from '@/hooks/generic/useLocalStorage' -import { useVoteManagementContext } from '@/context/voteManagement' -import { useApi } from '@/hooks/generic/useFetchApi' - -const TWITTER_API = import.meta.env.VITE_TWITTER_SERVERLESS_API - -if (!TWITTER_API) handleGenericError('useTwitter', { name: 'TWITTER_API', message: 'Missing env VITE_TWITTER_SERVERLESS_API' }) - -// Regex to match the expected Twitter post description -const MSG_REGEX = /🤫 I am authenticating with my Twitter account to cast my first encrypted vote with CRISP!\n\n#FHE #ZKP #CRISP/i - -export const useTwitter = () => { - const url = `${TWITTER_API}/twitter-data` - const { fetchData, isLoading } = useApi() - const [_socialAuth, setSocialAuth] = useLocalStorage('socialAuth', null) - const { setUser, getToken } = useVoteManagementContext() - - // Function to extract username from Twitter URL - const extractUsernameFromUrl = (url: string): string | null => { - const regex = /https:\/\/[^\/]+\/([^\/]+)\/status\/\d+/ - const match = url.match(regex) - return match ? match[1] : null - } - - // Function to extract post ID from Twitter URL - const extractPostId = (url: string): string | null => { - const regex = /\/status\/(\d+)$/ - const match = url.match(regex) - return match ? match[1] : null - } - - // Function to handle Twitter post verification - const handleTwitterPostVerification = async (postUrl: string) => { - const username = extractUsernameFromUrl(postUrl) - const result = await verifyPost(postUrl) - - if (result) { - // Convert the description to lowercase for case-insensitive matching - const descriptionLowerCase = result.description.toLowerCase() - - // Check if the description matches the regex - if (MSG_REGEX.test(descriptionLowerCase)) { - const postId = extractPostId(result.open_graph.url) ?? '' - const token = await getToken(postId) - - if (token && ['Authorized', 'Already Authorized'].includes(token.response)) { - const user = { - validationDate: new Date(), - avatar: result.open_graph.images[0].url ?? '', - username: username ?? '', - postId, - token: token.jwt_token, - } - setUser(user) - setSocialAuth(user) - } - } - } - } - - // API call to verify the Twitter post - const verifyPost = (postUrl: string) => fetchData(url, 'post', { url: postUrl }) - - return { - isLoading, - handleTwitterPostVerification, - } -} diff --git a/packages/client/src/main.tsx b/packages/client/src/main.tsx index bc54255..012f20d 100644 --- a/packages/client/src/main.tsx +++ b/packages/client/src/main.tsx @@ -5,15 +5,25 @@ import './globals.css' import { HashRouter } from 'react-router-dom' import { VoteManagementProvider } from '@/context/voteManagement/index.ts' import { NotificationAlertProvider } from './context/NotificationAlert/NotificationAlert.context.tsx' +import '@farcaster/auth-kit/styles.css' +import { AuthKitProvider } from '@farcaster/auth-kit' + +const config = { + relay: 'https://relay.farcaster.xyz', + domain: window.location.host, + siweUri: window.location.href, +} ReactDOM.createRoot(document.getElementById('root')!).render( - - - - - + + + + + + + , ) diff --git a/packages/client/src/model/auth.model.ts b/packages/client/src/model/auth.model.ts new file mode 100644 index 0000000..adf0bb6 --- /dev/null +++ b/packages/client/src/model/auth.model.ts @@ -0,0 +1,4 @@ +export interface Auth { + jwt_token: string + response: 'Already Authorized' | 'No Authorization' +} \ No newline at end of file diff --git a/packages/client/src/model/twitter.model.ts b/packages/client/src/model/twitter.model.ts deleted file mode 100644 index 6526fd9..0000000 --- a/packages/client/src/model/twitter.model.ts +++ /dev/null @@ -1,32 +0,0 @@ -export interface Twitter { - open_graph: OpenGraph - theme_color: string - description: string - favicon: string -} - -export interface OpenGraph { - site_name: string - type: string - url: string - title: string - description: string - images: Image[] -} - -export interface Image { - url: string -} - -export interface SocialAuth { - validationDate: Date - avatar: string - username: string - postId: string - token: string -} - -export interface Auth { - jwt_token: string - response: 'Already Authorized' | 'No Authorization' -} diff --git a/packages/client/src/pages/DailyPoll/DailyPoll.tsx b/packages/client/src/pages/DailyPoll/DailyPoll.tsx index 55d2cc6..959f343 100644 --- a/packages/client/src/pages/DailyPoll/DailyPoll.tsx +++ b/packages/client/src/pages/DailyPoll/DailyPoll.tsx @@ -43,7 +43,7 @@ const DailyPoll: React.FC = () => { return broadcastVote({ round_id: votingRound.round_id, enc_vote_bytes: Array.from(voteEncrypted), - postId: user.token, + postId: user.fid?.toString() ?? '', }) }, [broadcastVote, user, votingRound], diff --git a/packages/client/src/pages/Landing/components/DailyPoll.tsx b/packages/client/src/pages/Landing/components/DailyPoll.tsx index 2f6fbab..0d8b93c 100644 --- a/packages/client/src/pages/Landing/components/DailyPoll.tsx +++ b/packages/client/src/pages/Landing/components/DailyPoll.tsx @@ -1,13 +1,16 @@ -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' import { Poll } from '@/model/poll.model' import Card from '@/components/Cards/Card' import Modal from '@/components/Modal' import CircularTiles from '@/components/CircularTiles' -import RegisterModal from '@/pages/Register/Register' + import { useVoteManagementContext } from '@/context/voteManagement' import LoadingAnimation from '@/components/LoadingAnimation' import { hasPollEnded } from '@/utils/methods' import CountdownTimer from '@/components/CountdownTime' +import { useSignIn } from '@farcaster/auth-kit' + +import FarcasterModal from '@/components/FarcasterModal' type DailyPollSectionProps = { onVoted?: (vote: Poll) => void @@ -16,6 +19,10 @@ type DailyPollSectionProps = { } const DailyPollSection: React.FC = ({ onVoted, loading, endTime }) => { + const { url, connect, signIn, data, error } = useSignIn({ + timeout: 300000, + interval: 2000, + }) const { user, pollOptions, setPollOptions, roundState } = useVoteManagementContext() const isEnded = roundState ? hasPollEnded(roundState?.poll_length, roundState?.start_time) : false const status = roundState?.status @@ -28,6 +35,13 @@ const DailyPollSection: React.FC = ({ onVoted, loading, e setModalOpen(false) } + useEffect(() => { + const fetch = async () => { + await connect() + } + fetch() + }, []) + const handleChecked = (selectedId: number) => { const updatedOptions = pollOptions.map((option) => ({ ...option, @@ -39,7 +53,10 @@ const DailyPollSection: React.FC = ({ onVoted, loading, e } const castVote = () => { - if (!user) return openModal() + if (!user) { + signIn() + return openModal() + } if (pollSelected && onVoted) { onVoted(pollSelected) } @@ -103,8 +120,8 @@ const DailyPollSection: React.FC = ({ onVoted, loading, e )}
- - + + ) diff --git a/packages/client/src/pages/Register/Register.tsx b/packages/client/src/pages/Register/Register.tsx deleted file mode 100644 index dd2167c..0000000 --- a/packages/client/src/pages/Register/Register.tsx +++ /dev/null @@ -1,89 +0,0 @@ -// 'use client' - -import { useTwitter } from '@/hooks/twitter/useTwitter' -import React, { useState } from 'react' -interface RegisterProps { - onClose: () => void -} - -export const AUTH_MSG = `🤫 I am authenticating with my Twitter account to cast my first encrypted vote with CRISP! - -#FHE #ZKP #CRISP ` - -const RegisterModal: React.FC = ({ onClose }) => { - const [showVerification, setShowVerification] = useState(false) - const [postUrl, setPostUrl] = useState('') - const { isLoading, handleTwitterPostVerification } = useTwitter() - - const handlePost = () => { - if (!showVerification) { - window.open( - 'https://twitter.com/intent/post?text=%F0%9F%A4%AB%20I%20am%20authenticating%20with%20my%20Twitter%20account%20to%20cast%20my%20first%20encrypted%20vote%20with%20CRISP!%0A%0A%23FHE%20%23ZKP%20%23CRISP', - '_blank', - ) - setShowVerification(true) - } - } - - const handlePostVerification = async () => { - if (postUrl) { - await handleTwitterPostVerification(postUrl) - onClose() - } - } - - return ( -
- {showVerification ? ( - <> -
-

Submit Post URL

-

Share the link to your post

-
-
- - setPostUrl(target.value)} /> -
-
- - -
- - ) : ( - <> -
-

register

-

Verify your account

-
-
-

why am i doing this

-

- Since this is a simple single-use web app, we're creating an easy-to-use authentication system that only requires you to - validate ownership of your twitter account via a single post. -

-
-
-

WHAT HAPPENS NEXT?

-

- An address will be automatically generated and associated with your twitter account so you can easily authenticate and vote in - future polls. -

-
-
-