diff --git a/App.tsx b/App.tsx
index 6217f164c7..1d27c79c33 100644
--- a/App.tsx
+++ b/App.tsx
@@ -135,6 +135,7 @@ import AddContact from './views/Settings/AddContact';
import ContactDetails from './views/ContactDetails';
import PendingHTLCs from './views/PendingHTLCs';
+import ActivityExportOptions from './views/ActivityExportOptions';
// POS
import Order from './views/Order';
@@ -938,6 +939,12 @@ export default class App extends React.PureComponent {
Sweepremoteclosed
}
/>
+
>
diff --git a/locales/en.json b/locales/en.json
index 5259a69f4b..2caf85ee4b 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -1017,8 +1017,22 @@
"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.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.",
+ "views.ActivityExport.noDataAvailableForSelection": "No activity data is available for the selected type.",
+ "views.ActivityExport.noDataForSelectedDates": "No records found in the selected date range. Try a different date.",
+ "views.ActivityExport.noValidDataForDownload": "There is no valid data available for download.",
"views.Routing.RoutingEvent.sourceChannel": "Source Channel",
"views.Routing.RoutingEvent.destinationChannel": "Destination Channel",
"views.Olympians.title": "Olympians",
diff --git a/utils/ActivityCsvUtils.test.ts b/utils/ActivityCsvUtils.test.ts
new file mode 100644
index 0000000000..323f8f7aa5
--- /dev/null
+++ b/utils/ActivityCsvUtils.test.ts
@@ -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();
+ });
+ });
+});
diff --git a/utils/ActivityCsvUtils.ts b/utils/ActivityCsvUtils.ts
new file mode 100644
index 0000000000..18df30b754
--- /dev/null
+++ b/utils/ActivityCsvUtils.ts
@@ -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,
+ keysToInclude: Array<{ label: string; value: string }>
+): Promise => {
+ 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;
+ }
+};
diff --git a/views/Activity/ActivityToCsv.tsx b/views/Activity/ActivityToCsv.tsx
index 575b0dcb47..a5893e065c 100644
--- a/views/Activity/ActivityToCsv.tsx
+++ b/views/Activity/ActivityToCsv.tsx
@@ -1,15 +1,23 @@
import React, { useState } from 'react';
-import { StyleSheet, View, Platform, Alert, Modal } from 'react-native';
-import RNFS from 'react-native-fs';
-import Button from '../../components/Button';
-import TextInput from '../../components/TextInput';
-import { localeString } from '../../utils/LocaleUtils';
-import { themeColor } from '../../utils/ThemeUtils';
+import { StyleSheet, View, Alert, Modal } from 'react-native';
+
import Invoice from '../../models/Invoice';
import Payment from '../../models/Payment';
import Transaction from '../../models/Transaction';
+
+import Button from '../../components/Button';
+import TextInput from '../../components/TextInput';
import LoadingIndicator from '../../components/LoadingIndicator';
+import { localeString } from '../../utils/LocaleUtils';
+import { themeColor } from '../../utils/ThemeUtils';
+import {
+ getFormattedDateTime,
+ convertActivityToCsv,
+ saveCsvFile,
+ CSV_KEYS
+} from '../../utils/ActivityCsvUtils';
+
interface ActivityProps {
filteredActivity: Array;
isVisible: boolean;
@@ -29,87 +37,22 @@ const ActivityToCsv: React.FC = ({
closeModal();
};
- const getFormattedDateTime = () => {
- 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}`;
- };
-
- const convertActivityToCsv = async (
- data: Array,
- keysToInclude: Array
- ) => {
- if (!data || data.length === 0) {
- return '';
- }
-
- try {
- const header = keysToInclude.map((field) => field.label).join(',');
- const rows = data
- ?.map((item: any) =>
- keysToInclude
- .map((field) => `"${item[field.value]}"` || '')
- .join(',')
- )
- .join('\n');
-
- return `${header}\n${rows}`;
- } catch (err) {
- console.error(err);
- return '';
- }
- };
-
const downloadCsv = async () => {
setIsLoading(true);
setTimeout(async () => {
- const invoiceKeys = [
- { 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' }
- ];
-
- const paymentKeys = [
- { 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' }
- ];
-
- const transactionKeys = [
- { label: 'Transaction Hash', value: 'tx' },
- { label: 'Amount (sat)', value: 'getAmount' },
- { label: 'Total Fees (sat)', value: 'getFee' },
- { label: 'Note', value: 'getNote' },
- { label: 'Timestamp', value: 'getDate' }
- ];
-
const invoiceCsv = await convertActivityToCsv(
filteredActivity.filter((item: any) => item instanceof Invoice),
- invoiceKeys
+ CSV_KEYS.invoice
);
const paymentCsv = await convertActivityToCsv(
filteredActivity.filter((item: any) => item instanceof Payment),
- paymentKeys
+ CSV_KEYS.payment
);
const transactionCsv = await convertActivityToCsv(
filteredActivity.filter(
(item: any) => item instanceof Transaction
),
- transactionKeys
+ CSV_KEYS.transaction
);
if (!invoiceCsv && !paymentCsv && !transactionCsv) {
@@ -120,43 +63,21 @@ const ActivityToCsv: React.FC = ({
try {
const dateTime = getFormattedDateTime();
const baseFileName = customFileName || `zeus_${dateTime}`;
- const invoiceFileName = `${baseFileName}_ln_invoices.csv`;
- const paymentFileName = `${baseFileName}_ln_payments.csv`;
- const transactionFileName = `${baseFileName}_onchain.csv`;
-
- const invoiceFilePath =
- Platform.OS === 'android'
- ? `${RNFS.DownloadDirectoryPath}/${invoiceFileName}`
- : `${RNFS.DocumentDirectoryPath}/${invoiceFileName}`;
-
- const paymentFilePath =
- Platform.OS === 'android'
- ? `${RNFS.DownloadDirectoryPath}/${paymentFileName}`
- : `${RNFS.DocumentDirectoryPath}/${paymentFileName}`;
-
- const transactionFilePath =
- Platform.OS === 'android'
- ? `${RNFS.DownloadDirectoryPath}/${transactionFileName}`
- : `${RNFS.DocumentDirectoryPath}/${transactionFileName}`;
-
- if (invoiceCsv) {
- console.log('invoiceFilePath', invoiceFilePath);
- await RNFS.writeFile(invoiceFilePath, invoiceCsv, 'utf8');
- }
-
- if (paymentCsv) {
- console.log('paymentFilePath', paymentFilePath);
- await RNFS.writeFile(paymentFilePath, paymentCsv, 'utf8');
- }
-
- if (transactionCsv) {
- console.log('transactionFilePath', transactionFilePath);
- await RNFS.writeFile(
- transactionFilePath,
- transactionCsv,
- 'utf8'
+ if (invoiceCsv)
+ await saveCsvFile(
+ `${baseFileName}_ln_invoices.csv`,
+ invoiceCsv
+ );
+ if (paymentCsv)
+ await saveCsvFile(
+ `${baseFileName}_ln_payments.csv`,
+ paymentCsv
+ );
+ if (transactionCsv)
+ await saveCsvFile(
+ `${baseFileName}_onchain.csv`,
+ transactionCsv
);
- }
Alert.alert(
localeString('general.success'),
diff --git a/views/ActivityExportOptions.tsx b/views/ActivityExportOptions.tsx
new file mode 100644
index 0000000000..46b9ba3a77
--- /dev/null
+++ b/views/ActivityExportOptions.tsx
@@ -0,0 +1,616 @@
+import * as React from 'react';
+import {
+ View,
+ TouchableOpacity,
+ StyleSheet,
+ Alert,
+ ScrollView,
+ Modal,
+ Platform
+} from 'react-native';
+import Icon from 'react-native-vector-icons/Feather';
+import { inject, observer } from 'mobx-react';
+
+import Header from '../components/Header';
+import Button from '../components/Button';
+import TextInput from '../components/TextInput';
+import Text from '../components/Text';
+import LoadingIndicator from '../components/LoadingIndicator';
+
+import { localeString } from '../utils/LocaleUtils';
+import { themeColor } from '../utils/ThemeUtils';
+import {
+ getFormattedDateTime,
+ convertActivityToCsv,
+ saveCsvFile,
+ CSV_KEYS
+} from '.././utils/ActivityCsvUtils';
+
+import ActivityStore from '../stores/ActivityStore';
+import SettingsStore from '../stores/SettingsStore';
+
+import Invoice from '../models/Invoice';
+import Payment from '../models/Payment';
+import Transaction from '../models/Transaction';
+import DatePicker from 'react-native-date-picker';
+import { CheckBox } from 'react-native-elements';
+
+interface ActivityExportOptionsProps {
+ navigation: any;
+ ActivityStore: ActivityStore;
+ SettingsStore: SettingsStore;
+}
+
+interface ActivityExportOptionsState {
+ isCsvLoading: boolean;
+ isActivityFetching: boolean;
+ filteredActivity: any;
+ isModalVisible: boolean;
+ showInfoModal: boolean;
+ customFileName: string;
+ fromDate: any;
+ toDate: any;
+ exportType: any;
+ downloadCompleteData: boolean;
+}
+
+@inject('ActivityStore', 'SettingsStore')
+@observer
+export default class ActivityExportOptions extends React.Component<
+ ActivityExportOptionsProps,
+ ActivityExportOptionsState
+> {
+ constructor(props: ActivityExportOptionsProps) {
+ super(props);
+ this.state = {
+ isCsvLoading: false,
+ isActivityFetching: true,
+ filteredActivity: [],
+ isModalVisible: false,
+ showInfoModal: false,
+ customFileName: '',
+ fromDate: null,
+ toDate: null,
+ exportType: '',
+ downloadCompleteData: false
+ };
+ }
+
+ componentDidMount() {
+ this.fetchAndFilterActivity();
+ }
+
+ fetchAndFilterActivity = async () => {
+ const { SettingsStore, ActivityStore } = this.props;
+ const { locale } = SettingsStore.settings;
+
+ try {
+ // Call getActivityAndFilter to fetch and filter activity data
+ await ActivityStore.getActivityAndFilter(locale);
+
+ // Update filteredActivity in state
+ this.setState({
+ filteredActivity: ActivityStore.filteredActivity,
+ isActivityFetching: false
+ });
+ } catch (err) {
+ console.error('Failed to fetch activity data:', err);
+ this.setState({ isActivityFetching: false });
+ Alert.alert(localeString('general.error'));
+ }
+ };
+
+ filterDataByDate = (data: any) => {
+ const { fromDate, toDate, downloadCompleteData } = this.state;
+ if (!fromDate && !toDate) return data;
+
+ if (downloadCompleteData) return data;
+
+ return data.filter((item: any) => {
+ const itemDate = new Date(item.getDate);
+ if (isNaN(itemDate.getTime())) return false;
+ if (fromDate && itemDate < fromDate) return false;
+ if (toDate && itemDate > toDate) return false;
+ return true;
+ });
+ };
+
+ downloadCsv = async (type: 'invoice' | 'payment' | 'transaction') => {
+ this.setState({ isCsvLoading: true }, async () => {
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ let { filteredActivity } = this.state;
+
+ if (!filteredActivity || filteredActivity.length === 0) {
+ Alert.alert(
+ localeString(
+ 'views.ActivityExport.noDataAvailableForSelection'
+ )
+ );
+ this.setState({ isCsvLoading: false });
+ return;
+ }
+
+ let filteredData: any;
+
+ filteredData = filteredActivity.filter((item: any) => {
+ if (type === 'invoice') return item instanceof Invoice;
+ if (type === 'payment') return item instanceof Payment;
+ if (type === 'transaction') return item instanceof Transaction;
+ return false;
+ });
+ filteredData = this.filterDataByDate(filteredData);
+
+ if (!filteredData || filteredData.length === 0) {
+ Alert.alert(
+ localeString('views.ActivityExport.noDataForSelectedDates')
+ );
+ this.setState({ isCsvLoading: false });
+ return;
+ }
+
+ const csvData = await convertActivityToCsv(
+ filteredData,
+ CSV_KEYS[type]
+ );
+
+ if (!csvData) {
+ Alert.alert(
+ localeString('views.ActivityExport.noValidDataForDownload')
+ );
+ this.setState({ isCsvLoading: false });
+ return;
+ }
+
+ try {
+ const dateTime = getFormattedDateTime();
+ const baseFileName = this.state.customFileName
+ ? `${this.state.customFileName}.csv`
+ : `zeus_${dateTime}_${type}.csv`;
+
+ await saveCsvFile(baseFileName, csvData);
+
+ this.setState({ isModalVisible: false });
+
+ this.closeAndClearInput();
+
+ Alert.alert(
+ localeString('general.success'),
+ localeString('views.ActivityToCsv.csvDownloaded')
+ );
+ } catch (err) {
+ console.error('Failed to save CSV file:', err);
+ Alert.alert(
+ localeString('general.error'),
+ localeString('views.ActivityToCsv.csvDownloadFailed')
+ );
+ } finally {
+ this.setState({ isCsvLoading: false });
+ }
+ });
+ };
+
+ closeAndClearInput = () => {
+ this.setState({
+ isModalVisible: false,
+ customFileName: '',
+ fromDate: new Date(),
+ toDate: new Date(),
+ downloadCompleteData: false
+ });
+ };
+
+ openModal = (type: 'invoice' | 'transaction' | 'payment') => {
+ let earliestDate = new Date(
+ new Date().setMonth(new Date().getMonth() - 1)
+ ); // Default to 1 month ago
+
+ this.setState({
+ isModalVisible: true,
+ exportType: type,
+ fromDate: earliestDate,
+ toDate: new Date()
+ });
+ };
+
+ renderModal = () => {
+ const {
+ isModalVisible,
+ isCsvLoading,
+ customFileName,
+ fromDate,
+ toDate,
+ downloadCompleteData
+ } = this.state;
+ return (
+
+
+
+ {isCsvLoading ? (
+
+ ) : (
+ <>
+
+ {localeString(
+ 'views.ActivityExport.dateRange'
+ )}
+
+
+
+
+ this.setState({
+ downloadCompleteData:
+ !downloadCompleteData,
+ fromDate:
+ !downloadCompleteData
+ ? null
+ : fromDate,
+ toDate: !downloadCompleteData
+ ? null
+ : toDate
+ })
+ }
+ containerStyle={{
+ backgroundColor: 'transparent',
+ borderWidth: 0
+ }}
+ textStyle={{
+ color: themeColor('text')
+ }}
+ checkedColor="green"
+ />
+
+ {!downloadCompleteData && (
+ <>
+
+ {localeString(
+ 'views.ActivityExport.fromDate'
+ )}
+
+ {
+ this.setState({
+ fromDate: date
+ });
+ if (
+ toDate &&
+ date > toDate
+ ) {
+ this.setState({
+ toDate: date
+ });
+ }
+ }}
+ style={{
+ height: 100,
+ marginTop: 10,
+ marginBottom: 20,
+ alignSelf: 'center'
+ }}
+ maximumDate={new Date()}
+ textColor={themeColor('text')}
+ androidVariant="nativeAndroid"
+ />
+
+ {localeString(
+ 'views.ActivityExport.toDate'
+ )}
+
+ {
+ if (
+ fromDate &&
+ date < fromDate
+ ) {
+ Alert.alert(
+ 'Invalid Date'
+ );
+ } else {
+ this.setState({
+ toDate: date
+ });
+ }
+ }}
+ style={{
+ height: 100,
+ marginTop: 10,
+ marginBottom: 20,
+ alignSelf: 'center'
+ }}
+ maximumDate={new Date()}
+ textColor={themeColor('text')}
+ androidVariant="nativeAndroid"
+ />
+ >
+ )}
+
+
+
+ this.setState({
+ customFileName: text
+ })
+ }
+ style={{ marginHorizontal: 12 }}
+ />
+
+
+ >
+ )}
+
+
+
+ );
+ };
+
+ renderInfoModal = () => {
+ const { showInfoModal } = this.state;
+ return (
+ this.setState({ showInfoModal: false })}
+ >
+
+
+
+ {Platform.OS === 'android'
+ ? localeString(
+ 'views.ActivityExport.explainerAndroid'
+ )
+ : localeString(
+ 'views.ActivityExport.explaineriOS'
+ )}
+
+
+ this.setState({ showInfoModal: false })
+ }
+ secondary
+ />
+
+
+
+ );
+ };
+
+ render() {
+ const { isActivityFetching } = this.state;
+
+ return (
+
+ >
+ ) : (
+
+ this.setState({ showInfoModal: true })
+ }
+ >
+ ⓘ
+
+ )
+ }
+ navigation={this.props.navigation}
+ />
+ {this.renderModal()}
+ {this.renderInfoModal()}
+
+
+ {isActivityFetching ? (
+
+ ) : (
+ <>
+ this.openModal('invoice')}
+ >
+
+
+ {localeString(
+ 'views.ActivityExport.exportInvoices'
+ )}
+
+
+
+ this.openModal('payment')}
+ >
+
+
+ {localeString(
+ 'views.ActivityExport.exportPayments'
+ )}
+
+
+
+ this.openModal('transaction')}
+ >
+
+
+ {localeString(
+ 'views.ActivityExport.exportTransactions'
+ )}
+
+
+ >
+ )}
+
+
+ );
+ }
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ padding: 20
+ },
+ optionButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 15,
+ marginVertical: 10,
+ borderRadius: 10
+ },
+ optionText: {
+ marginLeft: 15,
+ fontSize: 16
+ },
+ modalOverlay: {
+ flex: 1,
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
+ justifyContent: 'center',
+ alignItems: 'center'
+ },
+ buttonContainer: {
+ width: '100%',
+ marginTop: 4
+ }
+});
diff --git a/views/Tools.tsx b/views/Tools.tsx
index a2a1c83036..f6735f797e 100644
--- a/views/Tools.tsx
+++ b/views/Tools.tsx
@@ -23,6 +23,7 @@ import { localeString } from '../utils/LocaleUtils';
import { themeColor } from '../utils/ThemeUtils';
import SettingsStore from '../stores/SettingsStore';
+import { Icon } from 'react-native-elements';
interface ToolsProps {
navigation: StackNavigationProp;
@@ -286,6 +287,45 @@ export default class Tools extends React.Component {
)}
+ {selectedNode && (
+
+
+ navigation.navigate('ActivityExportOptions')
+ }
+ >
+
+
+
+
+ {localeString('views.ActivityExport.title')}
+
+
+
+
+
+
+ )}
);