Skip to content

Commit

Permalink
Add polygon support
Browse files Browse the repository at this point in the history
  • Loading branch information
willyogo committed Jan 27, 2025
1 parent aec89d7 commit 6d0f8e6
Show file tree
Hide file tree
Showing 15 changed files with 345 additions and 130 deletions.
8 changes: 5 additions & 3 deletions src/components/chat/MessageInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ export function MessageInput() {
const [showGifPicker, setShowGifPicker] = useState(false);
const { login, address, isAuthenticated } = useAuth();
const room = useRoomStore((state) => state.room);
const version = useRoomStore((state) => state.version); // Subscribe to version changes
const version = useRoomStore((state) => state.version);
const { hasAccess, isLoading: checkingAccess } = useTokenGate(
room?.token_address || null,
room?.required_tokens || 0,
address
address,
room?.token_network || 'base'
);
const addMessage = useMessagesStore((state) => state.addMessage);
const emojiButtonRef = useRef<HTMLButtonElement>(null);
Expand Down Expand Up @@ -94,7 +95,8 @@ export function MessageInput() {
return <div className="text-center p-4">Checking access...</div>;
}

if (!hasAccess && room?.token_address) {
// Only show the token gate warning if the user is authenticated and doesn't have access
if (!hasAccess && room?.token_address && isAuthenticated) {
return (
<div className="text-center p-4 bg-yellow-50 text-yellow-800 rounded-lg">
Must hold {room.required_tokens} of token {room.token_address} to join chat
Expand Down
1 change: 1 addition & 0 deletions src/components/room/RoomHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export function RoomHeader({ room }: RoomHeaderProps) {
<TokenGateTooltip
tokenAddress={room.token_address}
requiredTokens={room.required_tokens}
network={room.token_network}
isOwner={isOwner}
onManageGate={() => setShowTokenGateModal(true)}
/>
Expand Down
25 changes: 18 additions & 7 deletions src/components/room/TokenGateInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,46 @@ import { useTokenSymbol } from '../../lib/hooks/useTokenSymbol';
import { useTokenGate } from '../../lib/hooks/useTokenGate';
import { useAuth } from '../../components/auth/useAuth';
import { useRoomStore } from '../../lib/store/room';
import { SUPPORTED_NETWORKS, type SupportedNetwork } from '../../lib/config';
import { useEffect } from 'react';

type TokenGateInfoProps = {
tokenAddress: string | null;
requiredTokens: number;
network?: SupportedNetwork;
isOwner: boolean;
onManageGate?: () => void;
};

export function TokenGateInfo({
tokenAddress: initialTokenAddress,
requiredTokens: initialRequiredTokens,
requiredTokens: initialRequiredTokens,
network: initialNetwork = 'base',
isOwner,
onManageGate
}: TokenGateInfoProps) {
const { address } = useAuth();
const room = useRoomStore(state => state.room);
const version = useRoomStore(state => state.version); // Subscribe to version changes
const version = useRoomStore(state => state.version);

// Use room store values, falling back to props
const tokenAddress = room?.token_address ?? initialTokenAddress;
const requiredTokens = room?.required_tokens ?? initialRequiredTokens;
const network = (room?.token_network ?? initialNetwork) as SupportedNetwork;

const { symbol, isLoading } = useTokenSymbol(tokenAddress);
const { hasAccess } = useTokenGate(tokenAddress, requiredTokens, address);
const { symbol, isLoading } = useTokenSymbol(tokenAddress, network);
const { hasAccess } = useTokenGate(tokenAddress, requiredTokens, address, network);

// Debug logging
useEffect(() => {
console.log('TokenGateInfo re-render:', {
version,
tokenAddress,
requiredTokens,
network,
roomState: room
});
}, [version, tokenAddress, requiredTokens, room]);
}, [version, tokenAddress, requiredTokens, network, room]);

if (!tokenAddress) {
if (isOwner && onManageGate) {
Expand All @@ -53,8 +58,12 @@ export function TokenGateInfo({
return null;
}

const baseScanUrl = `https://basescan.org/token/${tokenAddress}`;
const explorerUrl = network === 'polygon'
? `https://polygonscan.com/token/${tokenAddress}`
: `https://basescan.org/token/${tokenAddress}`;

const tokenDisplay = isLoading ? '...' : symbol || 'tokens';
const networkDisplay = SUPPORTED_NETWORKS[network].name;

return (
<div className="flex items-center gap-2 text-sm mt-3">
Expand All @@ -65,13 +74,15 @@ export function TokenGateInfo({
<span className="text-gray-600">
Required: {requiredTokens}{' '}
<a
href={baseScanUrl}
href={explorerUrl}
target="_blank"
rel="noopener noreferrer"
className="text-indigo-600 hover:text-indigo-700"
>
{tokenDisplay}
</a>
{' '}on{' '}
<span className="font-medium">{networkDisplay}</span>
</span>
{isOwner && onManageGate && (
<button
Expand Down
60 changes: 43 additions & 17 deletions src/components/room/TokenGateModal.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useState, useEffect } from 'react';
import { X } from 'lucide-react';
import { X, AlertCircle } from 'lucide-react';
import { isAddress } from 'viem';
import { supabase } from '../../lib/auth/supabase';
import { useAuth } from '../../components/auth/useAuth';
import { useRoomStore } from '../../lib/store/room';
import { type SupportedNetwork, SUPPORTED_NETWORKS } from '../../lib/config';

type TokenGateModalProps = {
roomName: string;
Expand All @@ -21,6 +22,7 @@ export function TokenGateModal({
const { address } = useAuth();
const [tokenAddress, setTokenAddress] = useState(currentTokenAddress || '');
const [requiredTokens, setRequiredTokens] = useState(currentRequiredTokens);
const [network, setNetwork] = useState<SupportedNetwork>('base');
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const setRoom = useRoomStore(state => state.setRoom);
Expand Down Expand Up @@ -66,15 +68,16 @@ export function TokenGateModal({
userAddress: normalizedAddress,
roomName: normalizedRoomName,
tokenAddress: tokenAddress || null,
requiredTokens: tokenAddress ? requiredTokens : 0
requiredTokens: tokenAddress ? requiredTokens : 0,
network
});

// First verify the room exists and user owns it
const { data: roomCheck, error: checkError } = await supabase
.from('rooms')
.select('name, owner_address')
.eq('name', normalizedRoomName)
.eq('owner_address', normalizedAddress)
.ilike('owner_address', normalizedAddress) // Use case-insensitive comparison
.single();

if (checkError) {
Expand All @@ -92,9 +95,10 @@ export function TokenGateModal({
.update({
token_address: tokenAddress || null,
required_tokens: tokenAddress ? requiredTokens : 0,
token_network: tokenAddress ? network : null,
})
.eq('name', normalizedRoomName)
.eq('owner_address', normalizedAddress)
.ilike('owner_address', normalizedAddress) // Use case-insensitive comparison
.select('*')
.single();

Expand Down Expand Up @@ -152,22 +156,44 @@ export function TokenGateModal({
</div>

{tokenAddress && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Required Tokens
</label>
<input
type="number"
value={requiredTokens}
onChange={(e) => setRequiredTokens(Number(e.target.value))}
min="0"
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
<>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Network
</label>
<select
value={network}
onChange={(e) => setNetwork(e.target.value as SupportedNetwork)}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
>
{Object.entries(SUPPORTED_NETWORKS).map(([key, chain]) => (
<option key={key} value={key}>
{chain.name}
</option>
))}
</select>
</div>

<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Required Tokens
</label>
<input
type="number"
value={requiredTokens}
onChange={(e) => setRequiredTokens(Number(e.target.value))}
min="0"
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
/>
</div>
</>
)}

{error && (
<p className="text-red-600 text-sm">{error}</p>
<div className="flex items-center gap-2 text-red-600 text-sm bg-red-50 p-3 rounded-lg">
<AlertCircle size={16} />
<p>{error}</p>
</div>
)}

<div className="flex justify-end gap-2 pt-4">
Expand Down
25 changes: 18 additions & 7 deletions src/components/room/TokenGateTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,48 @@ import { useTokenSymbol } from '../../lib/hooks/useTokenSymbol';
import { useTokenGate } from '../../lib/hooks/useTokenGate';
import { useAuth } from '../../components/auth/useAuth';
import { useRoomStore } from '../../lib/store/room';
import { SUPPORTED_NETWORKS, type SupportedNetwork } from '../../lib/config';

type TokenGateTooltipProps = {
tokenAddress: string | null;
requiredTokens: number;
network?: SupportedNetwork;
isOwner: boolean;
onManageGate?: () => void;
};

export function TokenGateTooltip({
tokenAddress: initialTokenAddress,
requiredTokens: initialRequiredTokens,
requiredTokens: initialRequiredTokens,
network: initialNetwork = 'base',
isOwner,
onManageGate
}: TokenGateTooltipProps) {
const { address } = useAuth();
const room = useRoomStore(state => state.room);
const version = useRoomStore(state => state.version); // Subscribe to version changes
const version = useRoomStore(state => state.version);
const [showTooltip, setShowTooltip] = useState(false);
const tooltipRef = useRef<HTMLDivElement>(null);
const timeoutRef = useRef<number>();

// Use room store values, falling back to props
const tokenAddress = room?.token_address ?? initialTokenAddress;
const requiredTokens = room?.required_tokens ?? initialRequiredTokens;
const network = (room?.token_network ?? initialNetwork) as SupportedNetwork;

const { symbol, isLoading } = useTokenSymbol(tokenAddress);
const { hasAccess } = useTokenGate(tokenAddress, requiredTokens, address);
const { symbol, isLoading } = useTokenSymbol(tokenAddress, network);
const { hasAccess } = useTokenGate(tokenAddress, requiredTokens, address, network);

// Debug logging
useEffect(() => {
console.log('TokenGateTooltip re-render:', {
version,
tokenAddress,
requiredTokens,
network,
roomState: room
});
}, [version, tokenAddress, requiredTokens, room]);
}, [version, tokenAddress, requiredTokens, network, room]);

useEffect(() => {
return () => {
Expand Down Expand Up @@ -84,8 +89,12 @@ export function TokenGateTooltip({
return null;
}

const baseScanUrl = `https://basescan.org/token/${tokenAddress}`;
const explorerUrl = network === 'polygon'
? `https://polygonscan.com/token/${tokenAddress}`
: `https://basescan.org/token/${tokenAddress}`;

const tokenDisplay = isLoading ? '...' : symbol || 'tokens';
const networkDisplay = SUPPORTED_NETWORKS[network].name;

return (
<div
Expand All @@ -112,13 +121,15 @@ export function TokenGateTooltip({
<div className="text-sm text-gray-600">
Required: {requiredTokens}{' '}
<a
href={baseScanUrl}
href={explorerUrl}
target="_blank"
rel="noopener noreferrer"
className="text-indigo-600 hover:text-indigo-700"
>
{tokenDisplay}
</a>
{' '}on{' '}
<span className="font-medium">{networkDisplay}</span>
</div>
{isOwner && onManageGate && (
<button
Expand Down
14 changes: 12 additions & 2 deletions src/lib/api/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { supabase } from '../auth/supabase';
import { isAddress } from 'viem';
import { getTokenOwner } from '../contracts/owner';
import type { Database } from '../types/supabase';
import type { SupportedNetwork } from '../config';

type Room = Database['public']['Tables']['rooms']['Row'];

Expand All @@ -22,7 +23,8 @@ export async function getRoom(name: string): Promise<Room | null> {
export async function upsertRoom(
name: string,
ownerAddress: string | null,
tokenAddress: string | null
tokenAddress: string | null,
network: SupportedNetwork = 'base'
): Promise<Room | null> {
try {
// Normalize room name to lowercase
Expand All @@ -35,14 +37,20 @@ export async function upsertRoom(
// If no owner address is provided and room name is a contract address
// try to get its owner
if (!ownerAddress && isAddress(normalizedName)) {
const contractOwner = await getTokenOwner(normalizedName);
console.log(`Attempting to detect owner for token ${normalizedName} on ${network}`);
const contractOwner = await getTokenOwner(normalizedName, network);

if (contractOwner) {
console.log(`Found owner ${contractOwner} for token ${normalizedName} on ${network}`);
ownerAddress = contractOwner;
} else {
console.log(`No owner found for token ${normalizedName} on ${network}`);
}
}

// If we still don't have an owner address, return null to prompt for input
if (!ownerAddress) {
console.log('No owner address found, will prompt for manual input');
return null;
}

Expand All @@ -53,6 +61,7 @@ export async function upsertRoom(
name: normalizedName,
owner_address: ownerAddress.toLowerCase(),
token_address: tokenAddress,
token_network: isAddress(normalizedName) ? network : null,
required_tokens: 0,
use_contract_avatar: isAddress(normalizedName)
})
Expand Down Expand Up @@ -87,6 +96,7 @@ export async function getPopularRooms(limit = 11): Promise<Room[]> {
name,
owner_address,
token_address,
token_network,
required_tokens,
avatar_url,
use_contract_avatar
Expand Down
14 changes: 14 additions & 0 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { GiphyFetch } from '@giphy/js-fetch-api';
import { type Chain, base, polygon } from 'viem/chains';

export const SUPPORTED_NETWORKS = {
base,
polygon
} as const;

export type SupportedNetwork = keyof typeof SUPPORTED_NETWORKS;

export const config = {
privyAppId: import.meta.env.VITE_PRIVY_APP_ID,
Expand All @@ -8,6 +16,11 @@ export const config = {
supabaseAnonKey: import.meta.env.VITE_SUPABASE_ANON_KEY,
etherscanApiKey: import.meta.env.VITE_ETHERSCAN_API_KEY,
giphyApiKey: import.meta.env.VITE_GIPHY_API_KEY || 'your-api-key',
polygonscanApiKey: import.meta.env.VITE_POLYGONSCAN_API_KEY,
rpcEndpoints: {
base: `https://base-mainnet.g.alchemy.com/v2/${import.meta.env.VITE_ALCHEMY_API_KEY}`,
polygon: `https://polygon-mainnet.g.alchemy.com/v2/${import.meta.env.VITE_ALCHEMY_API_KEY}`,
},
} as const;

// Validate required environment variables
Expand All @@ -19,6 +32,7 @@ const requiredEnvVars = {
VITE_SUPABASE_ANON_KEY: config.supabaseAnonKey,
VITE_ETHERSCAN_API_KEY: config.etherscanApiKey,
VITE_GIPHY_API_KEY: config.giphyApiKey,
VITE_POLYGONSCAN_API_KEY: config.polygonscanApiKey,
} as const;

Object.entries(requiredEnvVars).forEach(([key, value]) => {
Expand Down
Loading

0 comments on commit 6d0f8e6

Please sign in to comment.