diff --git a/bun.lock b/bun.lock index c3a9814..b1fb16a 100644 --- a/bun.lock +++ b/bun.lock @@ -14,15 +14,18 @@ }, "example": { "name": "example", - "version": "0.1.34", + "version": "0.1.37", "dependencies": { "@expo/vector-icons": "~14.0.4", + "@react-native-menu/menu": "^1.2.3", "@react-navigation/native": "^7.0.14", "expo": "~52.0.46", "expo-build-properties": "~0.13.2", + "expo-clipboard": "~7.0.1", "expo-crypto": "~14.0.2", "expo-dev-client": "~5.0.20", "expo-font": "~13.0.4", + "expo-haptics": "~14.0.1", "expo-linking": "~7.0.5", "expo-router": "~4.0.20", "expo-secure-store": "~14.0.1", @@ -48,7 +51,7 @@ }, "package": { "name": "react-native-candle", - "version": "0.1.34", + "version": "0.1.37", "devDependencies": { "@expo/config-plugins": "^9.0.10", "@release-it/bumper": "^7.0.5", @@ -544,6 +547,8 @@ "@react-native-community/cli-types": ["@react-native-community/cli-types@18.0.0", "", { "dependencies": { "joi": "^17.2.1" } }, "sha512-J84+4IRXl8WlVdoe1maTD5skYZZO9CbQ6LNXEHx1kaZcFmvPZKfjsaxuyQ+8BsSqZnM2izOw8dEWnAp/Zuwb0w=="], + "@react-native-menu/menu": ["@react-native-menu/menu@1.2.3", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-sEfiVIivsa0lSelFm9Wbm/RAi+XoEHc75GGhjwvSrj9KSCVvNNXwr9F8l42e1t/lzYvVYzmkYxLG6VKxrDYJiw=="], + "@react-native/assets-registry": ["@react-native/assets-registry@0.76.9", "", {}, "sha512-pN0Ws5xsjWOZ8P37efh0jqHHQmq+oNGKT4AyAoKRpxBDDDmlAmpaYjer9Qz7PpDKF+IUyRjF/+rBsM50a8JcUg=="], "@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.76.9", "", { "dependencies": { "@react-native/codegen": "0.76.9" } }, "sha512-vxL/vtDEIYHfWKm5oTaEmwcnNGsua/i9OjIxBDBFiJDu5i5RU3bpmDiXQm/bJxrJNPRp5lW0I0kpGihVhnMAIQ=="], @@ -1068,6 +1073,8 @@ "expo-build-properties": ["expo-build-properties@0.13.3", "", { "dependencies": { "ajv": "^8.11.0", "semver": "^7.6.0" }, "peerDependencies": { "expo": "*" } }, "sha512-gw7AYP+YF50Gr912BedelRDTfR4GnUEn9p5s25g4nv0hTJGWpBZdCYR5/Oi2rmCHJXxBqhPjxzV7JRh72fntLg=="], + "expo-clipboard": ["expo-clipboard@7.0.1", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-rqYk0+WoqitPcPKxmMxSpLonX1E5Ije3LBYfnYMbH3xU5Gr8EAH9QnOWOi4BgahUPvcot6nbFEnx+DqARrmxKQ=="], + "expo-constants": ["expo-constants@17.0.8", "", { "dependencies": { "@expo/config": "~10.0.11", "@expo/env": "~0.4.2" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-XfWRyQAf1yUNgWZ1TnE8pFBMqGmFP5Gb+SFSgszxDdOoheB/NI5D4p7q86kI2fvGyfTrxAe+D+74nZkfsGvUlg=="], "expo-crypto": ["expo-crypto@14.0.2", "", { "dependencies": { "base64-js": "^1.3.0" }, "peerDependencies": { "expo": "*" } }, "sha512-WRc9PBpJraJN29VD5Ef7nCecxJmZNyRKcGkNiDQC1nhY5agppzwhqh7zEzNFarE/GqDgSiaDHS8yd5EgFhP9AQ=="], @@ -1084,6 +1091,8 @@ "expo-font": ["expo-font@13.0.4", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-eAP5hyBgC8gafFtprsz0HMaB795qZfgJWqTmU0NfbSin1wUuVySFMEPMOrTkTgmazU73v4Cb4x7p86jY1XXYUw=="], + "expo-haptics": ["expo-haptics@14.0.1", "", { "peerDependencies": { "expo": "*" } }, "sha512-V81FZ7xRUfqM6uSI6FA1KnZ+QpEKnISqafob/xEfcx1ymwhm4V3snuLWWFjmAz+XaZQTqlYa8z3QbqEXz7G63w=="], + "expo-json-utils": ["expo-json-utils@0.14.0", "", {}, "sha512-xjGfK9dL0B1wLnOqNkX0jM9p48Y0I5xEPzHude28LY67UmamUyAACkqhZGaPClyPNfdzczk7Ej6WaRMT3HfXvw=="], "expo-keep-awake": ["expo-keep-awake@14.0.3", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-6Jh94G6NvTZfuLnm2vwIpKe3GdOiVBuISl7FI8GqN0/9UOg9E0WXXp5cDcfAG8bn80RfgLJS8P7EPUGTZyOvhg=="], diff --git a/example/app/(tabs)/AssetsScreens/get-asset-accounts.tsx b/example/app/(tabs)/AssetsScreens/get-asset-accounts.tsx index 3cb4189..5e684a5 100644 --- a/example/app/(tabs)/AssetsScreens/get-asset-accounts.tsx +++ b/example/app/(tabs)/AssetsScreens/get-asset-accounts.tsx @@ -1,108 +1,162 @@ +import { Ionicons } from "@expo/vector-icons"; +import type { NativeActionEvent } from "@react-native-menu/menu"; +import { MenuComponentRef, MenuView } from "@react-native-menu/menu"; import { useNavigation } from "@react-navigation/native"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; +import { Alert, SectionList, StyleSheet, Text, View } from "react-native"; import { - ActivityIndicator, - Alert, - RefreshControl, - ScrollView, - StyleSheet, - Text, -} from "react-native"; -import { AssetAccount, LinkedAccountStatusRef } from "react-native-candle"; + AssetAccountQuery, + GetAssetAccountsResponse, +} from "react-native-candle"; import { SafeAreaView } from "react-native-safe-area-context"; import { useCandleClient } from "../../Context/candle-context"; import { getLogo } from "../../Utils"; import { SharedListRow } from "../SharedComponents/shared-list-row"; +import { FILTER_CONFIG, SectionItem, updateFilters } from "./models"; export default function GetAssetAccountsScreen() { - const [assetAccounts, setAssetAccounts] = useState<{ - assetAccounts: AssetAccount[]; - linkedAccounts: LinkedAccountStatusRef[]; - }>(); + const menuRef = useRef(null); + const candleClient = useCandleClient(); const navigation = useNavigation(); + + const [filters, setFilters] = useState({}); + + const [assetAccounts, setAssetAccounts] = + useState(); const [isLoading, setIsLoading] = useState(true); - const candleClient = useCandleClient(); - const fetchAssetAccounts = async () => { + const fetchAssetAccounts = async (queryFilters: AssetAccountQuery) => { try { - const accounts = await candleClient.getAssetAccounts(); - setAssetAccounts(accounts); + setIsLoading(true); + const result = await candleClient.getAssetAccounts(queryFilters); + setAssetAccounts(result); } catch (error) { Alert.alert(`Failed to fetch asset accounts: ${error}`); + } finally { + setIsLoading(false); } }; useEffect(() => { - if (assetAccounts) return; - fetchAssetAccounts().finally(() => { - setIsLoading(false); + fetchAssetAccounts(filters); + }, [filters]); + + useEffect(() => { + navigation.setOptions({ + headerTitle: isLoading ? "Loading..." : "Asset Accounts", + headerRight: () => ( + { + const [key, value] = nativeEvent.event.split("|") as [ + "assetKind" | "linkedAccountIDs", + string + ]; + setFilters((prev) => updateFilters(prev, key, value)); + }} + actions={[ + ...FILTER_CONFIG.map((f) => ({ + id: f.key, + title: f.title, + subactions: f.options.map((opt) => ({ + id: `${f.key}|${opt.value}`, + title: opt.label, + state: + filters.assetKind === opt.value + ? ("on" as const) + : ("off" as const), + })), + })), + { + id: "linkedAccountIDs", + title: "Linked Accounts", + subactions: + assetAccounts?.linkedAccounts.map((acc) => ({ + id: `linkedAccountIDs|${acc.linkedAccountID}`, + title: acc.service, + state: filters.linkedAccountIDs + ? filters.linkedAccountIDs + .split(",") + .includes(acc.linkedAccountID) + ? ("on" as const) + : ("off" as const) + : ("off" as const), + })) ?? [], + }, + ]} + shouldOpenOnLongPress={false} + > + + + ), }); - }, []); + }, [filters, assetAccounts, isLoading]); return ( - { - setIsLoading(true); - fetchAssetAccounts().finally(() => { - setIsLoading(false); - }); - }} - /> + + sections={[ + { + title: "Linked Accounts", + data: + assetAccounts?.linkedAccounts.map((a) => ({ + kind: "account", + value: a, + })) ?? [], + }, + { + title: "Asset Accounts", + data: + assetAccounts?.assetAccounts.map((a) => ({ + kind: "assetAccount", + value: a, + })) ?? [], + }, + ]} + keyExtractor={(item) => + item.kind === "account" + ? item.value.linkedAccountID + : item.value.serviceAccountID } - contentInsetAdjustmentBehavior={"always"} - > - - {assetAccounts?.linkedAccounts == undefined ? "" : "Linked Accounts"} - - {assetAccounts?.linkedAccounts.map((account, index) => ( - - ))} - - {assetAccounts?.assetAccounts == undefined ? "" : "Asset Accounts"} - - {assetAccounts?.assetAccounts.map((account) => ( - { - navigation.navigate("Get Asset Accounts Details Screen", { - assetAccount: account, - }); - }} - key={account.serviceAccountID} - /> - ))} - - + renderItem={({ item }) => + item.kind === "account" ? ( + + ) : ( + + navigation.navigate("Get Asset Accounts Details Screen", { + assetAccount: item.value, + }) + } + /> + ) + } + renderSectionHeader={({ section: { title, data } }) => ( + {data.length > 0 ? title : ""} + )} + ListEmptyComponent={ + + No asset accounts found. + + } + refreshing={isLoading} + onRefresh={() => { + fetchAssetAccounts(filters); + }} + contentInsetAdjustmentBehavior="always" + /> ); } @@ -111,4 +165,14 @@ const styles = StyleSheet.create({ container: { flex: 1, }, + headerText: { + fontSize: 20, + fontWeight: "bold", + paddingHorizontal: 20, + marginVertical: 20, + }, + emptyContainer: { + padding: 20, + alignItems: "center", + }, }); diff --git a/example/app/(tabs)/AssetsScreens/models.ts b/example/app/(tabs)/AssetsScreens/models.ts new file mode 100644 index 0000000..404ad4e --- /dev/null +++ b/example/app/(tabs)/AssetsScreens/models.ts @@ -0,0 +1,58 @@ +import { + AssetAccount, + AssetAccountQuery, + LinkedAccountStatusRef, +} from "react-native-candle"; + +export const FILTER_CONFIG = [ + { + key: "assetKind", + title: "Asset Kind", + options: [ + { value: "crypto", label: "Crypto" }, + { value: "stock", label: "Stock" }, + { value: "fiat", label: "Fiat" }, + { value: "transport", label: "Transport" }, + ], + }, +] as const; + +export function toggleLinkedAccountIDs( + current: string | undefined, + id: string +): string | undefined { + const arr = current ? current.split(",") : []; + const index = arr.indexOf(id); + if (index >= 0) { + arr.splice(index, 1); + } else { + arr.push(id); + } + return arr.length ? arr.join(",") : undefined; +} + +export function updateFilters( + prev: AssetAccountQuery, + key: "assetKind" | "linkedAccountIDs", + value: string +): AssetAccountQuery { + switch (key) { + case "assetKind": + return { + ...prev, + assetKind: + prev.assetKind === value + ? undefined + : (value as AssetAccountQuery["assetKind"]), + }; + case "linkedAccountIDs": + return { + ...prev, + linkedAccountIDs: toggleLinkedAccountIDs(prev.linkedAccountIDs, value), + }; + } +} + +export type SectionItem = + | { kind: "account"; value: LinkedAccountStatusRef } + | { kind: "assetAccount"; value: AssetAccount }; diff --git a/example/app/(tabs)/LinkedAccountsScreens/get-linked-accounts.tsx b/example/app/(tabs)/LinkedAccountsScreens/get-linked-accounts.tsx index 4bf4a0b..8315521 100644 --- a/example/app/(tabs)/LinkedAccountsScreens/get-linked-accounts.tsx +++ b/example/app/(tabs)/LinkedAccountsScreens/get-linked-accounts.tsx @@ -28,9 +28,8 @@ type GetLinkedAccountsRouteProp = RouteProp< export default function GetLinkedAccountsScreen() { const route = useRoute(); - const candleClient = useCandleClient(); - const navigation = useNavigation(); + const candleClient = useCandleClient(); const [isLoading, setIsLoading] = useState(false); const [linkedAccounts, setLinkedAccounts] = useState( @@ -39,7 +38,11 @@ export default function GetLinkedAccountsScreen() { useEffect(() => { if (linkedAccounts.length > 0) return; - onRefresh(); + // Adds delay else the loading indicator doesn't show + const timeoutId = setTimeout(() => { + onRefresh(); + }, 300); + return () => clearTimeout(timeoutId); }, []); useEffect(() => { diff --git a/example/app/(tabs)/SharedComponents/detail-scroll-view.tsx b/example/app/(tabs)/SharedComponents/detail-scroll-view.tsx index 5dc7572..3e9021d 100644 --- a/example/app/(tabs)/SharedComponents/detail-scroll-view.tsx +++ b/example/app/(tabs)/SharedComponents/detail-scroll-view.tsx @@ -1,13 +1,25 @@ import { KV } from "@/app/Utils"; import React from "react"; import { StyleSheet, Text, View, ScrollView } from "react-native"; +import * as Clipboard from "expo-clipboard"; +import * as Haptics from "expo-haptics"; export function DetailScrollView({ flattened }: { flattened: KV[] }) { return ( {flattened.map((kv) => ( - + { + Clipboard.setStringAsync(kv.value).then(() => { + Haptics.notificationAsync( + Haptics.NotificationFeedbackType.Success + ); + }); + }} + > {kv.path} {kv.value} diff --git a/example/app/(tabs)/SharedComponents/shared-list-row.tsx b/example/app/(tabs)/SharedComponents/shared-list-row.tsx index c13a34d..238f934 100644 --- a/example/app/(tabs)/SharedComponents/shared-list-row.tsx +++ b/example/app/(tabs)/SharedComponents/shared-list-row.tsx @@ -1,4 +1,4 @@ -import { Image, Text, View } from "react-native"; +import { Image, Pressable, Text, View } from "react-native"; import { Feather } from "@expo/vector-icons"; export function SharedListRow({ @@ -13,7 +13,7 @@ export function SharedListRow({ subtitle: string; }) { return ( - { + onPress={() => { onTouchEnd?.(); }} > @@ -49,6 +49,6 @@ export function SharedListRow({ {onTouchEnd !== undefined ? ( ) : null} - + ); } diff --git a/example/app/(tabs)/TradeQuoteScreens/get-trade-quotes.tsx b/example/app/(tabs)/TradeQuoteScreens/get-trade-quotes.tsx index 09dae07..2ee1598 100644 --- a/example/app/(tabs)/TradeQuoteScreens/get-trade-quotes.tsx +++ b/example/app/(tabs)/TradeQuoteScreens/get-trade-quotes.tsx @@ -11,16 +11,16 @@ import { import { useCandleClient } from "../../Context/candle-context"; import { SafeAreaView } from "react-native-safe-area-context"; import { useState } from "react"; -import { LinkedAccountStatusRef, TradeQuote } from "react-native-candle"; +import { AssetKind, GetTradeQuotesResponse } from "react-native-candle"; import { SharedListRow } from "../SharedComponents/shared-list-row"; import { useNavigation } from "@react-navigation/native"; import { getLogo } from "@/app/Utils"; export default function GetTradeQuotesScreen() { - const [quotes, setQuotes] = useState<{ - tradeQuotes: TradeQuote<"transport", "fiat">[]; - linkedAccounts: LinkedAccountStatusRef[]; - }>(); + const candleClient = useCandleClient(); + const navigation = useNavigation(); + const [quotes, setQuotes] = + useState>(); const [isLoading, setIsLoading] = useState(false); const [origin, setOrigin] = useState<{ latitude: string; longitude: string }>( { @@ -35,8 +35,7 @@ export default function GetTradeQuotesScreen() { latitude: "40.7505", longitude: "-73.9935", }); - const candleClient = useCandleClient(); - const navigation = useNavigation(); + const [serviceAccountID, setServiceAccountID] = useState(""); const fetchTradeQuotes = async () => { try { @@ -45,13 +44,14 @@ export default function GetTradeQuotesScreen() { assetKind: "fiat", }, gained: { + serviceAccountID: serviceAccountID, originCoordinates: { - latitude: parseFloat(origin.latitude) || 0, - longitude: parseFloat(origin.longitude) || 0, + latitude: parseFloat(origin.latitude), + longitude: parseFloat(origin.longitude), }, destinationCoordinates: { - latitude: parseFloat(destination.latitude) || 0, - longitude: parseFloat(destination.longitude) || 0, + latitude: parseFloat(destination.latitude), + longitude: parseFloat(destination.longitude), }, assetKind: "transport", }, @@ -104,6 +104,15 @@ export default function GetTradeQuotesScreen() { } /> + Service Account ID + + setServiceAccountID(text)} + /> + { @@ -121,7 +130,7 @@ export default function GetTradeQuotesScreen() { }} /> - Fetch Quotes + {isLoading ? "Fetching Quotes" : "Fetch Quotes"} @@ -153,22 +162,25 @@ export default function GetTradeQuotesScreen() { > {quotes?.linkedAccounts == undefined ? "" : "Trade Quotes"} - {quotes?.tradeQuotes.map((quote, index) => ( - { - navigation.navigate("Get Trade Quotes Details Screen", { - quote: { - tradeQuotes: quote, - linkedAccounts: quotes.linkedAccounts[index], - }, - }); - }} - key={`quote-${index}`} - /> - ))} + {quotes?.tradeQuotes.map((quote, index) => + quote.lost.assetKind === "fiat" && + quote.gained.assetKind === "transport" ? ( + { + navigation.navigate("Get Trade Quotes Details Screen", { + quote: { + tradeQuotes: quote, + linkedAccounts: quotes.linkedAccounts[index], + }, + }); + }} + key={`quote-${index}`} + /> + ) : null + )} ); diff --git a/example/app/(tabs)/TradeScreen/get-trades.tsx b/example/app/(tabs)/TradeScreen/get-trades.tsx index 24b9fc1..f02bd40 100644 --- a/example/app/(tabs)/TradeScreen/get-trades.tsx +++ b/example/app/(tabs)/TradeScreen/get-trades.tsx @@ -1,117 +1,210 @@ +import { getLogo } from "@/app/Utils"; +import { Ionicons } from "@expo/vector-icons"; +import { MenuComponentRef, MenuView } from "@react-native-menu/menu"; +import { useNavigation } from "@react-navigation/native"; +import { useEffect, useMemo, useRef, useState } from "react"; import { - RefreshControl, - StyleSheet, - ScrollView, Alert, - ActivityIndicator, + NativeSyntheticEvent, + SectionList, + StyleSheet, Text, + TextInputChangeEventData, + View, } from "react-native"; +import { GetTradesResponse, TradeQuery } from "react-native-candle"; import { useCandleClient } from "../../Context/candle-context"; -import { SafeAreaView } from "react-native-safe-area-context"; -import { useEffect, useState } from "react"; -import { LinkedAccountStatusRef, Trade } from "react-native-candle"; import { SharedListRow } from "../SharedComponents/shared-list-row"; -import { useNavigation } from "@react-navigation/native"; -import { getLogo } from "@/app/Utils"; +import { + assetDisplayName, + counterpartyDisplayName, + FILTER_CONFIG, + SectionItem, +} from "./models"; +import { toggleLinkedAccountIDs } from "../AssetsScreens/models"; export default function GetTradesScreen() { - const [trades, setTrades] = useState<{ - trades: Trade[]; - linkedAccounts: LinkedAccountStatusRef[]; - }>(); - const navigation = useNavigation(); - const [isLoading, setIsLoading] = useState(true); + const menuRef = useRef(null); const candleClient = useCandleClient(); + const navigation = useNavigation(); - const fetchTrades = async () => { + const [isLoading, setIsLoading] = useState(false); + const [searchText, setSearchText] = useState(""); + const [filters, setFilters] = useState({}); + const [{ linkedAccounts, trades }, setTrades] = useState({ + trades: [], + linkedAccounts: [], + }); + + const fetchTrades = async (filters: TradeQuery) => { try { - const accounts = await candleClient.getTrades(); - setTrades(accounts); + setIsLoading(true); + const result = await candleClient.getTrades(filters); + setTrades(result); } catch (error) { Alert.alert(`Failed to fetch trades: ${error}`); + } finally { + setIsLoading(false); } }; useEffect(() => { - if (trades) return; - fetchTrades().finally(() => { - setIsLoading(false); + navigation.setOptions({ + headerTitle: isLoading ? "Loading..." : "Trades", + headerSearchBarOptions: { + placeholder: "Search by asset or counterparty", + hideWhenScrolling: false, + onChangeText: (e: NativeSyntheticEvent) => { + setSearchText(e.nativeEvent.text); + }, + }, + headerRight: () => ( + { + const [key, value] = nativeEvent.event.split("|") as [ + keyof TradeQuery, + string + ]; + switch (key) { + case "linkedAccountIDs": + setFilters((prev) => { + return { + ...prev, + [key]: toggleLinkedAccountIDs(prev.linkedAccountIDs, value), + }; + }); + break; + default: + setFilters((prev) => { + return { + ...prev, + [key]: value, + }; + }); + } + }} + actions={FILTER_CONFIG.map((f) => + f.key === "linkedAccountIDs" + ? { + id: f.key, + title: f.title, + subactions: + linkedAccounts.map((acc) => ({ + id: `linkedAccountIDs|${acc.linkedAccountID}`, + title: acc.service, + state: filters.linkedAccountIDs + ? filters.linkedAccountIDs + .split(",") + .includes(acc.linkedAccountID) + ? ("on" as const) + : ("off" as const) + : ("off" as const), + })) ?? [], + } + : { + id: f.key, + title: f.title, + subactions: f.options.map((opt) => ({ + id: `${f.key}|${opt.value}`, + title: opt.label, + state: filters[f.key] === opt.value ? "on" : "off", + })), + } + )} + shouldOpenOnLongPress={false} + > + + + + + ), }); - }, []); + }, [filters, isLoading]); + + useEffect(() => { + fetchTrades(filters); + }, [filters]); + + const filteredTrades = useMemo(() => { + if (searchText.length === 0) return trades; + const q = searchText.toLowerCase(); + return trades.filter((t) => { + const tokens = [ + assetDisplayName(t.lost), + assetDisplayName(t.gained), + counterpartyDisplayName(t.counterparty), + t.state, + ]; + return tokens.some((tok) => tok.toLowerCase().includes(q)); + }); + }, [trades, searchText]); return ( - - { - setIsLoading(true); - fetchTrades().finally(() => { - setIsLoading(false); - }); - }} - /> - } - contentInsetAdjustmentBehavior={"always"} - > - - {trades?.linkedAccounts == undefined ? "" : "Linked Accounts"} - - {trades?.linkedAccounts.map((account, index) => ( + + sections={[ + { + title: "Linked Accounts", + data: linkedAccounts.map((a) => ({ + kind: "account", + value: a, + })), + }, + { + title: "Trades", + data: filteredTrades.map((t) => ({ + kind: "trade", + value: t, + })), + }, + ]} + stickySectionHeadersEnabled + keyExtractor={(item, index) => + item.kind === "account" ? item.value.linkedAccountID : `trade-${index}` + } + renderItem={({ item }) => + item.kind === "account" ? ( - ))} - - {trades?.linkedAccounts == undefined ? "" : "Trades"} - - {trades?.trades.map((trade, index) => ( + ) : ( { + onTouchEnd={() => navigation.navigate("Get Trade Detail Screen", { - trade, - }); - }} - key={`trade-${index}`} + trade: item.value, + }) + } /> - ))} - - - + ) + } + renderSectionHeader={({ section: { title, data } }) => ( + {data.length > 0 ? title : ""} + )} + ListEmptyComponent={ + + No trades found. + + } + refreshing={isLoading} + onRefresh={() => fetchTrades(filters)} + contentInsetAdjustmentBehavior="always" + /> ); } @@ -119,4 +212,10 @@ const styles = StyleSheet.create({ container: { flex: 1, }, + headerText: { + fontSize: 20, + fontWeight: "bold", + paddingHorizontal: 20, + marginVertical: 20, + }, }); diff --git a/example/app/(tabs)/TradeScreen/models.ts b/example/app/(tabs)/TradeScreen/models.ts new file mode 100644 index 0000000..2a20b1f --- /dev/null +++ b/example/app/(tabs)/TradeScreen/models.ts @@ -0,0 +1,101 @@ +import { + Counterparty, + TradeAsset, + LinkedAccountStatusRef, + Trade, +} from "react-native-candle"; + +const SUPPORTED_SPANS = [ + { id: "PT3H", title: "3 Hours" }, + { id: "PT6H", title: "6 Hours" }, + { id: "PT12H", title: "12 Hours" }, + { id: "P1D", title: "1 Day" }, + { id: "P7D", title: "7 Days" }, + { id: "P1M", title: "1 Month" }, + { id: "P6M", title: "6 Months" }, + { id: "P1Y", title: "1 Year" }, +] as const; + +type SectionItem = + | { kind: "account"; value: LinkedAccountStatusRef } + | { kind: "trade"; value: Trade }; + +const FILTER_CONFIG = [ + { + key: "dateTimeSpan", + title: "Date/Time Span", + options: SUPPORTED_SPANS.map((s) => ({ + value: s.id, + label: s.title, + })), + }, + { + key: "lostAssetKind", + title: "Lost Asset Kind", + options: ["cash", "crypto", "stock", "transport", "nothing", "other"].map( + (k) => ({ + value: k, + label: k, + }) + ), + }, + { + key: "gainedAssetKind", + title: "Gained Asset Kind", + options: ["cash", "crypto", "stock", "transport", "nothing", "other"].map( + (k) => ({ + value: k, + label: k, + }) + ), + }, + { + key: "counterpartyKind", + title: "Counterparty Kind", + options: ["merchant", "user", "service"].map((k) => ({ + value: k, + label: k, + })), + }, + { + key: "linkedAccountIDs", + title: "Linked Accounts", + options: [], + }, +] as const; + +const assetDisplayName = (asset: TradeAsset): string => { + switch (asset.assetKind) { + case "transport": + return asset.name; + case "stock": + return asset.name; + case "nothing": + return asset.assetKind; + case "other": + return asset.assetKind; + case "crypto": + return asset.name; + case "fiat": + return asset.currencyCode; + } +}; + +const counterpartyDisplayName = (cp: Counterparty): string => { + switch (cp.kind) { + case "merchant": + return cp.name; + case "user": + return cp.username; + case "service": + return cp.service; + } +}; + +export { + SUPPORTED_SPANS, + FILTER_CONFIG, + assetDisplayName, + counterpartyDisplayName, + SectionItem, +}; diff --git a/example/app/_layout.tsx b/example/app/_layout.tsx index b7dbda5..6a1e443 100644 --- a/example/app/_layout.tsx +++ b/example/app/_layout.tsx @@ -1,4 +1,3 @@ -import { NavigationContainer } from "@react-navigation/native"; import { createNativeStackNavigator } from "@react-navigation/native-stack"; import { useMemo } from "react"; import "react-native-reanimated"; @@ -102,8 +101,9 @@ export default function RootLayout() { } const candleClient = useMemo(() => { - const appKey = process.env.EXPO_PUBLIC_CANDLE_APP_KEY; - const appSecret = process.env.EXPO_PUBLIC_CANDLE_APP_SECRET; + const appKey = "key-ykhw691crgrtw2tx3dwuizey"; + const appSecret = "sec-dSnRm6HsJmt/e/0ZnyqC/AxBayOZAYeWQLHsMZWfYfY="; + console.log("app", appKey, appSecret); if (!appKey || !appSecret) { throw new Error( "EXPO_PUBLIC_CANDLE_APP_KEY and EXPO_PUBLIC_CANDLE_APP_SECRET must be set in .env file" diff --git a/example/package.json b/example/package.json index 20b7832..265ba1b 100644 --- a/example/package.json +++ b/example/package.json @@ -13,12 +13,15 @@ }, "dependencies": { "@expo/vector-icons": "~14.0.4", + "@react-native-menu/menu": "^1.2.3", "@react-navigation/native": "^7.0.14", "expo": "~52.0.46", "expo-build-properties": "~0.13.2", + "expo-clipboard": "~7.0.1", "expo-crypto": "~14.0.2", "expo-dev-client": "~5.0.20", "expo-font": "~13.0.4", + "expo-haptics": "~14.0.1", "expo-linking": "~7.0.5", "expo-router": "~4.0.20", "expo-secure-store": "~14.0.1", diff --git a/package/src/index.ts b/package/src/index.ts index bc76b11..508febe 100644 --- a/package/src/index.ts +++ b/package/src/index.ts @@ -194,10 +194,9 @@ export class CandleClient { return this.candle.executeTool(tool); } - public async getAssetAccounts(query: AssetAccountQuery = {}): Promise<{ - assetAccounts: AssetAccount[]; - linkedAccounts: LinkedAccountStatusRef[]; - }> { + public async getAssetAccounts( + query: AssetAccountQuery = {} + ): Promise { const { assetAccounts, linkedAccounts } = await this.candle.getAssetAccounts(query); return { @@ -213,10 +212,7 @@ export class CandleClient { return this.convertToAssetAccount(account); } - public async getTrades(query: TradeQuery = {}): Promise<{ - trades: Trade[]; - linkedAccounts: LinkedAccountStatusRef[]; - }> { + public async getTrades(query: TradeQuery = {}): Promise { const { trades, linkedAccounts } = await this.candle.getTrades(query); return { trades: trades.map(({ dateTime, counterparty, gained, lost, state }) => ({ @@ -250,14 +246,9 @@ export class CandleClient { public async getTradeQuotes< GainedAssetKind extends AssetKind, LostAssetKind extends AssetKind - >(request: { - linkedAccountIDs?: string; - gained: { assetKind: GainedAssetKind } & AssetQuoteRequest; - lost: { assetKind: LostAssetKind } & AssetQuoteRequest; - }): Promise<{ - tradeQuotes: TradeQuote[]; - linkedAccounts: LinkedAccountStatusRef[]; - }> { + >( + request: TradeQuoteQuery + ): Promise> { let gainedRequest: TradeAssetQuoteRequest; switch (request.gained.assetKind) { @@ -612,7 +603,35 @@ type LinkedAccountDetail = details: { state: "inactive" | "unavailable" }; }); +type TradeQuoteQuery = { + linkedAccountIDs?: string; + gained: { assetKind: GainedAssetKind } & AssetQuoteRequest; + lost: { assetKind: LostAssetKind } & AssetQuoteRequest; +}; + +type GetTradesResponse = { + trades: Trade[]; + linkedAccounts: LinkedAccountStatusRef[]; +}; + +type GetAssetAccountsResponse = { + assetAccounts: AssetAccount[]; + linkedAccounts: LinkedAccountStatusRef[]; +}; + +type GetTradeQuotesResponse< + GainedAssetKind extends AssetKind, + LostAssetKind extends AssetKind +> = { + tradeQuotes: TradeQuote[]; + linkedAccounts: LinkedAccountStatusRef[]; +}; + export type { + GetTradeQuotesResponse, + GetAssetAccountsResponse, + GetTradesResponse, + TradeQuoteQuery, Address, AppUser, AssetAccount, @@ -631,4 +650,5 @@ export type { TradeQuery, TradeQuote, TradeState, + AssetAccountQuery, };