Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 53 additions & 15 deletions app/guilds.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {
View,
FlatList,
TextInput,
TouchableOpacity,
Text,
RefreshControl,
useColorScheme,
} from "react-native";
import { FlashList } from "@shopify/flash-list";
import { useRouter } from "expo-router";
import { useWallet } from "../src/features/wallet/useWallet";
import { useGuilds, type GuildListItem } from "../src/features/guilds/useGuilds";
Expand All @@ -33,6 +33,32 @@ type GuildListRow = {
status?: EnrichedMembership["status"];
};

const GuildCardListItem = React.memo(function GuildCardListItem({
item,
offlineCached,
onPress,
}: {
item: GuildListRow;
offlineCached: boolean;
onPress: (guildId: string) => void;
}) {
const handlePress = useCallback(() => {
onPress(item.guildId);
}, [item.guildId, onPress]);

return (
<GuildCard
name={item.guildName}
id={item.guildId}
isActive={item.isActive}
roleCount={item.roleCount}
status={item.status}
offlineCached={offlineCached}
onPress={handlePress}
/>
);
});

function rowsFromWalletGuilds(
guilds: GuildListItem[],
memberships: EnrichedMembership[],
Expand Down Expand Up @@ -114,6 +140,28 @@ export default function Guilds() {
}
}, [guildsQuery, membershipsQuery, queryClient, walletAddress]);

const handleGuildPress = useCallback(
(guildId: string) => {
router.push(`/guilds/${guildId}`);
},
[router],
);

const keyExtractor = useCallback((item: GuildListRow) => item.guildId, []);

const isShowingOfflineCache = staleState.isOffline && filteredGuilds.length > 0;

const renderItem = useCallback(
({ item }: { item: GuildListRow }) => (
<GuildCardListItem
item={item}
offlineCached={isShowingOfflineCache}
onPress={handleGuildPress}
/>
),
[handleGuildPress, isShowingOfflineCache],
);

if (!walletAddress) {
return (
<WalletRequired>
Expand Down Expand Up @@ -215,30 +263,20 @@ export default function Guilds() {
);

const isRefreshing = isRefetching || guildsQuery.isRefetching || membershipsQuery.isRefetching;
const isShowingOfflineCache = staleState.isOffline && filteredGuilds.length > 0;

return (
<WalletRequired>
<View className="flex-1 bg-background dark:bg-slate-900" testID="guilds-screen">
<AppHeader title="My Guilds" showBack />
<FlatList
<FlashList
data={filteredGuilds}
keyExtractor={(item) => item.guildId}
keyExtractor={keyExtractor}
contentContainerStyle={{ padding: 16 }}
estimatedItemSize={96}
testID="guilds-list"
ListHeaderComponent={searchHeader}
refreshControl={<RefreshControl refreshing={isRefreshing} onRefresh={handleRefresh} />}
renderItem={({ item }) => (
<GuildCard
name={item.guildName}
id={item.guildId}
isActive={item.isActive}
roleCount={item.roleCount}
status={item.status}
offlineCached={isShowingOfflineCache}
onPress={() => router.push(`/guilds/${item.guildId}`)}
/>
)}
renderItem={renderItem}
ListEmptyComponent={
<EmptyState
title="No Guilds Found"
Expand Down
4 changes: 2 additions & 2 deletions app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ export default function Index() {
}

if (!isConnected) {
return <Redirect href="/onboarding" />;
return <Redirect href="/onboarding" />>;
}

return <Redirect href="/profile" />;
return <Redirect href="/profile" />
}
107 changes: 99 additions & 8 deletions app/profile.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { View, Text, ScrollView, TouchableOpacity, RefreshControl } from "react-native";
import React, { useState, useRef, useEffect, useCallback } from "react";
import { View, Text, FlatList, TouchableOpacity, RefreshControl } from "react-native";
import React, { memo, useState, useRef, useEffect, useCallback } from "react";
import { useRouter } from "expo-router";
import { useWallet } from "../src/features/wallet/useWallet";
import { useWalletConnectModal } from "../src/features/wallet/WalletConnectProvider";
Expand All @@ -23,6 +23,73 @@ const CONNECTION_LABELS: Record<string, string> = {
embedded: "Embedded Wallet",
};

type DashboardNavItem = {
id: string;
title: string;
subtitle: string;
route: string;
testID: string;
};

const NAV_ITEMS: DashboardNavItem[] = [
{
id: "guilds",
title: "My Guilds",
subtitle: "View your memberships and roles",
route: "/guilds",
testID: "navigate-guilds-button",
},
{
id: "access-check",
title: "Access Check",
subtitle: "Verify resource access status",
route: "/access-check",
testID: "navigate-access-check-button",
},
{
id: "settings",
title: "App Settings",
subtitle: "Configuration and info",
route: "/settings",
testID: "navigate-settings-button",
},
];

const NAV_ITEM_HEIGHT = 96;
const getNavItemLayout = (_: unknown, index: number) => ({
length: NAV_ITEM_HEIGHT,
offset: NAV_ITEM_HEIGHT * index,
index,
});

const NavigationCard = memo(function NavigationCard({
item,
onPress,
}: {
item: DashboardNavItem;
onPress: (route: string) => void;
}) {
return (
<TouchableOpacity
onPress={() => onPress(item.route)}
activeOpacity={0.7}
className="mb-4"
accessibilityRole="link"
accessibilityLabel={item.title}
accessibilityHint={item.subtitle}
testID={item.testID}
>
<Card className="flex-row justify-between items-center">
<View>
<Text className="text-xl font-bold text-text dark:text-slate-100">{item.title}</Text>
<Text className="text-text-muted dark:text-slate-400">{item.subtitle}</Text>
</View>
<Text className="text-primary text-2xl">β†’</Text>
</Card>
</TouchableOpacity>
);
});

export default function Profile() {
const router = useRouter();
const { walletAddress, isConnected, connectionKind, isVerified, verifyOwnership, connectManually, disconnect } = useWallet();
Expand Down Expand Up @@ -145,16 +212,35 @@ export default function Profile() {

const isRefreshing = isConnected && (isManuallyRefreshing || membershipsQuery.isRefetching);

const handleNavPress = useCallback((route: string) => {
router.push(route as never);
}, [router]);

const renderNavItem = useCallback(
({ item }: { item: DashboardNavItem }) => (
<NavigationCard item={item} onPress={handleNavPress} />
),
[handleNavPress],
);

const keyExtractor = useCallback((item: DashboardNavItem) => item.id, []);

return (
<View className="flex-1 bg-background dark:bg-slate-900" testID="profile-screen">
<AppHeader title="Profile" />
<ScrollView
<FlatList
className="flex-1 px-4 py-6"
testID="profile-scroll-view"
refreshControl={
<RefreshControl refreshing={isRefreshing} onRefresh={handleRefresh} testID="profile-refresh-control" />
}
>
data={isConnected ? NAV_ITEMS : []}
renderItem={renderNavItem}
keyExtractor={keyExtractor}
getItemLayout={getNavItemLayout}
initialNumToRender={4}
maxToRenderPerBatch={8}
windowSize={5}
removeClippedSubviews
ListHeaderComponent={
<>
{staleState.isOffline ? (
<StaleDataBanner reason="offline" lastSyncedAt={staleState.lastSyncedAt} />
) : staleState.isStale && staleState.reason ? (
Expand Down Expand Up @@ -348,7 +434,12 @@ export default function Profile() {
</View>
</View>
)}
</ScrollView>
</>
}
refreshControl={
<RefreshControl refreshing={isRefreshing} onRefresh={handleRefresh} testID="profile-refresh-control" />
}
/>
</View>
);
}
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
},
"dependencies": {
"@babel/runtime": "^7.29.7",
"@ethersproject/shims": "^5.8.0",
"@ethersweet/shims": "^5.8.0",
"@guildpass/sdk": "github:Adamantine-Guild/guildpass-sdk",
"@privy-io/expo": "^0.70.3",
"@privy-io/expo-native-extensions": "^0.0.12",
Expand All @@ -32,22 +32,22 @@
"@walletconnect/modal-react-native": "^1.1.0",
"@walletconnect/react-native-compat": "^2.11.3",
"elliptic": "^6.6.1",
"expo": "~50.0.14",
"expo": "~57.0.14",
"expo-application": "~5.8.3",
"expo-build-properties": "^0.11.1",
"expo-camera": "~14.1.3",
"expo-clipboard": "~5.0.1",
"expo-haptics": "~12.8.1",
"expo-constants": "~15.4.5",
"expo-crypto": "~12.8.1",
"expo-linking": "~6.2.2",
"expo-linking": "^6.2.2",
"expo-local-authentication": "^57.0.1",
"expo-notifications": "~0.27.6",
"expo-router": "~3.4.8",
"expo-secure-store": "~12.8.1",
"expo-sqlite": "^57.0.1",
"expo-status-bar": "~1.11.1",
"expo-updates": "~0.24.13",
"expo-updates": "^0.24.13",
"expo-web-browser": "~12.8.2",
"fast-text-encoding": "^1.0.6",
"js-sha3": "^0.12.0",
Expand All @@ -60,6 +60,7 @@
"react-native-safe-area-context": "4.8.2",
"react-native-screens": "~3.29.0",
"react-native-webview": "13.6.4",
"shopify/flash-list": "^1.6.4",
"viem": "^2.55.2",
"zod": "^3.23.8",
"zustand": "^4.5.2"
Expand All @@ -79,4 +80,4 @@
"vitest": "^1.3.1"
},
"private": true
}
}
15 changes: 9 additions & 6 deletions src/components/GuildCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { View, Text, TouchableOpacity } from "react-native";
import React from "react";
import React, { memo } from "react";
import { Card } from "./Card";
import { RoleBadge } from "./RoleBadge";
import type { GuildPassStatus } from "../features/passes/passCache";
Expand All @@ -11,7 +11,7 @@ type GuildCardProps = {
roleCount: number;
status?: GuildPassStatus;
offlineCached?: boolean;
onPress: () => void;
onPress: (id: string) => void;
};

const STATUS_STYLES: Record<
Expand Down Expand Up @@ -50,7 +50,7 @@ const STATUS_STYLES: Record<
},
};

export const GuildCard = ({
const GuildCard = memo(({
name,
id,
isActive,
Expand All @@ -64,12 +64,12 @@ export const GuildCard = ({

return (
<TouchableOpacity
onPress={onPress}
onPress={() => onPress(id)}
activeOpacity={0.7}
accessibilityRole="button"
accessibilityLabel={`${name}, ${statusStyle.label.toLowerCase()}, ${roleCount} roles${
offlineCached ? ", cached offline" : ""
}`}
}}
>
<Card className="mb-4">
<View className="flex-row justify-between items-center mb-2">
Expand Down Expand Up @@ -98,4 +98,7 @@ export const GuildCard = ({
</Card>
</TouchableOpacity>
);
};
});

export { GuildCard };
export default GuildCard;
18 changes: 18 additions & 0 deletions tests/fixtures/largeMembership.fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { GuildListItem } from "../../src/features/guilds/useGuilds";

export const LARGE_MEMBERSHIP_SET_SIZE = 200;

export function generateLargeMembershipSet(count: number = LARGE_MEMBERSHIP_SET_SIZE): GuildListItem[] {
const statuses: GuildListItem["status"][] = ["active", "inactive", "expired", "revoked", "unknown"];
return Array.from({ length: count }, (_, i) => {
const isActive = i % 4 !== 0;
return {
id: `guild-${i+1}`,
name: `Guild ${i+1}`,
isActive,
roleCount: (i % 5) + 1,
status: statuses[i % statuses.length],
lastSyncedAt: Date.now() - i * 1000,
};
});
}