Skip to content

Commit 69b405f

Browse files
committed
Tools: Add Activity Export Feature
1 parent 8a09915 commit 69b405f

File tree

4 files changed

+410
-0
lines changed

4 files changed

+410
-0
lines changed

App.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ import AddContact from './views/Settings/AddContact';
131131
import ContactDetails from './views/ContactDetails';
132132
import CurrencyConverter from './views/Settings/CurrencyConverter';
133133
import PendingHTLCs from './views/PendingHTLCs';
134+
import ActivityExportOptions from './views/ActivityExportOptions';
134135

135136
// POS
136137
import Order from './views/Order';
@@ -903,6 +904,12 @@ export default class App extends React.PureComponent {
903904
OnChainAddresses
904905
}
905906
/>
907+
<Stack.Screen
908+
name="ActivityExportOptions" // @ts-ignore:next-line
909+
component={
910+
ActivityExportOptions
911+
}
912+
/>
906913
</Stack.Navigator>
907914
</NavigationContainer>
908915
</>

locales/en.json

+6
Original file line numberDiff line numberDiff line change
@@ -996,8 +996,14 @@
996996
"views.ActivityFilter.ampInvoices": "AMP invoices",
997997
"views.ActivityToCsv.title": "Download Activity",
998998
"views.ActivityToCsv.csvDownloaded": "CSV file has been downloaded",
999+
"views.ActivityToCsv.csvDownloadFailed": "Failed to download CSV file",
9991000
"views.ActivityToCsv.textInputPlaceholder": "File name (optional)",
10001001
"views.ActivityToCsv.downloadButton": "Download CSV",
1002+
"views.activityExport.title": "Export CSV Data",
1003+
"views.activityExport.noData": "No data to export",
1004+
"views.activityExport.exportPayments": "Export Payments",
1005+
"views.activityExport.exportInvoices": "Export Invoices",
1006+
"views.activityExport.exportTransactions": "Export Transactions",
10011007
"views.Routing.RoutingEvent.sourceChannel": "Source Channel",
10021008
"views.Routing.RoutingEvent.destinationChannel": "Destination Channel",
10031009
"views.Olympians.title": "Olympians",

views/ActivityExportOptions.tsx

+358
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
import * as React from 'react';
2+
import {
3+
View,
4+
Text,
5+
TouchableOpacity,
6+
StyleSheet,
7+
Alert,
8+
Platform,
9+
ScrollView,
10+
ActivityIndicator
11+
} from 'react-native';
12+
import RNFS from 'react-native-fs';
13+
import Icon from 'react-native-vector-icons/Feather';
14+
import { inject, observer } from 'mobx-react';
15+
16+
import Header from '../components/Header';
17+
import LoadingIndicator from '../components/LoadingIndicator';
18+
19+
import { localeString } from '../utils/LocaleUtils';
20+
import { themeColor } from '../utils/ThemeUtils';
21+
22+
import ActivityStore from '../stores/ActivityStore';
23+
import SettingsStore from '../stores/SettingsStore';
24+
25+
import Invoice from '../models/Invoice';
26+
import Payment from '../models/Payment';
27+
import Transaction from '../models/Transaction';
28+
29+
interface ActivityExportOptionsProps {
30+
navigation: any;
31+
ActivityStore: ActivityStore;
32+
SettingsStore: SettingsStore;
33+
}
34+
35+
interface ActivityExportOptionsState {
36+
isCsvLoading: boolean;
37+
isActivityFetching: boolean;
38+
filteredActivity: any;
39+
}
40+
41+
@inject('ActivityStore', 'SettingsStore')
42+
@observer
43+
export default class ActivityExportOptions extends React.Component<
44+
ActivityExportOptionsProps,
45+
ActivityExportOptionsState
46+
> {
47+
constructor(props: ActivityExportOptionsProps) {
48+
super(props);
49+
this.state = {
50+
isCsvLoading: false,
51+
isActivityFetching: true,
52+
filteredActivity: []
53+
};
54+
}
55+
56+
componentDidMount() {
57+
this.fetchAndFilterActivity();
58+
}
59+
60+
fetchAndFilterActivity = async () => {
61+
const { SettingsStore, ActivityStore } = this.props;
62+
const { locale } = SettingsStore.settings;
63+
64+
try {
65+
// Call getActivityAndFilter to fetch and filter activity data
66+
await ActivityStore.getActivityAndFilter(locale);
67+
68+
// Update filteredActivity in state
69+
this.setState({
70+
filteredActivity: ActivityStore.filteredActivity,
71+
isActivityFetching: false
72+
});
73+
} catch (err) {
74+
console.error('Failed to fetch activity data:', err);
75+
this.setState({ isActivityFetching: false });
76+
Alert.alert(localeString('general.error'));
77+
}
78+
};
79+
80+
getFormattedDateTime = () => {
81+
const now = new Date();
82+
const year = now.getFullYear();
83+
const month = (now.getMonth() + 1).toString().padStart(2, '0');
84+
const day = now.getDate().toString().padStart(2, '0');
85+
const hours = now.getHours().toString().padStart(2, '0');
86+
const minutes = now.getMinutes().toString().padStart(2, '0');
87+
const seconds = now.getSeconds().toString().padStart(2, '0');
88+
89+
return `${year}${month}${day}_${hours}${minutes}${seconds}`;
90+
};
91+
92+
convertActivityToCsv = async (
93+
data: Array<Invoice | Payment | Transaction>,
94+
keysToInclude: Array<any>
95+
) => {
96+
if (!data || data.length === 0) {
97+
return '';
98+
}
99+
100+
try {
101+
const header = keysToInclude.map((field) => field.label).join(',');
102+
const rows = data
103+
?.map((item: any) =>
104+
keysToInclude
105+
.map((field) => `"${item[field.value]}"` || '')
106+
.join(',')
107+
)
108+
.join('\n');
109+
110+
return `${header}\n${rows}`;
111+
} catch (err) {
112+
console.error(err);
113+
return '';
114+
}
115+
};
116+
117+
downloadCsv = async (type: 'invoice' | 'payment' | 'transaction') => {
118+
const { filteredActivity } = this.state;
119+
120+
// If filteredActivity is empty, try fetching it again
121+
if (!filteredActivity || filteredActivity.length === 0) {
122+
Alert.alert(
123+
localeString('general.warning'),
124+
localeString('views.ActivityToCsv.noData')
125+
);
126+
await this.fetchAndFilterActivity();
127+
return;
128+
}
129+
130+
this.setState({ isCsvLoading: true });
131+
132+
let keysToInclude: any;
133+
let filteredData: any;
134+
135+
switch (type) {
136+
case 'invoice':
137+
keysToInclude = [
138+
{ label: 'Amount Paid (sat)', value: 'getAmount' },
139+
{ label: 'Payment Request', value: 'getPaymentRequest' },
140+
{ label: 'Payment Hash', value: 'getRHash' },
141+
{ label: 'Memo', value: 'getMemo' },
142+
{ label: 'Note', value: 'getNote' },
143+
{ label: 'Creation Date', value: 'getCreationDate' },
144+
{ label: 'Expiry', value: 'formattedTimeUntilExpiry' }
145+
];
146+
filteredData = filteredActivity.filter(
147+
(item: any) => item instanceof Invoice
148+
);
149+
break;
150+
151+
case 'payment':
152+
keysToInclude = [
153+
{ label: 'Destination', value: 'getDestination' },
154+
{ label: 'Payment Request', value: 'getPaymentRequest' },
155+
{ label: 'Payment Hash', value: 'paymentHash' },
156+
{ label: 'Amount Paid (sat)', value: 'getAmount' },
157+
{ label: 'Memo', value: 'getMemo' },
158+
{ label: 'Note', value: 'getNote' },
159+
{ label: 'Creation Date', value: 'getDate' }
160+
];
161+
filteredData = filteredActivity.filter(
162+
(item: any) =>
163+
item instanceof Payment && item?.getDestination
164+
);
165+
break;
166+
167+
case 'transaction':
168+
keysToInclude = [
169+
{ label: 'Transaction Hash', value: 'tx' },
170+
{ label: 'Amount (sat)', value: 'getAmount' },
171+
{ label: 'Total Fees (sat)', value: 'getFee' },
172+
{ label: 'Note', value: 'getNote' },
173+
{ label: 'Timestamp', value: 'getDate' }
174+
];
175+
filteredData = filteredActivity.filter(
176+
(item: any) => item instanceof Transaction
177+
);
178+
break;
179+
180+
default:
181+
keysToInclude = [];
182+
filteredData = [];
183+
break;
184+
}
185+
186+
const csvData = await this.convertActivityToCsv(
187+
filteredData,
188+
keysToInclude
189+
);
190+
191+
if (!csvData) {
192+
this.setState({ isCsvLoading: false });
193+
Alert.alert(
194+
localeString('general.error'),
195+
localeString('views.ActivityToCsv.noData')
196+
);
197+
return;
198+
}
199+
200+
try {
201+
const dateTime = this.getFormattedDateTime();
202+
const baseFileName = `zeus_${dateTime}_${type}.csv`;
203+
const filePath =
204+
Platform.OS === 'android'
205+
? `${RNFS.DownloadDirectoryPath}/${baseFileName}`
206+
: `${RNFS.DocumentDirectoryPath}/${baseFileName}`;
207+
208+
await RNFS.writeFile(filePath, csvData, 'utf8');
209+
210+
this.setState({ isCsvLoading: false });
211+
212+
Alert.alert(
213+
localeString('general.success'),
214+
localeString('views.ActivityToCsv.csvDownloaded')
215+
);
216+
} catch (err) {
217+
console.error('Failed to save CSV file:', err);
218+
Alert.alert(
219+
localeString('general.error'),
220+
localeString('views.ActivityToCsv.csvDownloadFailed')
221+
);
222+
} finally {
223+
this.setState({ isCsvLoading: false });
224+
}
225+
};
226+
227+
render() {
228+
const { isCsvLoading, isActivityFetching } = this.state;
229+
230+
return (
231+
<ScrollView>
232+
<Header
233+
leftComponent="Back"
234+
centerComponent={{
235+
text: 'Activity Export Options',
236+
style: {
237+
color: themeColor('text'),
238+
fontFamily: 'PPNeueMontreal-Book'
239+
}
240+
}}
241+
navigation={this.props.navigation}
242+
/>
243+
{isCsvLoading && (
244+
<ActivityIndicator
245+
size="large"
246+
color={themeColor('text')}
247+
/>
248+
)}
249+
250+
<View
251+
style={{
252+
...styles.container,
253+
backgroundColor: themeColor('background')
254+
}}
255+
>
256+
{isActivityFetching ? (
257+
<LoadingIndicator />
258+
) : (
259+
<>
260+
<TouchableOpacity
261+
style={{
262+
...styles.optionButton,
263+
backgroundColor: themeColor('secondary')
264+
}}
265+
onPress={() => this.downloadCsv('invoice')}
266+
disabled={isCsvLoading}
267+
>
268+
<Icon
269+
name="file-text"
270+
size={24}
271+
color={themeColor('text')}
272+
/>
273+
<Text
274+
style={{
275+
...styles.optionText,
276+
color: themeColor('text')
277+
}}
278+
>
279+
{localeString(
280+
'views.activityExport.exportInvoices'
281+
)}
282+
</Text>
283+
</TouchableOpacity>
284+
285+
<TouchableOpacity
286+
style={{
287+
...styles.optionButton,
288+
backgroundColor: themeColor('secondary')
289+
}}
290+
onPress={() => this.downloadCsv('payment')}
291+
disabled={isCsvLoading}
292+
>
293+
<Icon
294+
name="credit-card"
295+
size={24}
296+
color={themeColor('text')}
297+
/>
298+
<Text
299+
style={{
300+
...styles.optionText,
301+
color: themeColor('text')
302+
}}
303+
>
304+
{localeString(
305+
'views.activityExport.exportPayments'
306+
)}
307+
</Text>
308+
</TouchableOpacity>
309+
310+
<TouchableOpacity
311+
style={{
312+
...styles.optionButton,
313+
backgroundColor: themeColor('secondary')
314+
}}
315+
onPress={() => this.downloadCsv('transaction')}
316+
disabled={isCsvLoading}
317+
>
318+
<Icon
319+
name="dollar-sign"
320+
size={24}
321+
color={themeColor('text')}
322+
/>
323+
<Text
324+
style={{
325+
...styles.optionText,
326+
color: themeColor('text')
327+
}}
328+
>
329+
{localeString(
330+
'views.activityExport.exportTransactions'
331+
)}
332+
</Text>
333+
</TouchableOpacity>
334+
</>
335+
)}
336+
</View>
337+
</ScrollView>
338+
);
339+
}
340+
}
341+
342+
const styles = StyleSheet.create({
343+
container: {
344+
flex: 1,
345+
padding: 20
346+
},
347+
optionButton: {
348+
flexDirection: 'row',
349+
alignItems: 'center',
350+
padding: 15,
351+
marginVertical: 10,
352+
borderRadius: 10
353+
},
354+
optionText: {
355+
marginLeft: 15,
356+
fontSize: 16
357+
}
358+
});

0 commit comments

Comments
 (0)