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
6 changes: 6 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
@@ -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);
51 changes: 51 additions & 0 deletions src/api/resources.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { loadApiBaseUrl, saveApiBaseUrl } from "./apiSettings";
import Constants from "expo-constants";
import { Keypair, Transaction } from "stellar-sdk";

import type { CatalogFilters, RegistryStatus, Resource } from "../types";
import { logError } from "../utils/errorLogger";
Expand All @@ -8,6 +9,10 @@ const DEFAULT_API_BASE_URL =
(Constants.expoConfig?.extra?.apiUrl as string | undefined) ?? "http://localhost:4021";
let apiBaseUrl = DEFAULT_API_BASE_URL;

const DEFAULT_NETWORK_PASSPHRASE =
(Constants.expoConfig?.extra?.networkPassphrase as string | undefined) ??
"Test SDF Network ; September 2015";

function buildQuery(filters?: CatalogFilters): string {
if (!filters) return "";

Expand Down Expand Up @@ -69,3 +74,49 @@ export async function fetchRegistryStatus(): Promise<RegistryStatus> {
}
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<void> {
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;
}
164 changes: 136 additions & 28 deletions src/components/ResourceCard.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import * as Clipboard from "expo-clipboard";
import { useState } from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
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";
import { PaywallModal } from "./PaywallModal";

Expand Down Expand Up @@ -44,15 +52,40 @@
export function ResourceCard({ resource, onCopyUrl, onRegister }: ResourceCardProps) {
const verification = verificationStyle(resource.verificationStatus);
const onchain = onchainStyle(resource.onchainStatus);
const [paywallOpen, setPaywallOpen] = useState(false);
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<string | null>(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 (
<View style={shared.card}>

Check failure on line 88 in src/components/ResourceCard.tsx

View workflow job for this annotation

GitHub Actions / ci

JSX element 'View' has no corresponding closing tag.
<Text style={typography.cardTitle}>{resource.title}</Text>

{resource.publisherName ? (
Expand All @@ -64,23 +97,13 @@
</Text>

<View style={styles.badges}>
<View
style={[shared.badge, { backgroundColor: verification.backgroundColor }]}
accessibilityRole="text"
accessibilityLabel={`Verification status: ${resource.verificationStatus}`}
>
<Text style={[shared.badgeText, { color: verification.color }]}>
<View style={[shared.badge, { backgroundColor: verification.backgroundColor }]}>
<Text style={[shared.badgeText, { color: verification.color }]}>
{resource.verificationStatus}
</Text>
</View>
<View
style={[shared.badge, { backgroundColor: onchain.backgroundColor }]}
accessibilityRole="text"
accessibilityLabel={`On-chain status: ${
resource.onchainStatus === "none" ? "not on-chain" : resource.onchainStatus
}`}
>
<Text style={[shared.badgeText, { color: onchain.color }]}>
<View style={[shared.badge, { backgroundColor: onchain.backgroundColor }]}>
<Text style={[shared.badgeText, { color: onchain.color }]}>
{resource.onchainStatus === "none" ? "not on-chain" : resource.onchainStatus}
</Text>
</View>
Expand All @@ -99,34 +122,119 @@
</Pressable>
</View>

<PaywallModal
visible={paywallOpen}
accessUrl={resource.accessUrl}
resourceTitle={resource.title}
price={resource.price}
onClose={() => setPaywallOpen(false)}
/>
</View>
);
}
{editing ? (
<View style={styles.editor}>
<TextInput
value={newPrice}
onChangeText={setNewPrice}
placeholder="New price"
placeholderTextColor={colors.textSubtle}
keyboardType="numeric"
style={styles.input}
editable={!isBusy}
/>
<TextInput
value={secretKey}
onChangeText={setSecretKey}
placeholder="Stellar secret key"
placeholderTextColor={colors.textSubtle}
secureTextEntry
style={styles.input}
editable={!isBusy}
/>
<View style={styles.actionRow}>
<Pressable
onPress={() => setEditing(false)}
style={[shared.button, styles.secondaryButton]}
disabled={isBusy}
>
<Text style={shared.buttonText}>Cancel</Text>
</Pressable>
<Pressable
onPress={handleSavePrice}
style={[
shared.button,
styles.primaryButton,
isBusy ? styles.disabledButton : null,
]}
disabled={isBusy || !newPrice || !secretKey}
>
{isBusy ? (
<ActivityIndicator color="#ffffff" />
) : (
<Text style={[shared.buttonText, styles.primaryButtonText]}>Save Price</Text>
)}
</Pressable>
</View>
{statusLabel ? <Text style={styles.statusText}>{statusLabel}</Text> : null}
{error ? <Text style={styles.errorText}>{error}</Text> : null}
{successMessage ? <Text style={styles.successText}>{successMessage}</Text> : null}
</View>
) : (
<Pressable
onPress={() => setEditing(true)}
style={[shared.button, styles.editButton]}
>
<Text style={shared.buttonText}>Edit price</Text>
</Pressable>
)}

const styles = StyleSheet.create({
badges: {

Check failure on line 183 in src/components/ResourceCard.tsx

View workflow job for this annotation

GitHub Actions / ci

'}' expected.
flexDirection: "row",

Check failure on line 184 in src/components/ResourceCard.tsx

View workflow job for this annotation

GitHub Actions / ci

'}' expected.
flexWrap: "wrap",
gap: 6,
},

Check failure on line 187 in src/components/ResourceCard.tsx

View workflow job for this annotation

GitHub Actions / ci

Unexpected token. Did you mean `{'}'}` or `&rbrace;`?
footer: {
flexDirection: "row",

Check failure on line 189 in src/components/ResourceCard.tsx

View workflow job for this annotation

GitHub Actions / ci

'}' expected.
alignItems: "center",
justifyContent: "space-between",
marginTop: 4,
},

Check failure on line 193 in src/components/ResourceCard.tsx

View workflow job for this annotation

GitHub Actions / ci

Unexpected token. Did you mean `{'}'}` or `&rbrace;`?
actions: {
editor: {
marginTop: 16,

Check failure on line 195 in src/components/ResourceCard.tsx

View workflow job for this annotation

GitHub Actions / ci

'}' expected.
gap: 10,
},

Check failure on line 197 in src/components/ResourceCard.tsx

View workflow job for this annotation

GitHub Actions / ci

Unexpected token. Did you mean `{'}'}` or `&rbrace;`?
input: {
borderWidth: 1,

Check failure on line 199 in src/components/ResourceCard.tsx

View workflow job for this annotation

GitHub Actions / ci

'}' expected.
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,
},
registerBtn: {
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,
},
});
46 changes: 46 additions & 0 deletions src/hooks/useEditPrice.ts
Original file line number Diff line number Diff line change
@@ -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<EditPriceStatus>("idle");
const [error, setError] = useState<string | null>(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),
};
}
Loading