From bb4a10a3f1d0545bd3e915b3274d394df6e69fb0 Mon Sep 17 00:00:00 2001 From: Tarrence van As Date: Wed, 17 Sep 2025 08:49:33 -0500 Subject: [PATCH 1/6] feat: support viewing inventory without connected controller MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow users to view collectibles and inventory when they don't have a controller connected by using the owner address from the URL path. Changes: - Add useViewerAddress hook to handle address resolution from URL or connected controller - Update collection/collectible hooks to accept optional accountAddress parameter - Hide action buttons (Send, List, Purchase) when in view-only mode - Disable selection functionality when viewing without controller - Maintain full backward compatibility for existing functionality This enables read-only access to inventory pages for users browsing without authentication while preserving all interactive features for connected users. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../inventory/collection/collection-asset.tsx | 121 +++++++++--------- .../inventory/collection/collection.tsx | 117 +++++++++-------- .../inventory/collection/collections.tsx | 6 +- .../src/components/inventory/token/token.tsx | 32 +++-- .../src/components/inventory/token/tokens.tsx | 49 ++++--- packages/keychain/src/hooks/collectible.ts | 11 +- packages/keychain/src/hooks/collection.ts | 19 ++- packages/keychain/src/hooks/viewer.ts | 45 +++++++ 8 files changed, 251 insertions(+), 149 deletions(-) create mode 100644 packages/keychain/src/hooks/viewer.ts diff --git a/packages/keychain/src/components/inventory/collection/collection-asset.tsx b/packages/keychain/src/components/inventory/collection/collection-asset.tsx index 28ad3130b4..6aaa20c569 100644 --- a/packages/keychain/src/components/inventory/collection/collection-asset.tsx +++ b/packages/keychain/src/components/inventory/collection/collection-asset.tsx @@ -39,20 +39,22 @@ import { useAccount } from "@/hooks/account"; import { useConnection, useControllerTheme } from "@/hooks/connection"; import { useNavigation } from "@/context/navigation"; import { createExecuteUrl } from "@/utils/connection/execute"; +import { useViewerAddress } from "@/hooks/viewer"; const OFFSET = 10; export function CollectionAsset() { const { chainId, project } = useConnection(); const account = useAccount(); + const { address: viewerAddress, isViewOnly } = useViewerAddress(); const explorer = useExplorer(); - const address = account?.address || ""; + const address = viewerAddress || account?.address || ""; const [searchParams, setSearchParams] = useSearchParams(); const { navigate } = useNavigation(); const [cap, setCap] = useState(OFFSET); const theme = useControllerTheme(); const { editions } = useArcade(); - const { tokens } = useTokens(); + const { tokens } = useTokens(viewerAddress); const { provider, selfOrders, order, setAmount } = useMarketplace(); const [loading, setLoading] = useState(false); const edition: EditionModel | undefined = useMemo(() => { @@ -69,6 +71,7 @@ export function CollectionAsset() { } = useCollection({ contractAddress: contractAddress, tokenIds: tokenId ? [tokenId] : [], + accountAddress: viewerAddress, }); const { ownership, status: ownershipStatus } = useOwnership({ @@ -305,66 +308,68 @@ export function CollectionAsset() { - -
- - - - - + {!isViewOnly && ( + +
- - - - -
-
+ + + + + + + + + +
+
+ )} )} diff --git a/packages/keychain/src/components/inventory/collection/collection.tsx b/packages/keychain/src/components/inventory/collection/collection.tsx index e45f52e735..566c35de23 100644 --- a/packages/keychain/src/components/inventory/collection/collection.tsx +++ b/packages/keychain/src/components/inventory/collection/collection.tsx @@ -26,6 +26,7 @@ import { useControllerTheme } from "@/hooks/connection"; import { useArcade } from "@/hooks/arcade"; import { EditionModel, GameModel } from "@cartridge/arcade"; import { useMarketplace } from "@/hooks/marketplace"; +import { useViewerAddress } from "@/hooks/viewer"; export function Collection() { const { games, editions } = useArcade(); @@ -33,6 +34,7 @@ export function Collection() { const { project } = useConnection(); const { collectionOrders: orders } = useMarketplace(); const theme = useControllerTheme(); + const { address: viewerAddress, isViewOnly } = useViewerAddress(); const edition: EditionModel | undefined = useMemo(() => { return Object.values(editions).find( @@ -46,7 +48,10 @@ export function Collection() { const location = useLocation(); const [searchParams] = useSearchParams(); - const { collection, assets, status } = useCollection({ contractAddress }); + const { collection, assets, status } = useCollection({ + contractAddress, + accountAddress: viewerAddress, + }); // Use local state for selection instead of URL parameters const [selectedTokenIds, setSelectedTokenIds] = useState([]); @@ -119,23 +124,25 @@ export function Collection() { certified /> -
- -
- {selection - ? `${selectedTokenIds.length} Selected` - : "Select all"} + {!isViewOnly && ( +
+ +
+ {selection + ? `${selectedTokenIds.length} Selected` + : "Select all"} +
-
+ )}
{assets.map((asset) => { @@ -150,7 +157,7 @@ export function Collection() { state={location.state} key={asset.tokenId} onClick={(e: React.MouseEvent) => { - if (selection) { + if (selection && !isViewOnly) { e.preventDefault(); handleSelect(asset.tokenId); } @@ -165,10 +172,12 @@ export function Collection() { : `${asset.name} #${parseInt(BigInt(asset.tokenId).toString())}` } image={asset.imageUrl || placeholder} - selectable + selectable={!isViewOnly} selected={isSelected} listingCount={listingCount} - onSelect={() => handleSelect(asset.tokenId)} + onSelect={() => + !isViewOnly && handleSelect(asset.tokenId) + } className="rounded overflow-hidden" /> @@ -177,40 +186,42 @@ export function Collection() {
- -
- - - - - - -
-
+ + + + + +
+ + )} )} diff --git a/packages/keychain/src/components/inventory/collection/collections.tsx b/packages/keychain/src/components/inventory/collection/collections.tsx index c094e74133..64ba621af1 100644 --- a/packages/keychain/src/components/inventory/collection/collections.tsx +++ b/packages/keychain/src/components/inventory/collection/collections.tsx @@ -7,13 +7,15 @@ import { useCollectibles } from "@/hooks/collectible"; import { useArcade } from "@/hooks/arcade"; import { useConnection, useControllerTheme } from "@/hooks/connection"; import { EditionModel } from "@cartridge/arcade"; +import { useViewerAddress } from "@/hooks/viewer"; import { getChecksumAddress } from "starknet"; import { useMarketplace } from "@/hooks/marketplace"; export function Collections() { - const { collections, status: CollectionsStatus } = useCollections(); - const { collectibles, status: CollectiblesStatus } = useCollectibles(); + const { address } = useViewerAddress(); + const { collections, status: CollectionsStatus } = useCollections(address); + const { collectibles, status: CollectiblesStatus } = useCollectibles(address); const { editions } = useArcade(); const { getCollectionOrders } = useMarketplace(); const { project } = useConnection(); diff --git a/packages/keychain/src/components/inventory/token/token.tsx b/packages/keychain/src/components/inventory/token/token.tsx index d05498eda1..f50062eb91 100644 --- a/packages/keychain/src/components/inventory/token/token.tsx +++ b/packages/keychain/src/components/inventory/token/token.tsx @@ -22,6 +22,7 @@ import { useCallback, useMemo } from "react"; import { useConnection } from "@/hooks/connection"; import { useVersion } from "@/hooks/version"; import { useNavigation } from "@/context/navigation"; +import { useViewerAddress } from "@/hooks/viewer"; export function Token() { const { address } = useParams<{ address: string }>(); @@ -38,6 +39,7 @@ function Credits() { // TODO: Get parent from keychain connection if needed const { navigate } = useNavigation(); const account = useAccount(); + const { isViewOnly } = useViewerAddress(); const username = account?.username || ""; const credit = useCreditBalance({ username, @@ -70,15 +72,17 @@ function Credits() { - - - + {!isViewOnly && ( + + + + )} ); } @@ -113,14 +117,18 @@ const CreditsLoadingState = () => { function ERC20() { const { address } = useParams<{ address: string }>(); const account = useAccount(); - const accountAddress = account?.address || ""; + const { address: viewerAddress, isViewOnly } = useViewerAddress(); + const accountAddress = viewerAddress || account?.address || ""; const { controller } = useConnection(); const explorer = useExplorer(); const { transfers } = useData(); const { isControllerGte } = useVersion(); const chainId = constants.StarknetChainId.SN_MAIN; // Use mainnet as default - const { token } = useToken({ tokenAddress: address! }); + const { token } = useToken({ + tokenAddress: address!, + accountAddress: viewerAddress, + }); const [searchParams] = useSearchParams(); const compatibility = useMemo(() => { @@ -225,7 +233,7 @@ function ERC20() { - {compatibility && controller && ( + {compatibility && controller && !isViewOnly && (