Skip to content

Commit 8da6ee7

Browse files
authored
Merge pull request #2785 from shubhamkmr04/shubham/export-csv-in-tools
Tools: Add Activity Export Feature
2 parents 8503c50 + 8df1106 commit 8da6ee7

File tree

7 files changed

+991
-110
lines changed

7 files changed

+991
-110
lines changed

App.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ import AddContact from './views/Settings/AddContact';
135135
import ContactDetails from './views/ContactDetails';
136136

137137
import PendingHTLCs from './views/PendingHTLCs';
138+
import ActivityExportOptions from './views/ActivityExportOptions';
138139

139140
// POS
140141
import Order from './views/Order';
@@ -938,6 +939,12 @@ export default class App extends React.PureComponent {
938939
Sweepremoteclosed
939940
}
940941
/>
942+
<Stack.Screen
943+
name="ActivityExportOptions" // @ts-ignore:next-line
944+
component={
945+
ActivityExportOptions
946+
}
947+
/>
941948
</Stack.Navigator>
942949
</NavigationContainer>
943950
</>

locales/en.json

+14
Original file line numberDiff line numberDiff line change
@@ -1018,8 +1018,22 @@
10181018
"views.ActivityFilter.ampInvoices": "AMP invoices",
10191019
"views.ActivityToCsv.title": "Download Activity",
10201020
"views.ActivityToCsv.csvDownloaded": "CSV file has been downloaded",
1021+
"views.ActivityToCsv.csvDownloadFailed": "Failed to download CSV file",
10211022
"views.ActivityToCsv.textInputPlaceholder": "File name (optional)",
10221023
"views.ActivityToCsv.downloadButton": "Download CSV",
1024+
"views.ActivityExport.title": "Export CSV Data",
1025+
"views.ActivityExport.exportPayments": "Export Payments",
1026+
"views.ActivityExport.exportInvoices": "Export Invoices",
1027+
"views.ActivityExport.exportTransactions": "Export Transactions",
1028+
"views.ActivityExport.fromDate": "From Date (Start Date)",
1029+
"views.ActivityExport.toDate": "To Date (End Date)",
1030+
"views.ActivityExport.dateRange": "Select Date Range:",
1031+
"views.ActivityExport.downloadCompleteData": "Download Complete Data",
1032+
"views.ActivityExport.explainerAndroid": "Downloaded CSV files can be found in Files > Downloads.",
1033+
"views.ActivityExport.explaineriOS": "Downloaded CSV files can be found in the Files app under the ZEUS folder.",
1034+
"views.ActivityExport.noDataAvailableForSelection": "No activity data is available for the selected type.",
1035+
"views.ActivityExport.noDataForSelectedDates": "No records found in the selected date range. Try a different date.",
1036+
"views.ActivityExport.noValidDataForDownload": "There is no valid data available for download.",
10231037
"views.Routing.RoutingEvent.sourceChannel": "Source Channel",
10241038
"views.Routing.RoutingEvent.destinationChannel": "Destination Channel",
10251039
"views.Olympians.title": "Olympians",

utils/ActivityCsvUtils.test.ts

+200
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import {
2+
getFormattedDateTime,
3+
convertActivityToCsv,
4+
saveCsvFile,
5+
CSV_KEYS
6+
} from '.././utils/ActivityCsvUtils';
7+
import RNFS from 'react-native-fs';
8+
import { Platform } from 'react-native';
9+
10+
jest.mock('react-native-fs', () => ({
11+
DownloadDirectoryPath: '/mock/download/path',
12+
DocumentDirectoryPath: '/mock/document/path',
13+
writeFile: jest.fn()
14+
}));
15+
16+
jest.mock('react-native', () => ({
17+
Platform: { OS: 'android' }
18+
}));
19+
20+
describe('activityCsvUtils', () => {
21+
describe('getFormattedDateTime', () => {
22+
it('returns a properly formatted timestamp', () => {
23+
const result = getFormattedDateTime();
24+
expect(result).toMatch(/^\d{8}_\d{6}$/); // Example: 20250212_140719
25+
});
26+
});
27+
28+
describe('convertActivityToCsv', () => {
29+
it('correctly formats Invoice CSV data', async () => {
30+
const mockInvoices = [
31+
{
32+
getAmount: 1500,
33+
getPaymentRequest: 'inv_req123',
34+
getRHash: 'hash_inv1',
35+
getMemo: 'Test Memo',
36+
getNote: 'Test Note',
37+
getCreationDate: '2024-02-10',
38+
formattedTimeUntilExpiry: '30 min'
39+
},
40+
{
41+
getAmount: 3000,
42+
getPaymentRequest: 'inv_req456',
43+
getRHash: 'hash_inv2',
44+
getMemo: '',
45+
getNote: '',
46+
getCreationDate: '2024-02-11',
47+
formattedTimeUntilExpiry: '1 hour'
48+
}
49+
];
50+
51+
const result = await convertActivityToCsv(
52+
mockInvoices,
53+
CSV_KEYS.invoice
54+
);
55+
expect(result).toContain(
56+
'"1500","inv_req123","hash_inv1","Test Memo","Test Note","2024-02-10","30 min"'
57+
);
58+
expect(result).toContain(
59+
'"3000","inv_req456","hash_inv2","","","2024-02-11","1 hour"'
60+
);
61+
});
62+
63+
it('correctly formats Payment CSV data', async () => {
64+
const mockPayments = [
65+
{
66+
getDestination: 'dest123',
67+
getPaymentRequest: 'pay_req123',
68+
paymentHash: 'hash_pay1',
69+
getAmount: 800,
70+
getMemo: 'Payment Memo',
71+
getNote: 'Payment Note',
72+
getDate: '2024-02-09'
73+
},
74+
{
75+
getDestination: 'dest456',
76+
getPaymentRequest: 'pay_req456',
77+
paymentHash: 'hash_pay2',
78+
getAmount: 1600,
79+
getMemo: '',
80+
getNote: '',
81+
getDate: '2024-02-08'
82+
}
83+
];
84+
85+
const result = await convertActivityToCsv(
86+
mockPayments,
87+
CSV_KEYS.payment
88+
);
89+
expect(result).toContain(
90+
'"dest123","pay_req123","hash_pay1","800","Payment Memo","Payment Note","2024-02-09"'
91+
);
92+
expect(result).toContain(
93+
'"dest456","pay_req456","hash_pay2","1600","","","2024-02-08"'
94+
);
95+
});
96+
97+
it('correctly formats Transaction CSV data', async () => {
98+
const mockTransactions = [
99+
{
100+
tx: 'txhash1',
101+
getAmount: 2000,
102+
getFee: 50,
103+
getNote: 'Tx Note1',
104+
getDate: '2024-02-07'
105+
},
106+
{
107+
tx: 'txhash2',
108+
getAmount: 5000,
109+
getFee: 100,
110+
getNote: '',
111+
getDate: '2024-02-06'
112+
}
113+
];
114+
115+
const result = await convertActivityToCsv(
116+
mockTransactions,
117+
CSV_KEYS.transaction
118+
);
119+
expect(result).toContain(
120+
'"txhash1","2000","50","Tx Note1","2024-02-07"'
121+
);
122+
expect(result).toContain('"txhash2","5000","100","","2024-02-06"');
123+
});
124+
125+
it('handles missing fields for Invoice CSV', async () => {
126+
const mockInvoices = [{ getAmount: 1500 }];
127+
const result = await convertActivityToCsv(
128+
mockInvoices,
129+
CSV_KEYS.invoice
130+
);
131+
expect(result).toContain('"1500","","","","","",""');
132+
});
133+
134+
it('handles missing fields for Payment CSV', async () => {
135+
const mockPayments = [{ getDestination: 'dest123' }];
136+
const result = await convertActivityToCsv(
137+
mockPayments,
138+
CSV_KEYS.payment
139+
);
140+
expect(result).toContain('"dest123","","","","","",""');
141+
});
142+
143+
it('handles missing fields for Transaction CSV', async () => {
144+
const mockTransactions = [{ tx: 'txhash1', getAmount: 2000 }];
145+
const result = await convertActivityToCsv(
146+
mockTransactions,
147+
CSV_KEYS.transaction
148+
);
149+
expect(result).toContain('"txhash1","2000","","",""');
150+
});
151+
});
152+
153+
describe('saveCsvFile', () => {
154+
beforeEach(() => {
155+
jest.clearAllMocks();
156+
});
157+
158+
it('writes the CSV file to the correct path on Android', async () => {
159+
(Platform.OS as any) = 'android';
160+
(RNFS.writeFile as jest.Mock).mockResolvedValue(undefined);
161+
162+
await saveCsvFile('test.csv', 'mock,csv,data');
163+
164+
expect(RNFS.writeFile).toHaveBeenCalledWith(
165+
'/mock/download/path/test.csv',
166+
'mock,csv,data',
167+
'utf8'
168+
);
169+
});
170+
171+
it('writes the CSV file to the correct path on iOS', async () => {
172+
(Platform.OS as any) = 'ios';
173+
(RNFS.writeFile as jest.Mock).mockResolvedValue(undefined);
174+
175+
await saveCsvFile('test.csv', 'mock,csv,data');
176+
177+
expect(RNFS.writeFile).toHaveBeenCalledWith(
178+
'/mock/document/path/test.csv',
179+
'mock,csv,data',
180+
'utf8'
181+
);
182+
});
183+
184+
it('throws an error when file writing fails (but suppresses console error)', async () => {
185+
const consoleErrorSpy = jest
186+
.spyOn(console, 'error')
187+
.mockImplementation(() => {});
188+
189+
(RNFS.writeFile as jest.Mock).mockRejectedValue(
190+
new Error('File write failed')
191+
);
192+
193+
await expect(
194+
saveCsvFile('test.csv', 'mock,csv,data')
195+
).rejects.toThrow('File write failed');
196+
197+
consoleErrorSpy.mockRestore();
198+
});
199+
});
200+
});

utils/ActivityCsvUtils.ts

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import RNFS from 'react-native-fs';
2+
import { Platform } from 'react-native';
3+
4+
// Keys for CSV export.
5+
export const CSV_KEYS = {
6+
invoice: [
7+
{ label: 'Amount Paid (sat)', value: 'getAmount' },
8+
{ label: 'Payment Request', value: 'getPaymentRequest' },
9+
{ label: 'Payment Hash', value: 'getRHash' },
10+
{ label: 'Memo', value: 'getMemo' },
11+
{ label: 'Note', value: 'getNote' },
12+
{ label: 'Creation Date', value: 'getCreationDate' },
13+
{ label: 'Expiry', value: 'formattedTimeUntilExpiry' }
14+
],
15+
payment: [
16+
{ label: 'Destination', value: 'getDestination' },
17+
{ label: 'Payment Request', value: 'getPaymentRequest' },
18+
{ label: 'Payment Hash', value: 'paymentHash' },
19+
{ label: 'Amount Paid (sat)', value: 'getAmount' },
20+
{ label: 'Memo', value: 'getMemo' },
21+
{ label: 'Note', value: 'getNote' },
22+
{ label: 'Creation Date', value: 'getDate' }
23+
],
24+
transaction: [
25+
{ label: 'Transaction Hash', value: 'tx' },
26+
{ label: 'Amount (sat)', value: 'getAmount' },
27+
{ label: 'Total Fees (sat)', value: 'getFee' },
28+
{ label: 'Note', value: 'getNote' },
29+
{ label: 'Timestamp', value: 'getDate' }
30+
]
31+
};
32+
33+
// Generates a formatted timestamp string for file naming.
34+
export const getFormattedDateTime = (): string => {
35+
const now = new Date();
36+
const year = now.getFullYear();
37+
const month = (now.getMonth() + 1).toString().padStart(2, '0');
38+
const day = now.getDate().toString().padStart(2, '0');
39+
const hours = now.getHours().toString().padStart(2, '0');
40+
const minutes = now.getMinutes().toString().padStart(2, '0');
41+
const seconds = now.getSeconds().toString().padStart(2, '0');
42+
return `${year}${month}${day}_${hours}${minutes}${seconds}`;
43+
};
44+
45+
// Converts activity data into a CSV string.
46+
export const convertActivityToCsv = async (
47+
data: Array<any>,
48+
keysToInclude: Array<{ label: string; value: string }>
49+
): Promise<string> => {
50+
if (!data || data.length === 0) return '';
51+
52+
try {
53+
const header = keysToInclude.map((field) => field.label).join(',');
54+
const rows = data
55+
.map((item) =>
56+
keysToInclude
57+
.map((field) => `"${item[field.value] || ''}"`)
58+
.join(',')
59+
)
60+
.join('\n');
61+
62+
return `${header}\n${rows}`;
63+
} catch (err) {
64+
console.error(err);
65+
return '';
66+
}
67+
};
68+
69+
//Saves CSV file to the device.
70+
export const saveCsvFile = async (fileName: string, csvData: string) => {
71+
try {
72+
const filePath =
73+
Platform.OS === 'android'
74+
? `${RNFS.DownloadDirectoryPath}/${fileName}`
75+
: `${RNFS.DocumentDirectoryPath}/${fileName}`;
76+
77+
console.log(`Saving file to: ${filePath}`);
78+
await RNFS.writeFile(filePath, csvData, 'utf8');
79+
} catch (err) {
80+
console.error('Failed to save CSV file:', err);
81+
throw err;
82+
}
83+
};

0 commit comments

Comments
 (0)