-
-
Notifications
You must be signed in to change notification settings - Fork 157
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Tools: Add Activity Export Feature #2785
Merged
kaloudis
merged 11 commits into
ZeusLN:master
from
shubhamkmr04:shubham/export-csv-in-tools
Feb 15, 2025
Merged
Changes from 8 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
311b5cf
Tools: Add Activity Export Feature
shubhamkmr04 3603ff7
Fix download lag of payments
shubhamkmr04 1532d8f
Added date filter modal and the option to download complete data
shubhamkmr04 fde74a2
Refactored CSV export to reuse utils functions across ActivityExportO…
shubhamkmr04 d0eaf5a
Show Activity export option only a node is added
shubhamkmr04 f242956
Add test file for ActivityCsvUtils
shubhamkmr04 8e73643
Add info of downloaded CSV location
shubhamkmr04 acbb68b
Fix unwanted white border in android
shubhamkmr04 5dea3e3
Remove svg icon and use text character instead
shubhamkmr04 cc4858a
set default download timeframe to one month
shubhamkmr04 8df1106
Improved error handling
shubhamkmr04 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1017,8 +1017,20 @@ | |
"views.ActivityFilter.ampInvoices": "AMP invoices", | ||
"views.ActivityToCsv.title": "Download Activity", | ||
"views.ActivityToCsv.csvDownloaded": "CSV file has been downloaded", | ||
"views.ActivityToCsv.csvDownloadFailed": "Failed to download CSV file", | ||
"views.ActivityToCsv.textInputPlaceholder": "File name (optional)", | ||
"views.ActivityToCsv.downloadButton": "Download CSV", | ||
"views.activityExport.title": "Export CSV Data", | ||
"views.activityExport.noData": "No data to export", | ||
"views.activityExport.exportPayments": "Export Payments", | ||
"views.activityExport.exportInvoices": "Export Invoices", | ||
"views.activityExport.exportTransactions": "Export Transactions", | ||
"views.activityExport.fromDate": "From Date (Start Date)", | ||
"views.activityExport.toDate": "To Date (End Date)", | ||
"views.activityExport.dateRange": "Select Date Range:", | ||
"views.activityExport.downloadCompleteData": "Download Complete Data", | ||
"views.activityExport.explainerAndroid": "Downloaded CSV files can be found in Files > Downloads.", | ||
"views.activityExport.explaineriOS": "Downloaded CSV files can be found in the Files app under the ZEUS folder.", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good to see the Android and iOS breakdown here |
||
"views.Routing.RoutingEvent.sourceChannel": "Source Channel", | ||
"views.Routing.RoutingEvent.destinationChannel": "Destination Channel", | ||
"views.Olympians.title": "Olympians", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
import { | ||
getFormattedDateTime, | ||
convertActivityToCsv, | ||
saveCsvFile, | ||
CSV_KEYS | ||
} from '.././utils/ActivityCsvUtils'; | ||
import RNFS from 'react-native-fs'; | ||
import { Platform } from 'react-native'; | ||
|
||
jest.mock('react-native-fs', () => ({ | ||
DownloadDirectoryPath: '/mock/download/path', | ||
DocumentDirectoryPath: '/mock/document/path', | ||
writeFile: jest.fn() | ||
})); | ||
|
||
jest.mock('react-native', () => ({ | ||
Platform: { OS: 'android' } | ||
})); | ||
|
||
describe('activityCsvUtils', () => { | ||
describe('getFormattedDateTime', () => { | ||
it('returns a properly formatted timestamp', () => { | ||
const result = getFormattedDateTime(); | ||
expect(result).toMatch(/^\d{8}_\d{6}$/); // Example: 20250212_140719 | ||
}); | ||
}); | ||
|
||
describe('convertActivityToCsv', () => { | ||
it('correctly formats Invoice CSV data', async () => { | ||
const mockInvoices = [ | ||
{ | ||
getAmount: 1500, | ||
getPaymentRequest: 'inv_req123', | ||
getRHash: 'hash_inv1', | ||
getMemo: 'Test Memo', | ||
getNote: 'Test Note', | ||
getCreationDate: '2024-02-10', | ||
formattedTimeUntilExpiry: '30 min' | ||
}, | ||
{ | ||
getAmount: 3000, | ||
getPaymentRequest: 'inv_req456', | ||
getRHash: 'hash_inv2', | ||
getMemo: '', | ||
getNote: '', | ||
getCreationDate: '2024-02-11', | ||
formattedTimeUntilExpiry: '1 hour' | ||
} | ||
]; | ||
|
||
const result = await convertActivityToCsv( | ||
mockInvoices, | ||
CSV_KEYS.invoice | ||
); | ||
expect(result).toContain( | ||
'"1500","inv_req123","hash_inv1","Test Memo","Test Note","2024-02-10","30 min"' | ||
); | ||
expect(result).toContain( | ||
'"3000","inv_req456","hash_inv2","","","2024-02-11","1 hour"' | ||
); | ||
}); | ||
|
||
it('correctly formats Payment CSV data', async () => { | ||
const mockPayments = [ | ||
{ | ||
getDestination: 'dest123', | ||
getPaymentRequest: 'pay_req123', | ||
paymentHash: 'hash_pay1', | ||
getAmount: 800, | ||
getMemo: 'Payment Memo', | ||
getNote: 'Payment Note', | ||
getDate: '2024-02-09' | ||
}, | ||
{ | ||
getDestination: 'dest456', | ||
getPaymentRequest: 'pay_req456', | ||
paymentHash: 'hash_pay2', | ||
getAmount: 1600, | ||
getMemo: '', | ||
getNote: '', | ||
getDate: '2024-02-08' | ||
} | ||
]; | ||
|
||
const result = await convertActivityToCsv( | ||
mockPayments, | ||
CSV_KEYS.payment | ||
); | ||
expect(result).toContain( | ||
'"dest123","pay_req123","hash_pay1","800","Payment Memo","Payment Note","2024-02-09"' | ||
); | ||
expect(result).toContain( | ||
'"dest456","pay_req456","hash_pay2","1600","","","2024-02-08"' | ||
); | ||
}); | ||
|
||
it('correctly formats Transaction CSV data', async () => { | ||
const mockTransactions = [ | ||
{ | ||
tx: 'txhash1', | ||
getAmount: 2000, | ||
getFee: 50, | ||
getNote: 'Tx Note1', | ||
getDate: '2024-02-07' | ||
}, | ||
{ | ||
tx: 'txhash2', | ||
getAmount: 5000, | ||
getFee: 100, | ||
getNote: '', | ||
getDate: '2024-02-06' | ||
} | ||
]; | ||
|
||
const result = await convertActivityToCsv( | ||
mockTransactions, | ||
CSV_KEYS.transaction | ||
); | ||
expect(result).toContain( | ||
'"txhash1","2000","50","Tx Note1","2024-02-07"' | ||
); | ||
expect(result).toContain('"txhash2","5000","100","","2024-02-06"'); | ||
}); | ||
|
||
it('handles missing fields for Invoice CSV', async () => { | ||
const mockInvoices = [{ getAmount: 1500 }]; | ||
const result = await convertActivityToCsv( | ||
mockInvoices, | ||
CSV_KEYS.invoice | ||
); | ||
expect(result).toContain('"1500","","","","","",""'); | ||
}); | ||
|
||
it('handles missing fields for Payment CSV', async () => { | ||
const mockPayments = [{ getDestination: 'dest123' }]; | ||
const result = await convertActivityToCsv( | ||
mockPayments, | ||
CSV_KEYS.payment | ||
); | ||
expect(result).toContain('"dest123","","","","","",""'); | ||
}); | ||
|
||
it('handles missing fields for Transaction CSV', async () => { | ||
const mockTransactions = [{ tx: 'txhash1', getAmount: 2000 }]; | ||
const result = await convertActivityToCsv( | ||
mockTransactions, | ||
CSV_KEYS.transaction | ||
); | ||
expect(result).toContain('"txhash1","2000","","",""'); | ||
}); | ||
}); | ||
|
||
describe('saveCsvFile', () => { | ||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('writes the CSV file to the correct path on Android', async () => { | ||
(Platform.OS as any) = 'android'; | ||
(RNFS.writeFile as jest.Mock).mockResolvedValue(undefined); | ||
|
||
await saveCsvFile('test.csv', 'mock,csv,data'); | ||
|
||
expect(RNFS.writeFile).toHaveBeenCalledWith( | ||
'/mock/download/path/test.csv', | ||
'mock,csv,data', | ||
'utf8' | ||
); | ||
}); | ||
|
||
it('writes the CSV file to the correct path on iOS', async () => { | ||
(Platform.OS as any) = 'ios'; | ||
(RNFS.writeFile as jest.Mock).mockResolvedValue(undefined); | ||
|
||
await saveCsvFile('test.csv', 'mock,csv,data'); | ||
|
||
expect(RNFS.writeFile).toHaveBeenCalledWith( | ||
'/mock/document/path/test.csv', | ||
'mock,csv,data', | ||
'utf8' | ||
); | ||
}); | ||
|
||
it('throws an error when file writing fails (but suppresses console error)', async () => { | ||
const consoleErrorSpy = jest | ||
.spyOn(console, 'error') | ||
.mockImplementation(() => {}); | ||
|
||
(RNFS.writeFile as jest.Mock).mockRejectedValue( | ||
new Error('File write failed') | ||
); | ||
|
||
await expect( | ||
saveCsvFile('test.csv', 'mock,csv,data') | ||
).rejects.toThrow('File write failed'); | ||
|
||
consoleErrorSpy.mockRestore(); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import RNFS from 'react-native-fs'; | ||
import { Platform } from 'react-native'; | ||
|
||
// Keys for CSV export. | ||
export const CSV_KEYS = { | ||
invoice: [ | ||
{ label: 'Amount Paid (sat)', value: 'getAmount' }, | ||
{ label: 'Payment Request', value: 'getPaymentRequest' }, | ||
{ label: 'Payment Hash', value: 'getRHash' }, | ||
{ label: 'Memo', value: 'getMemo' }, | ||
{ label: 'Note', value: 'getNote' }, | ||
{ label: 'Creation Date', value: 'getCreationDate' }, | ||
{ label: 'Expiry', value: 'formattedTimeUntilExpiry' } | ||
], | ||
payment: [ | ||
{ label: 'Destination', value: 'getDestination' }, | ||
{ label: 'Payment Request', value: 'getPaymentRequest' }, | ||
{ label: 'Payment Hash', value: 'paymentHash' }, | ||
{ label: 'Amount Paid (sat)', value: 'getAmount' }, | ||
{ label: 'Memo', value: 'getMemo' }, | ||
{ label: 'Note', value: 'getNote' }, | ||
{ label: 'Creation Date', value: 'getDate' } | ||
], | ||
transaction: [ | ||
{ label: 'Transaction Hash', value: 'tx' }, | ||
{ label: 'Amount (sat)', value: 'getAmount' }, | ||
{ label: 'Total Fees (sat)', value: 'getFee' }, | ||
{ label: 'Note', value: 'getNote' }, | ||
{ label: 'Timestamp', value: 'getDate' } | ||
] | ||
}; | ||
|
||
// Generates a formatted timestamp string for file naming. | ||
export const getFormattedDateTime = (): string => { | ||
const now = new Date(); | ||
const year = now.getFullYear(); | ||
const month = (now.getMonth() + 1).toString().padStart(2, '0'); | ||
const day = now.getDate().toString().padStart(2, '0'); | ||
const hours = now.getHours().toString().padStart(2, '0'); | ||
const minutes = now.getMinutes().toString().padStart(2, '0'); | ||
const seconds = now.getSeconds().toString().padStart(2, '0'); | ||
return `${year}${month}${day}_${hours}${minutes}${seconds}`; | ||
}; | ||
|
||
// Converts activity data into a CSV string. | ||
export const convertActivityToCsv = async ( | ||
data: Array<any>, | ||
keysToInclude: Array<{ label: string; value: string }> | ||
): Promise<string> => { | ||
if (!data || data.length === 0) return ''; | ||
|
||
try { | ||
const header = keysToInclude.map((field) => field.label).join(','); | ||
const rows = data | ||
.map((item) => | ||
keysToInclude | ||
.map((field) => `"${item[field.value] || ''}"`) | ||
.join(',') | ||
) | ||
.join('\n'); | ||
|
||
return `${header}\n${rows}`; | ||
} catch (err) { | ||
console.error(err); | ||
return ''; | ||
} | ||
}; | ||
|
||
//Saves CSV file to the device. | ||
export const saveCsvFile = async (fileName: string, csvData: string) => { | ||
try { | ||
const filePath = | ||
Platform.OS === 'android' | ||
? `${RNFS.DownloadDirectoryPath}/${fileName}` | ||
: `${RNFS.DocumentDirectoryPath}/${fileName}`; | ||
|
||
console.log(`Saving file to: ${filePath}`); | ||
await RNFS.writeFile(filePath, csvData, 'utf8'); | ||
} catch (err) { | ||
console.error('Failed to save CSV file:', err); | ||
throw err; | ||
} | ||
}; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you see if we can use the text character here instead? we use it in the
KeyValue
componentThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure. pushed the changes