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
55 changes: 29 additions & 26 deletions app/guilds.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@
import { useMembership } from "../src/features/membership/useMembership";
import { AppHeader } from "../src/components/AppHeader";
import { GuildCard } from "../src/components/GuildCard";
import { LoadingState } from "../src/components/LoadingState";

Check warning on line 7 in app/guilds.tsx

View workflow job for this annotation

GitHub Actions / Validate

'LoadingState' is defined but never used
import { ErrorState } from "../src/components/ErrorState";

Check warning on line 8 in app/guilds.tsx

View workflow job for this annotation

GitHub Actions / Validate

'ErrorState' is defined but never used
import { EmptyState } from "../src/components/EmptyState";
import { WalletRequired } from "../src/components/WalletRequired";
import React from "react";

export default function Guilds() {
const router = useRouter();
const { walletAddress } = useWallet();
const { getMembership } = useMembership(walletAddress);

Check warning on line 16 in app/guilds.tsx

View workflow job for this annotation

GitHub Actions / Validate

'getMembership' is assigned a value but never used

// In a real app, you would fetch all guilds.
// For MVP, we'll show a few example guilds that the user can explore.
Expand All @@ -23,31 +24,33 @@
];

return (
<View className="flex-1 bg-background" testID="guilds-screen">
<AppHeader title="My Guilds" showBack />
<FlatList
data={exampleGuilds}
keyExtractor={(item) => item.id}
contentContainerStyle={{ padding: 16 }}
testID="guilds-list"
renderItem={({ item }) => (
<GuildCard
name={item.name}
id={item.id}
isActive={item.isActive}
roleCount={item.roleCount}
onPress={() => router.push(`/guilds/${item.id}`)}
/>
)}
ListEmptyComponent={
<EmptyState
title="No Guilds Found"
message="You are not a member of any guilds yet."
actionTitle="Explore Guilds"
onAction={() => {}}
/>
}
/>
</View>
<WalletRequired>
<View className="flex-1 bg-background" testID="guilds-screen">
<AppHeader title="My Guilds" showBack />
<FlatList
data={exampleGuilds}
keyExtractor={(item) => item.id}
contentContainerStyle={{ padding: 16 }}
testID="guilds-list"
renderItem={({ item }) => (
<GuildCard
name={item.name}
id={item.id}
isActive={item.isActive}
roleCount={item.roleCount}
onPress={() => router.push(`/guilds/${item.id}`)}
/>
)}
ListEmptyComponent={
<EmptyState
title="No Guilds Found"
message="You are not a member of any guilds yet."
actionTitle="Explore Guilds"
onAction={() => {}}
/>
}
/>
</View>
</WalletRequired>
);
}
183 changes: 87 additions & 96 deletions app/guilds/[guildId].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ErrorState } from "../../src/components/ErrorState";
import { Card } from "../../src/components/Card";
import { RoleBadge } from "../../src/components/RoleBadge";
import { StaleDataBanner } from "../../src/components/StaleDataBanner";
import { WalletRequired } from "../../src/components/WalletRequired";
import { useCombinedStaleState } from "../../src/features/offline/useStaleQuery";
import React from "react";

Expand All @@ -34,106 +35,96 @@ export default function GuildDetail() {

const staleState = useCombinedStaleState([guildQuery, membershipQuery, rolesQuery]);

if (!validGuildId) {
return <ErrorState message="Invalid guild ID provided" />;
}

if (
(guildPending && guildLoading) ||
(memPending && memLoading) ||
(rolesPending && rolesLoading)
) {
return <LoadingState message="Fetching guild details..." />;
}

if (guildError && !guild) {
return (
<ErrorState
message={
staleState.isOffline
? "You are offline. Please reconnect to load guild details."
: "Failed to load guild details"
}
/>
);
}

if (!guild) {
return (
<ErrorState
message={
staleState.isOffline
? "You are offline. Please reconnect to load guild details."
: "Failed to load guild details"
}
/>
);
}

return (
<View className="flex-1 bg-background" testID="guild-detail-screen">
<AppHeader title={guild.name} showBack />
<ScrollView className="flex-1 px-4 py-6">
{staleState.isOffline ? (
<StaleDataBanner reason="offline" lastSyncedAt={staleState.lastSyncedAt} />
) : staleState.isStale && staleState.reason ? (
<StaleDataBanner reason={staleState.reason} lastSyncedAt={staleState.lastSyncedAt} />
) : null}
<WalletRequired>
<View className="flex-1 bg-background" testID="guild-detail-screen">
{!validGuildId ? (
<ErrorState message="Invalid guild ID provided" />
) : (guildPending && guildLoading) || (memPending && memLoading) || (rolesPending && rolesLoading) ? (
<LoadingState message="Fetching guild details..." />
) : guildError && !guild ? (
<ErrorState
message={
staleState.isOffline
? "You are offline. Please reconnect to load guild details."
: "Failed to load guild details"
}
/>
) : !guild ? (
<ErrorState
message={
staleState.isOffline
? "You are offline. Please reconnect to load guild details."
: "Failed to load guild details"
}
/>
) : (
<>
<AppHeader title={guild.name} showBack />
<ScrollView className="flex-1 px-4 py-6">
{staleState.isOffline ? (
<StaleDataBanner reason="offline" lastSyncedAt={staleState.lastSyncedAt} />
) : staleState.isStale && staleState.reason ? (
<StaleDataBanner reason={staleState.reason} lastSyncedAt={staleState.lastSyncedAt} />
) : null}

<Card className="mb-6">
<Text className="text-2xl font-bold text-text mb-2" testID="guild-name">
{guild.name}
</Text>
<Text className="text-text-muted mb-4" testID="guild-description">
{guild.description || "No description provided."}
</Text>
<Card className="mb-6">
<Text className="text-2xl font-bold text-text mb-2" testID="guild-name">
{guild.name}
</Text>
<Text className="text-text-muted mb-4" testID="guild-description">
{guild.description || "No description provided."}
</Text>

<View className="border-t border-border pt-4">
<View className="flex-row justify-between mb-2">
<Text className="text-text-muted">Owner</Text>
<Text className="text-text font-medium" numberOfLines={1} testID="guild-owner">
{guild.ownerAddress.substring(0, 6)}...{guild.ownerAddress.substring(38)}
</Text>
</View>
<View className="flex-row justify-between">
<Text className="text-text-muted">Chain ID</Text>
<Text className="text-text font-medium" testID="guild-chain-id">
{guild.chainId}
</Text>
</View>
</View>
</Card>
<View className="border-t border-border pt-4">
<View className="flex-row justify-between mb-2">
<Text className="text-text-muted">Owner</Text>
<Text className="text-text font-medium" numberOfLines={1} testID="guild-owner">
{guild.ownerAddress.substring(0, 6)}...{guild.ownerAddress.substring(38)}
</Text>
</View>
<View className="flex-row justify-between">
<Text className="text-text-muted">Chain ID</Text>
<Text className="text-text font-medium" testID="guild-chain-id">
{guild.chainId}
</Text>
</View>
</View>
</Card>

<View className="mb-6">
<Text className="text-lg font-bold text-text mb-3">Your Membership</Text>
<Card
className={membership?.isActive ? "border-success/30" : ""}
accessibilityLabel={`Membership status: ${membership?.isActive ? "Active Member" : "Not a Member"}`}
testID="membership-status-card"
>
<View className="flex-row justify-between items-center">
<Text className="text-text font-medium">Status</Text>
<Text
className={`font-bold ${membership?.isActive ? "text-success" : "text-text-muted"}`}
testID="membership-status-text"
>
{membership?.isActive ? "Active Member" : "Not a Member"}
</Text>
</View>
</Card>
</View>
<View className="mb-6">
<Text className="text-lg font-bold text-text mb-3">Your Membership</Text>
<Card
className={membership?.isActive ? "border-success/30" : ""}
accessibilityLabel={`Membership status: ${membership?.isActive ? "Active Member" : "Not a Member"}`}
testID="membership-status-card"
>
<View className="flex-row justify-between items-center">
<Text className="text-text font-medium">Status</Text>
<Text
className={`font-bold ${membership?.isActive ? "text-success" : "text-text-muted"}`}
testID="membership-status-text"
>
{membership?.isActive ? "Active Member" : "Not a Member"}
</Text>
</View>
</Card>
</View>

<View className="mb-6">
<Text className="text-lg font-bold text-text mb-3">Available Roles</Text>
<View className="flex-row flex-wrap" testID="guild-roles-list">
{roles && roles.length > 0 ? (
roles.map((role: { id: string; name: string }) => <RoleBadge key={role.id} name={role.name} />)
) : (
<Text className="text-text-muted italic">No roles defined for this guild.</Text>
)}
</View>
</View>
</ScrollView>
</View>
<View className="mb-6">
<Text className="text-lg font-bold text-text mb-3">Available Roles</Text>
<View className="flex-row flex-wrap" testID="guild-roles-list">
{roles && roles.length > 0 ? (
roles.map((role: { id: string; name: string }) => <RoleBadge key={role.id} name={role.name} />)
) : (
<Text className="text-text-muted italic">No roles defined for this guild.</Text>
)}
</View>
</View>
</ScrollView>
</>
)}
</View>
</WalletRequired>
);
}
23 changes: 13 additions & 10 deletions app/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
import { AppHeader } from "../src/components/AppHeader";
import { Card } from "../src/components/Card";
import { Button } from "../src/components/Button";
import { WalletRequired } from "../src/components/WalletRequired";
import { appConfig } from "../src/config/appConfig";
import { resetAppState } from "../src/lib/resetAppState";
import React, { useState } from "react";

export default function Settings() {
const { isConnected } = useWallet();

Check warning on line 12 in app/settings.tsx

View workflow job for this annotation

GitHub Actions / Validate

'isConnected' is assigned a value but never used
const [isResetting, setIsResetting] = useState(false);

const handleReset = async () => {
Expand Down Expand Up @@ -51,16 +52,18 @@

<Text className="text-lg font-bold text-text mb-3">Account</Text>
<Card className="mb-8">
<Text className="text-text-muted mb-4">
will disconnect your current wallet address and clear any local cache.
</Text>
<Button
title="Reset App State"
onPress={handleReset}
variant="danger"
loading={isResetting}
disabled={!isConnected || isResetting}
/>
<WalletRequired redirect={false}>
<Text className="text-text-muted mb-4">
will disconnect your current wallet address and clear any local cache.
</Text>
<Button
title="Reset App State"
onPress={handleReset}
variant="danger"
loading={isResetting}
disabled={isResetting}
/>
</WalletRequired>
</Card>

<View className="items-center mt-12">
Expand Down
64 changes: 64 additions & 0 deletions src/components/WalletRequired.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { View, Text } from "react-native";
import React, { useEffect } from "react";
import { useRouter } from "expo-router";
import { useWallet } from "../features/wallet/useWallet";
import { Button } from "./Button";

interface WalletRequiredProps {
children: React.ReactNode;
/**
* When true (default), redirects to /profile if wallet is not connected.
* When false, renders an inline connect-wallet prompt instead.
*/
redirect?: boolean;
}

/**
* Route guard that ensures a wallet is connected before rendering
* wallet-scoped content. Redirects to /profile or shows a connect prompt.
*/
export function WalletRequired({
children,
redirect = true,
}: WalletRequiredProps) {
const { isConnected, isHydrated } = useWallet();
const router = useRouter();

useEffect(() => {
if (isHydrated && !isConnected && redirect) {
router.replace("/profile");
}
}, [isHydrated, isConnected, redirect, router]);

// Wait for store hydration to avoid flash of wrong state
if (!isHydrated) {
return null;
}

if (!isConnected) {
if (redirect) {
return null;
}

return (
<View
className="flex-1 justify-center items-center p-6 bg-background"
testID="wallet-required-prompt"
>
<Text className="text-text text-xl font-bold text-center mb-3">
Wallet connection required
</Text>
<Text className="text-text-muted text-center mb-8">
Please connect your wallet to access this screen.
</Text>
<Button
title="Connect Wallet"
onPress={() => router.replace("/profile")}
testID="wallet-required-connect"
/>
</View>
);
}

return <>{children}</>;
}
Loading
Loading