Skip to content
Open
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
13 changes: 11 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

228 changes: 146 additions & 82 deletions example/app/(tabs)/AssetsScreens/get-asset-accounts.tsx
Original file line number Diff line number Diff line change
@@ -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<MenuComponentRef>(null);
const candleClient = useCandleClient();
const navigation = useNavigation<any>();

const [filters, setFilters] = useState<AssetAccountQuery>({});

const [assetAccounts, setAssetAccounts] =
useState<GetAssetAccountsResponse>();
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: () => (
<MenuView
ref={menuRef}
title="Select Filter"
onPressAction={({ nativeEvent }: NativeActionEvent) => {
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}
>
<Ionicons
name="ellipsis-horizontal-circle-outline"
size={28}
color="black"
/>
</MenuView>
),
});
}, []);
}, [filters, assetAccounts, isLoading]);

return (
<SafeAreaView style={[styles.container]}>
<ScrollView
refreshControl={
<RefreshControl
refreshing={isLoading}
onRefresh={() => {
setIsLoading(true);
fetchAssetAccounts().finally(() => {
setIsLoading(false);
});
}}
/>
<SectionList<SectionItem>
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"}
>
<Text
style={{
fontSize: 20,
fontWeight: "bold",
paddingHorizontal: 20,
marginVertical: 20,
}}
>
{assetAccounts?.linkedAccounts == undefined ? "" : "Linked Accounts"}
</Text>
{assetAccounts?.linkedAccounts.map((account, index) => (
<SharedListRow
key={account.linkedAccountID}
title={account.service}
subtitle={account.state}
uri={getLogo(account.service)}
/>
))}
<Text
style={{
fontSize: 20,
fontWeight: "bold",
paddingHorizontal: 20,
marginVertical: 20,
}}
>
{assetAccounts?.assetAccounts == undefined ? "" : "Asset Accounts"}
</Text>
{assetAccounts?.assetAccounts.map((account) => (
<SharedListRow
title={account.nickname}
subtitle={account.accountKind}
uri={getLogo(account.service)}
onTouchEnd={() => {
navigation.navigate("Get Asset Accounts Details Screen", {
assetAccount: account,
});
}}
key={account.serviceAccountID}
/>
))}
<ActivityIndicator
animating={isLoading}
size="large"
color="black"
style={{ marginTop: 20 }}
/>
</ScrollView>
renderItem={({ item }) =>
item.kind === "account" ? (
<SharedListRow
title={item.value.service}
subtitle={item.value.state}
uri={getLogo(item.value.service)}
/>
) : (
<SharedListRow
title={item.value.nickname}
subtitle={item.value.accountKind}
uri={getLogo(item.value.service)}
onTouchEnd={() =>
navigation.navigate("Get Asset Accounts Details Screen", {
assetAccount: item.value,
})
}
/>
)
}
renderSectionHeader={({ section: { title, data } }) => (
<Text style={styles.headerText}>{data.length > 0 ? title : ""}</Text>
)}
ListEmptyComponent={
<View style={styles.emptyContainer}>
<Text>No asset accounts found.</Text>
</View>
}
refreshing={isLoading}
onRefresh={() => {
fetchAssetAccounts(filters);
}}
contentInsetAdjustmentBehavior="always"
/>
</SafeAreaView>
);
}
Expand All @@ -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",
},
});
58 changes: 58 additions & 0 deletions example/app/(tabs)/AssetsScreens/models.ts
Original file line number Diff line number Diff line change
@@ -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 };
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,8 @@ type GetLinkedAccountsRouteProp = RouteProp<

export default function GetLinkedAccountsScreen() {
const route = useRoute<GetLinkedAccountsRouteProp>();
const candleClient = useCandleClient();

const navigation = useNavigation<any>();
const candleClient = useCandleClient();

const [isLoading, setIsLoading] = useState(false);
const [linkedAccounts, setLinkedAccounts] = useState<LinkedAccountDetail[]>(
Expand All @@ -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(() => {
Expand Down
Loading
Loading