Skip to content

Commit f6d290e

Browse files
authored
Merge pull request #60 from IQCoreTeam/fix/mobile-dispose-reequip-parity
Mobile skill dispose and re-equip parity
2 parents af627b4 + 0c1c61a commit f6d290e

5 files changed

Lines changed: 86 additions & 7 deletions

File tree

surfaces/webview/src/market/MarketScreen.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export function MarketScreen() {
4949
detail={state.marketDetail}
5050
owned={state.marketOwned.includes(state.marketDetail.card.name)}
5151
onBack={() => { send({ type: "ownedSkills" }); clearMarketDetail(); }}
52+
onOpenSkill={(card) => send({ type: "getSkillDetail", mint: card.id })}
5253
/>
5354
{state.buyCelebrate && <BuyCelebration />}
5455
</div>
@@ -195,6 +196,7 @@ export function MarketScreen() {
195196
key={card.id}
196197
card={card}
197198
owned={state.marketOwned.includes(card.name)}
199+
disposed={Object.values(state.marketDisposed).includes(card.id)}
198200
firing={state.firingSkill === card.name}
199201
onOpen={handleOpenCard}
200202
/>

surfaces/webview/src/market/SkillCardTile.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import { SkillIcon } from "../icons";
44
interface Props {
55
card: SkillCard;
66
owned?: boolean;
7+
disposed?: boolean;
78
firing?: boolean;
89
onOpen: (card: SkillCard) => void;
910
}
1011

11-
export function SkillCardTile({ card, owned, firing, onOpen }: Props) {
12+
export function SkillCardTile({ card, owned, disposed, firing, onOpen }: Props) {
1213
const priceSol = card.price ? (Number(card.price) / 1_000_000_000).toFixed(3) : null;
1314
return (
1415
<button
@@ -18,6 +19,7 @@ export function SkillCardTile({ card, owned, firing, onOpen }: Props) {
1819
"bg-zinc-900 border-zinc-800 hover:border-zinc-600 active:scale-[0.98]",
1920
firing ? "skill-firing border-green-500/60" : "",
2021
owned ? "border-l-2 border-l-green-500" : "",
22+
disposed ? "opacity-55 grayscale border-dashed border-zinc-700" : "",
2123
].join(" ")}
2224
>
2325
<div className="flex items-start gap-2">
@@ -36,6 +38,11 @@ export function SkillCardTile({ card, owned, firing, onOpen }: Props) {
3638
owned
3739
</span>
3840
)}
41+
{disposed && (
42+
<span className="shrink-0 rounded px-1 py-0.5 text-[10px] font-semibold bg-zinc-800 text-zinc-500">
43+
un-equipped
44+
</span>
45+
)}
3946
{firing && (
4047
<span className="shrink-0 rounded px-1 py-0.5 text-[10px] font-semibold bg-green-500/20 text-green-300 animate-pulse">
4148
casting

surfaces/webview/src/market/SkillDetailView.tsx

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
import { useState } from "react";
22
import { useStore } from "../state/store";
3-
import type { SkillDetail } from "../transport/protocol";
3+
import type { SkillCard, SkillDetail } from "../transport/protocol";
44
import { SkillIcon } from "../icons";
55

66
interface Props {
77
detail: SkillDetail;
88
owned: boolean;
99
onBack: () => void;
10+
onOpenSkill?: (card: SkillCard) => void;
1011
}
1112

12-
export function SkillDetailView({ detail, owned, onBack }: Props) {
13-
const { send } = useStore();
13+
export function SkillDetailView({ detail, owned, onBack, onOpenSkill }: Props) {
14+
const { state, send } = useStore();
1415
const [buying, setBuying] = useState(false);
1516
const [noteText, setNoteText] = useState("");
1617
const [noteGitLink, setNoteGitLink] = useState("");
1718
const { card, skillText, notes } = detail;
1819
const priceSol = card.price ? (Number(card.price) / 1_000_000_000).toFixed(3) : null;
20+
const disposed = Object.values(state.marketDisposed).includes(card.id);
1921

2022
function handleBuy() {
2123
setBuying(true);
@@ -40,6 +42,11 @@ export function SkillDetailView({ detail, owned, onBack }: Props) {
4042
owned
4143
</span>
4244
)}
45+
{disposed && (
46+
<span className="ml-auto shrink-0 rounded px-1.5 py-0.5 text-[10px] font-semibold bg-zinc-800 text-zinc-500">
47+
un-equipped
48+
</span>
49+
)}
4350
</header>
4451

4552
<div className="flex-1 overflow-y-auto p-3 space-y-4">
@@ -68,6 +75,25 @@ export function SkillDetailView({ detail, owned, onBack }: Props) {
6875
</div>
6976
)}
7077

78+
{Array.isArray(detail.requiredCards) && detail.requiredCards.length > 0 && (
79+
<div className="space-y-2">
80+
<p className="text-[11px] text-zinc-500 uppercase tracking-wide">Required skills</p>
81+
<div className="space-y-2">
82+
{detail.requiredCards.map((req) => (
83+
<button
84+
key={req.id}
85+
type="button"
86+
onClick={() => onOpenSkill?.(req)}
87+
className="w-full rounded-lg bg-zinc-900 border border-zinc-800 p-2.5 text-left active:bg-zinc-800"
88+
>
89+
<p className="text-xs font-medium text-zinc-200">{req.name}</p>
90+
<p className="mt-0.5 line-clamp-2 text-[11px] text-zinc-500">{req.description}</p>
91+
</button>
92+
))}
93+
</div>
94+
</div>
95+
)}
96+
7197
{Array.isArray(notes) && notes.length > 0 && (
7298
<div className="space-y-2">
7399
<p className="text-[11px] text-zinc-500 uppercase tracking-wide">Comments</p>
@@ -107,7 +133,29 @@ export function SkillDetailView({ detail, owned, onBack }: Props) {
107133
)}
108134
</div>
109135

110-
{!owned && (
136+
{owned && (
137+
<div className="shrink-0 border-t border-red-900/40 bg-gradient-to-t from-red-950/30 to-transparent p-3 pb-[max(0.75rem,env(safe-area-inset-bottom))]">
138+
<button
139+
onClick={() => send({ type: "disposeSkill", skillId: card.id })}
140+
className="w-full rounded-xl border border-red-500/30 bg-red-950/20 py-3 text-sm font-semibold text-red-400 active:bg-red-900/30"
141+
>
142+
Remove Skill
143+
</button>
144+
</div>
145+
)}
146+
147+
{!owned && disposed && (
148+
<div className="shrink-0 border-t border-green-800/40 bg-gradient-to-t from-green-900/30 to-transparent p-3 pb-[max(0.75rem,env(safe-area-inset-bottom))]">
149+
<button
150+
onClick={() => send({ type: "reEquipSkill", skillId: card.id })}
151+
className="w-full rounded-xl bg-green-600 py-3 text-sm font-semibold text-white active:bg-green-500"
152+
>
153+
Re-equip Skill
154+
</button>
155+
</div>
156+
)}
157+
158+
{!owned && !disposed && (
111159
<div className="shrink-0 border-t border-amber-700/40 bg-gradient-to-t from-amber-900/30 to-transparent p-3 pb-[max(0.75rem,env(safe-area-inset-bottom))]">
112160
<button
113161
onClick={handleBuy}

surfaces/webview/src/state/store.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export interface State {
5151
marketSearchError: string | null;
5252
marketDetail: SkillDetail | null;
5353
marketOwned: string[];
54+
marketDisposed: Record<string, string>;
5455
marketBalance: number | null;
5556
rpcStatus: RpcStatus | null;
5657
publishResult: { ok: boolean; mint?: string; error?: string } | null;
@@ -125,6 +126,7 @@ const initialState: State = {
125126
marketSearchError: null,
126127
marketDetail: null,
127128
marketOwned: [],
129+
marketDisposed: {},
128130
marketBalance: null,
129131
rpcStatus: null,
130132
publishResult: null,
@@ -358,8 +360,24 @@ function reducer(state: State, ev: Action): State {
358360
marketOwned: ev.ok ? [...state.marketOwned, ev.slug ?? ev.skillId] : state.marketOwned,
359361
buyCelebrate: ev.ok ? true : state.buyCelebrate,
360362
};
363+
case "disposeResult":
364+
return {
365+
...state,
366+
toast: ev.ok ? "Skill removed." : `Remove failed: ${ev.error ?? "unknown"}`,
367+
marketOwned: ev.ok ? state.marketOwned.filter((name) => name !== (ev.slug ?? ev.skillId)) : state.marketOwned,
368+
marketDisposed: ev.ok ? { ...state.marketDisposed, [ev.slug ?? ev.skillId]: ev.skillId } : state.marketDisposed,
369+
};
370+
case "reEquipResult":
371+
return {
372+
...state,
373+
toast: ev.ok ? "Skill re-equipped." : `Re-equip failed: ${ev.error ?? "unknown"}`,
374+
marketOwned: ev.ok ? [...state.marketOwned, ev.slug ?? ev.skillId] : state.marketOwned,
375+
marketDisposed: ev.ok
376+
? Object.fromEntries(Object.entries(state.marketDisposed).filter(([, mint]) => mint !== ev.skillId && mint !== ev.slug))
377+
: state.marketDisposed,
378+
};
361379
case "ownedSkills":
362-
return { ...state, marketOwned: ev.names };
380+
return { ...state, marketOwned: ev.names, marketDisposed: ev.disposedMints ?? {} };
363381
case "balance":
364382
return { ...state, marketBalance: ev.lamports };
365383
case "rpcStatus":

surfaces/webview/src/transport/protocol.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ export type ClientMessage =
123123
| { type: "searchSkills"; query: string; kind?: "skill" | "workflow" }
124124
| { type: "getSkillDetail"; mint: string }
125125
| { type: "buySkill"; skillId: string; creatorWallet?: string }
126+
| { type: "disposeSkill"; skillId: string }
127+
| { type: "reEquipSkill"; skillId: string }
126128
| { type: "ownedSkills" }
127129
| { type: "getBalance" }
128130
| { type: "getRpcStatus" }
@@ -186,7 +188,9 @@ export type ServerMessage =
186188
| { type: "searchError"; message: string }
187189
| { type: "skillDetail"; detail: import("@iqlabs-official/agent-sdk").SkillDetail }
188190
| { type: "buyResult"; skillId: string; ok: boolean; slug?: string; error?: string }
189-
| { type: "ownedSkills"; names: string[]; mints?: Record<string, string> }
191+
| { type: "disposeResult"; skillId: string; ok: boolean; slug?: string; error?: string }
192+
| { type: "reEquipResult"; skillId: string; ok: boolean; slug?: string; error?: string }
193+
| { type: "ownedSkills"; names: string[]; mints?: Record<string, string>; disposedMints?: Record<string, string> }
190194
| { type: "balance"; lamports: number | null }
191195
| { type: "skillActive"; name: string }
192196
| { type: "rpcStatus"; status: import("@iqlabs-official/agent-sdk").RpcStatus }

0 commit comments

Comments
 (0)