From c87df0167361759cb47bb7288de8f6427c857a05 Mon Sep 17 00:00:00 2001 From: Lacastar2000 Date: Tue, 23 Jun 2026 16:27:00 +0000 Subject: [PATCH] =?UTF-8?q?Edit=20Price=20flow=20(prepare=20=E2=86=92=20si?= =?UTF-8?q?gn=20=E2=86=92=20submit)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.ts | 6 ++ src/api/resources.ts | 47 ++++++++++ src/components/ResourceCard.tsx | 151 ++++++++++++++++++++++++++++++-- src/hooks/useEditPrice.ts | 46 ++++++++++ 4 files changed, 242 insertions(+), 8 deletions(-) create mode 100644 src/hooks/useEditPrice.ts diff --git a/index.ts b/index.ts index e5802d2..693b6df 100644 --- a/index.ts +++ b/index.ts @@ -1,5 +1,11 @@ +import { Buffer } from "buffer"; import { registerRootComponent } from "expo"; import App from "./App"; +if (typeof global.Buffer === "undefined") { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (global as any).Buffer = Buffer; +} + registerRootComponent(App); diff --git a/src/api/resources.ts b/src/api/resources.ts index 6ebfc70..bfc684d 100644 --- a/src/api/resources.ts +++ b/src/api/resources.ts @@ -1,10 +1,15 @@ import Constants from "expo-constants"; +import { Keypair, Transaction } from "stellar-sdk"; import type { CatalogFilters, RegistryStatus, Resource } from "../types"; const API_BASE = (Constants.expoConfig?.extra?.apiUrl as string | undefined) ?? "http://localhost:4021"; +const DEFAULT_NETWORK_PASSPHRASE = + (Constants.expoConfig?.extra?.networkPassphrase as string | undefined) ?? + "Test SDF Network ; September 2015"; + function buildQuery(filters?: CatalogFilters): string { if (!filters) return ""; @@ -37,6 +42,48 @@ export async function fetchRegistryStatus(): Promise { return res.json(); } +export async function prepareEditPrice( + resourceId: string, + price: string +): Promise<{ xdr: string; networkPassphrase?: string }> { + const response = await fetch(`${API_BASE}/resources/${encodeURIComponent(resourceId)}/price/prepare`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ price }), + }); + + if (!response.ok) { + const body = await response.text(); + throw new Error(body || "Failed to prepare price edit transaction."); + } + + return response.json(); +} + +export async function submitPriceEdit(resourceId: string, signedXdr: string): Promise { + const response = await fetch(`${API_BASE}/resources/${encodeURIComponent(resourceId)}/price`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ xdr: signedXdr }), + }); + + if (!response.ok) { + const body = await response.text(); + throw new Error(body || "Failed to submit signed price edit transaction."); + } +} + +export function signTransactionXdr( + xdr: string, + secretKey: string, + networkPassphrase: string = DEFAULT_NETWORK_PASSPHRASE +): string { + const transaction = new Transaction(xdr, networkPassphrase); + const keypair = Keypair.fromSecret(secretKey.trim()); + transaction.sign(keypair); + return transaction.toEnvelope().toXDR("base64"); +} + export function getApiBaseUrl(): string { return API_BASE; } diff --git a/src/components/ResourceCard.tsx b/src/components/ResourceCard.tsx index 23fbe85..54f110b 100644 --- a/src/components/ResourceCard.tsx +++ b/src/components/ResourceCard.tsx @@ -1,7 +1,16 @@ import * as Clipboard from "expo-clipboard"; -import { Pressable, StyleSheet, Text, View } from "react-native"; +import { useState } from "react"; +import { + ActivityIndicator, + Pressable, + StyleSheet, + Text, + TextInput, + View, +} from "react-native"; import type { Resource } from "../types"; +import { useEditPrice } from "../hooks/useEditPrice"; import { colors, shared, typography } from "../theme"; interface ResourceCardProps { @@ -41,12 +50,38 @@ function onchainStyle(status: Resource["onchainStatus"]) { export function ResourceCard({ resource, onCopyUrl }: ResourceCardProps) { const verification = verificationStyle(resource.verificationStatus); const onchain = onchainStyle(resource.onchainStatus); + const { status, error, editPrice, resetError } = useEditPrice(); + const [editing, setEditing] = useState(false); + const [newPrice, setNewPrice] = useState(resource.price); + const [secretKey, setSecretKey] = useState(""); + const [successMessage, setSuccessMessage] = useState(null); async function handleCopy() { await Clipboard.setStringAsync(resource.accessUrl); onCopyUrl("Resource URL copied"); } + async function handleSavePrice() { + resetError(); + setSuccessMessage(null); + const ok = await editPrice(resource.id, newPrice, secretKey); + if (ok) { + setSuccessMessage("Price edit submitted."); + setEditing(false); + setSecretKey(""); + } + } + + const isBusy = status !== "idle"; + const statusLabel = + status === "preparing" + ? "Preparing transaction…" + : status === "signing" + ? "Signing transaction…" + : status === "submitting" + ? "Submitting transaction…" + : null; + return ( {resource.title} @@ -60,13 +95,13 @@ export function ResourceCard({ resource, onCopyUrl }: ResourceCardProps) { - - + + {resource.verificationStatus} - - + + {resource.onchainStatus === "none" ? "not on-chain" : resource.onchainStatus} @@ -78,9 +113,63 @@ export function ResourceCard({ resource, onCopyUrl }: ResourceCardProps) { Copy URL - - ); -} + + {editing ? ( + + + + + setEditing(false)} + style={[shared.button, styles.secondaryButton]} + disabled={isBusy} + > + Cancel + + + {isBusy ? ( + + ) : ( + Save Price + )} + + + {statusLabel ? {statusLabel} : null} + {error ? {error} : null} + {successMessage ? {successMessage} : null} + + ) : ( + setEditing(true)} + style={[shared.button, styles.editButton]} + > + Edit price + + )} const styles = StyleSheet.create({ badges: { @@ -94,4 +183,50 @@ const styles = StyleSheet.create({ justifyContent: "space-between", marginTop: 4, }, + editor: { + marginTop: 16, + gap: 10, + }, + input: { + borderWidth: 1, + borderColor: colors.border, + borderRadius: 12, + backgroundColor: colors.surface, + paddingHorizontal: 12, + paddingVertical: 10, + color: colors.text, + fontSize: 14, + }, + actionRow: { + flexDirection: "row", + justifyContent: "space-between", + gap: 8, + }, + primaryButton: { + backgroundColor: colors.primary, + }, + primaryButtonText: { + color: "#ffffff", + }, + disabledButton: { + opacity: 0.6, + }, + statusText: { + color: colors.primary, + fontSize: 13, + }, + errorText: { + color: colors.danger, + fontSize: 13, + }, + successText: { + color: colors.success, + fontSize: 13, + }, + editButton: { + marginTop: 12, + }, + secondaryButton: { + backgroundColor: colors.neutralBg, + }, }); diff --git a/src/hooks/useEditPrice.ts b/src/hooks/useEditPrice.ts new file mode 100644 index 0000000..f717b5b --- /dev/null +++ b/src/hooks/useEditPrice.ts @@ -0,0 +1,46 @@ +import { useCallback, useState } from "react"; + +import { + prepareEditPrice, + signTransactionXdr, + submitPriceEdit, +} from "../api/resources"; + +export type EditPriceStatus = "idle" | "preparing" | "signing" | "submitting"; + +export function useEditPrice() { + const [status, setStatus] = useState("idle"); + const [error, setError] = useState(null); + + const editPrice = useCallback( + async (resourceId: string, price: string, secretKey: string) => { + setError(null); + setStatus("preparing"); + + try { + const { xdr, networkPassphrase } = await prepareEditPrice(resourceId, price); + setStatus("signing"); + + const signedXdr = signTransactionXdr(xdr, secretKey, networkPassphrase); + setStatus("submitting"); + + await submitPriceEdit(resourceId, signedXdr); + setStatus("idle"); + return true; + } catch (err) { + const message = err instanceof Error ? err.message : "Unable to update price."; + setError(message); + setStatus("idle"); + return false; + } + }, + [] + ); + + return { + status, + error, + editPrice, + resetError: () => setError(null), + }; +}