Skip to content

Commit 5f29d6c

Browse files
committed
feat: farcaster miniapp support
1 parent 009b123 commit 5f29d6c

File tree

13 files changed

+460
-8
lines changed

13 files changed

+460
-8
lines changed

.github/workflows/netlify.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ jobs:
4242
CUSTOM_ORGANIZATION_DOMAINS: ${{ secrets.CUSTOM_ORGANIZATION_DOMAINS }}
4343
STRIPE_PUBLIC_KEY: ${{ secrets.STRIPE_PUBLIC_KEY }}
4444
CALCOM_EVENT_SLUG: ${{ secrets.CALCOM_EVENT_SLUG }}
45+
APP_DOMAIN: vocdoni-app-dev.netlify.app
4546

4647
- name: Deploy to Netlify (dev)
4748
uses: nwtgck/[email protected]

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
"@ethersproject/abstract-signer": "^5.7.0",
2222
"@ethersproject/address": "^5.7.0",
2323
"@ethersproject/providers": "^5.7.0",
24+
"@farcaster/miniapp-sdk": "^0.2.1",
25+
"@farcaster/miniapp-wagmi-connector": "^1.1.0",
2426
"@fontsource/inter": "^5.2.5",
2527
"@gsap/react": "^2.1.2",
2628
"@lexical/code": "^0.34.0",

pnpm-lock.yaml

Lines changed: 112 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.well-known

src/components/Process/Aside.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Link as ReactRouterLink } from 'react-router-dom'
99
import { useAccount } from 'wagmi'
1010
import { CensusMeta, CensusTypes } from './Census/CensusType'
1111
import { CspAuth } from './CSP/CSPAuthModal'
12+
import { isFarcasterMiniapp } from './farcaster-utils'
1213
import LogoutButton from './LogoutButton'
1314

1415
const results = (result: number, decimals?: number) =>
@@ -179,13 +180,25 @@ export const CensusConnectButton = () => {
179180
const isCSP = election.census.type === CensusType.CSP
180181
const isSpreadsheet = census?.type === CensusTypes.Spreadsheet
181182
const isWeb3 = census?.type === CensusTypes.Web3
183+
const isInFarcaster = isFarcasterMiniapp()
182184

183185
return (
184186
<>
185187
{isWeb3 && !connected && (
186-
<Button colorScheme='black' onClick={openConnectModal} w='full'>
187-
{t('menu.connect').toString()}
188-
</Button>
188+
<>
189+
{isInFarcaster ? (
190+
// In Farcaster miniapp: show status message
191+
// The connection happens automatically in FarcasterAwareClientProvider
192+
<Text fontSize='sm' textAlign='center' color='texts.subtle'>
193+
{t('farcaster.connecting')}
194+
</Text>
195+
) : (
196+
// Not in Farcaster: show RainbowKit connect button
197+
<Button colorScheme='black' onClick={openConnectModal} w='full'>
198+
{t('menu.connect').toString()}
199+
</Button>
200+
)}
201+
</>
189202
)}
190203
{isCSP && !connected && <CspAuth />}
191204
{isSpreadsheet && !connected && <SpreadsheetAccess />}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import type { Signer } from '@ethersproject/abstract-signer'
2+
import type { ExternalProvider } from '@ethersproject/providers'
3+
import { Web3Provider } from '@ethersproject/providers'
4+
import { sdk } from '@farcaster/miniapp-sdk'
5+
6+
/**
7+
* Check if the app is running inside a Farcaster miniapp context
8+
*/
9+
export const isFarcasterMiniapp = (): boolean => {
10+
// Check if APP_DOMAIN is set (feature flag)
11+
if (!import.meta.env.APP_DOMAIN) {
12+
return false
13+
}
14+
15+
// Check if running in browser (not SSR)
16+
if (typeof window === 'undefined') {
17+
return false
18+
}
19+
20+
// Basic check - will be validated during SDK initialization
21+
return true
22+
}
23+
24+
/**
25+
* Initialize the Farcaster SDK
26+
* Must be called after the app loads to hide the splash screen
27+
*/
28+
export const initializeFarcasterSDK = async (): Promise<void> => {
29+
if (!isFarcasterMiniapp()) {
30+
return
31+
}
32+
33+
try {
34+
// Check if context is available
35+
const context = await sdk.context
36+
if (!context || !context.client) {
37+
throw new Error('Not running in Farcaster miniapp context')
38+
}
39+
40+
await sdk.actions.ready()
41+
console.log('✅ Farcaster SDK initialized')
42+
} catch (error) {
43+
console.error('❌ Failed to initialize Farcaster SDK:', error)
44+
throw error
45+
}
46+
}
47+
48+
/**
49+
* Get the Ethereum provider from Farcaster wallet
50+
* Returns an EIP-1193 compatible provider
51+
*/
52+
export const getFarcasterEthereumProvider = async () => {
53+
if (!isFarcasterMiniapp()) {
54+
throw new Error('Not running in Farcaster miniapp context')
55+
}
56+
57+
try {
58+
const provider = await sdk.wallet.getEthereumProvider()
59+
if (!provider) {
60+
throw new Error('Farcaster Ethereum provider not available')
61+
}
62+
return provider
63+
} catch (error) {
64+
console.error('❌ Failed to get Farcaster Ethereum provider:', error)
65+
throw error
66+
}
67+
}
68+
69+
/**
70+
* Convert Farcaster's Ethereum provider to an ethers.js Signer
71+
* This is compatible with the Vocdoni SDK which expects an ethers v5 signer
72+
*/
73+
export const farcasterSignerAdapter = async (): Promise<Signer | null> => {
74+
if (!isFarcasterMiniapp()) {
75+
return null
76+
}
77+
78+
try {
79+
const provider = await getFarcasterEthereumProvider()
80+
const web3Provider = new Web3Provider(provider as ExternalProvider)
81+
82+
// Request account access
83+
const accounts = (await provider.request({ method: 'eth_requestAccounts' })) as string[]
84+
if (!accounts || accounts.length === 0) {
85+
throw new Error('No accounts found in Farcaster wallet')
86+
}
87+
88+
const signer = web3Provider.getSigner(accounts[0])
89+
console.log('✅ Farcaster signer created for account:', accounts[0])
90+
91+
return signer
92+
} catch (error) {
93+
console.error('❌ Failed to create Farcaster signer:', error)
94+
return null
95+
}
96+
}
97+
98+
/**
99+
* Get the connected wallet address from Farcaster
100+
*/
101+
export const getFarcasterAddress = async (): Promise<string | null> => {
102+
if (!isFarcasterMiniapp()) {
103+
return null
104+
}
105+
106+
try {
107+
const provider = await getFarcasterEthereumProvider()
108+
const accounts = (await provider.request({ method: 'eth_accounts' })) as string[]
109+
return accounts && accounts.length > 0 ? accounts[0] : null
110+
} catch (error) {
111+
console.error('❌ Failed to get Farcaster address:', error)
112+
return null
113+
}
114+
}

0 commit comments

Comments
 (0)