Skip to content
Draft
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
7 changes: 6 additions & 1 deletion client/src/components/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@ import { PlayerPage } from "./pages/player";
import { useProject } from "@/hooks/project";
import { MarketPage } from "./pages/market";
import { usePageTracking } from "@/hooks/usePageTracking";
import { StarterpackClaimPage } from "./pages/starterpack-claim";

export function App() {
// Initialize page tracking
usePageTracking();
const { player, collection } = useProject();
const { player, collection, starterpackId } = useProject();

if (starterpackId) {
return <StarterpackClaimPage />;
}

if (player) {
return <PlayerPage />;
Expand Down
30 changes: 30 additions & 0 deletions client/src/components/pages/starterpack-claim.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Button } from "@cartridge/ui";
import { useClaimViewModel } from "@/features/starterpack/useClaimViewModel";
import { Loader2 } from "lucide-react";

export function StarterpackClaimPage() {
const {
isConnected,
isLoading,
handleConnectAndClaim,
} = useClaimViewModel();

return (
<div className="flex items-center justify-center min-h-screen p-6">
<Button
onClick={handleConnectAndClaim}
disabled={isLoading}
className="h-12 text-lg font-semibold px-8"
>
{isLoading ? (
<>
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
{isConnected ? "Claiming..." : "Connecting..."}
</>
) : (
<>{isConnected ? "Claim" : "Connect & Claim"}</>
)}
</Button>
</div>
);
}
166 changes: 166 additions & 0 deletions client/src/features/starterpack/useClaimViewModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { useState, useCallback, useMemo } from "react";
import { useAccount, useConnect } from "@starknet-react/core";
import { useParams } from "@tanstack/react-router";
import { useReferralAttribution } from "@/hooks/useReferralAttribution";

/**
* Mock starterpack data structure
* TODO: Replace with actual data from contract in Phase 2
*/
interface StarterpackData {
id: string;
name: string;
description: string;
imageUri: string;
price: string;
paymentToken: string;
}

interface ClaimState {
isLoading: boolean;
error: string | null;
txHash?: string;
isSuccess: boolean;
}

/**
* View model hook for starterpack claiming page
*
* Manages:
* - Starterpack data fetching (mocked for now)
* - Wallet connection
* - Claim transaction flow
* - Referral attribution integration
*/
export function useClaimViewModel() {
const { starterpackId } = useParams({ from: "/starterpack/$starterpackId" });
const { account, isConnected, address } = useAccount();
const { connect, connectors } = useConnect();
const attribution = useReferralAttribution();

const [claimState, setClaimState] = useState<ClaimState>({
isLoading: false,
error: null,
isSuccess: false,
});

// Mock starterpack data
// TODO: Phase 2 - Replace with actual contract query
const starterpack = useMemo((): StarterpackData | null => {
if (!starterpackId) return null;

return {
id: starterpackId,
name: `Starterpack #${starterpackId}`,
description:
"Get started with exclusive items, resources, and bonuses to jumpstart your adventure!",
imageUri: "https://via.placeholder.com/400x400?text=Starterpack",
price: "0.01",
paymentToken: "ETH",
};
}, [starterpackId]);

/**
* Handle wallet connection
*/
const handleConnect = useCallback(async () => {
try {
setClaimState((prev) => ({ ...prev, isLoading: true, error: null }));

// Use the first available connector (typically Controller)
const connector = connectors[0];
if (!connector) {
throw new Error("No connector available");
}

await connect({ connector });

setClaimState((prev) => ({ ...prev, isLoading: false }));
} catch (error) {
console.error("Connection error:", error);
setClaimState({
isLoading: false,
error: error instanceof Error ? error.message : "Failed to connect wallet",
isSuccess: false,
});
}
}, [connect, connectors]);

/**
* Handle starterpack claim
* TODO: Phase 2 - Implement actual contract call
*/
const handleClaim = useCallback(async () => {
if (!account || !starterpackId) {
console.error("Missing account or starterpack ID");
return;
}

try {
setClaimState({ isLoading: true, error: null, isSuccess: false });

// TODO: Phase 2 - Replace with actual contract call
// const { provider } = useArcade();
// const calls = provider.starterpack.issue({
// starterpackId: Number(starterpackId),
// recipient: address,
// referrer: attribution.referrer,
// referrerGroup: attribution.referrerGroup
// });
// const result = await account.execute(calls);

// Mock transaction delay
await new Promise((resolve) => setTimeout(resolve, 2000));

setClaimState({
isLoading: false,
error: null,
txHash: "0xmock_transaction_hash",
isSuccess: true,
});
} catch (error) {
console.error("Claim error:", error);
setClaimState({
isLoading: false,
error: error instanceof Error ? error.message : "Failed to claim starterpack",
isSuccess: false,
});
}
}, [account, starterpackId, address, attribution]);

/**
* Combined connect & claim action
*/
const handleConnectAndClaim = useCallback(async () => {
if (!isConnected) {
await handleConnect();
// After connection, user needs to click claim again
// This prevents auto-claiming immediately after connection
return;
}

await handleClaim();
}, [isConnected, handleConnect, handleClaim]);

return {
// Data
starterpackId,
starterpack,
attribution: attribution.attribution,

// Connection state
isConnected,
address,

// Claim state
isLoading: claimState.isLoading,
error: claimState.error,
txHash: claimState.txHash,
isSuccess: claimState.isSuccess,

// Actions
handleConnect,
handleClaim,
handleConnectAndClaim,
};
}
16 changes: 16 additions & 0 deletions client/src/hooks/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ interface RouteParams {
collection?: string;
tab?: string;
token?: string;
starterpack?: string;
}

export const parseRouteParams = (pathname: string): RouteParams => {
Expand Down Expand Up @@ -67,6 +68,12 @@ export const parseRouteParams = (pathname: string): RouteParams => {
index += 1;
}
break;
case "starterpack":
if (next) {
params.starterpack = next;
index += 1;
}
break;
default:
if (!params.tab && TAB_SEGMENTS.includes(segment as any)) {
params.tab = segment;
Expand All @@ -92,6 +99,7 @@ export const useProject = () => {
edition: editionParam,
player: playerParam,
collection: collectionParam,
starterpack: starterpackParam,
tab,
} = useMemo(
() => parseRouteParams(routerState.location.pathname),
Expand Down Expand Up @@ -177,12 +185,20 @@ export const useProject = () => {
return undefined;
}, [playerData, playerParam]);

const starterpackId = useMemo(() => {
if (!starterpackParam) return undefined;
// Parse starterpack ID as number
const id = Number.parseInt(starterpackParam, 10);
return Number.isNaN(id) ? undefined : starterpackParam;
}, [starterpackParam]);

return {
game,
edition,
player,
filter,
collection,
tab,
starterpackId,
};
};
44 changes: 44 additions & 0 deletions client/src/hooks/useReferralAttribution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useEffect, useState } from "react";
import { useSearch } from "@tanstack/react-router";
import {
captureAttribution,
getAttribution,
type ReferralAttribution,
} from "@/lib/referral";

/**
* Hook to manage referral attribution
*
* Auto-captures referral parameters from URL on mount and provides
* access to current attribution state.
*
* @returns Current attribution state and validity
*/
export function useReferralAttribution() {
const search = useSearch({ strict: false });
const [attribution, setAttribution] = useState<ReferralAttribution | null>(
null,
);

// Capture attribution on mount if URL params exist
useEffect(() => {
// Get search params from TanStack Router
const searchParams = new URLSearchParams(
typeof search === "object" ? (search as Record<string, string>) : {},
);

// Attempt to capture attribution
captureAttribution(searchParams);

// Load current attribution state
const current = getAttribution();
setAttribution(current);
}, [search]);

return {
attribution,
hasAttribution: !!attribution,
referrer: attribution?.referrer,
referrerGroup: attribution?.referrerGroup,
};
}
Loading
Loading