Skip to content

Commit 9732aea

Browse files
committed
Tools: Add Activity Export Feature
1 parent 8a09915 commit 9732aea

File tree

4 files changed

+409
-0
lines changed

4 files changed

+409
-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

+357
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,357 @@
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) => item instanceof Payment
163+
);
164+
break;
165+
166+
case 'transaction':
167+
keysToInclude = [
168+
{ label: 'Transaction Hash', value: 'tx' },
169+
{ label: 'Amount (sat)', value: 'getAmount' },
170+
{ label: 'Total Fees (sat)', value: 'getFee' },
171+
{ label: 'Note', value: 'getNote' },
172+
{ label: 'Timestamp', value: 'getDate' }
173+
];
174+
filteredData = filteredActivity.filter(
175+
(item: any) => item instanceof Transaction
176+
);
177+
break;
178+
179+
default:
180+
keysToInclude = [];
181+
filteredData = [];
182+
break;
183+
}
184+
185+
const csvData = await this.convertActivityToCsv(
186+
filteredData,
187+
keysToInclude
188+
);
189+
190+
if (!csvData) {
191+
this.setState({ isCsvLoading: false });
192+
Alert.alert(
193+
localeString('general.error'),
194+
localeString('views.ActivityToCsv.noData')
195+
);
196+
return;
197+
}
198+
199+
try {
200+
const dateTime = this.getFormattedDateTime();
201+
const baseFileName = `zeus_${dateTime}_${type}.csv`;
202+
const filePath =
203+
Platform.OS === 'android'
204+
? `${RNFS.DownloadDirectoryPath}/${baseFileName}`
205+
: `${RNFS.DocumentDirectoryPath}/${baseFileName}`;
206+
207+
await RNFS.writeFile(filePath, csvData, 'utf8');
208+
209+
this.setState({ isCsvLoading: false });
210+
211+
Alert.alert(
212+
localeString('general.success'),
213+
localeString('views.ActivityToCsv.csvDownloaded')
214+
);
215+
} catch (err) {
216+
console.error('Failed to save CSV file:', err);
217+
Alert.alert(
218+
localeString('general.error'),
219+
localeString('views.ActivityToCsv.csvDownloadFailed')
220+
);
221+
} finally {
222+
this.setState({ isCsvLoading: false });
223+
}
224+
};
225+
226+
render() {
227+
const { isCsvLoading, isActivityFetching } = this.state;
228+
229+
return (
230+
<ScrollView>
231+
<Header
232+
leftComponent="Back"
233+
centerComponent={{
234+
text: 'Activity Export Options',
235+
style: {
236+
color: themeColor('text'),
237+
fontFamily: 'PPNeueMontreal-Book'
238+
}
239+
}}
240+
navigation={this.props.navigation}
241+
/>
242+
{isCsvLoading && (
243+
<ActivityIndicator
244+
size="large"
245+
color={themeColor('text')}
246+
/>
247+
)}
248+
249+
<View
250+
style={{
251+
...styles.container,
252+
backgroundColor: themeColor('background')
253+
}}
254+
>
255+
{isActivityFetching ? (
256+
<LoadingIndicator />
257+
) : (
258+
<>
259+
<TouchableOpacity
260+
style={{
261+
...styles.optionButton,
262+
backgroundColor: themeColor('secondary')
263+
}}
264+
onPress={() => this.downloadCsv('invoice')}
265+
disabled={isCsvLoading}
266+
>
267+
<Icon
268+
name="file-text"
269+
size={24}
270+
color={themeColor('text')}
271+
/>
272+
<Text
273+
style={{
274+
...styles.optionText,
275+
color: themeColor('text')
276+
}}
277+
>
278+
{localeString(
279+
'views.activityExport.exportInvoices'
280+
)}
281+
</Text>
282+
</TouchableOpacity>
283+
284+
<TouchableOpacity
285+
style={{
286+
...styles.optionButton,
287+
backgroundColor: themeColor('secondary')
288+
}}
289+
onPress={() => this.downloadCsv('payment')}
290+
disabled={isCsvLoading}
291+
>
292+
<Icon
293+
name="credit-card"
294+
size={24}
295+
color={themeColor('text')}
296+
/>
297+
<Text
298+
style={{
299+
...styles.optionText,
300+
color: themeColor('text')
301+
}}
302+
>
303+
{localeString(
304+
'views.activityExport.exportPayments'
305+
)}
306+
</Text>
307+
</TouchableOpacity>
308+
309+
<TouchableOpacity
310+
style={{
311+
...styles.optionButton,
312+
backgroundColor: themeColor('secondary')
313+
}}
314+
onPress={() => this.downloadCsv('transaction')}
315+
disabled={isCsvLoading}
316+
>
317+
<Icon
318+
name="dollar-sign"
319+
size={24}
320+
color={themeColor('text')}
321+
/>
322+
<Text
323+
style={{
324+
...styles.optionText,
325+
color: themeColor('text')
326+
}}
327+
>
328+
{localeString(
329+
'views.activityExport.exportTransactions'
330+
)}
331+
</Text>
332+
</TouchableOpacity>
333+
</>
334+
)}
335+
</View>
336+
</ScrollView>
337+
);
338+
}
339+
}
340+
341+
const styles = StyleSheet.create({
342+
container: {
343+
flex: 1,
344+
padding: 20
345+
},
346+
optionButton: {
347+
flexDirection: 'row',
348+
alignItems: 'center',
349+
padding: 15,
350+
marginVertical: 10,
351+
borderRadius: 10
352+
},
353+
optionText: {
354+
marginLeft: 15,
355+
fontSize: 16
356+
}
357+
});

0 commit comments

Comments
 (0)