What changed, and why it matters
This commit adds an optional 'Verbose Logging' setting to the Skylight Wallet app. When enabled, the app writes detailed logs of wallet operations to a plain text file stored in the app's data folder. The logs include sensitive details such as wallet addresses, transaction destinations, amounts, daemon/proxy addresses, and some API responses. The setting is off by default and must be manually enabled by the user. The main risk is that a user who turns this on may later share the log file with a support person or expose it through a backup, accidentally leaking private wallet activity. The commit itself does not appear to be malicious; it is a debugging feature, but it increases the app's overall sensitivity to data exposure.
Treat this as a defensive review note rather than a critical vulnerability. If maintaining or auditing this app, verify that: (1) the verbose log file is excluded from cloud backups and device backups via Android `allowBackup`/iOS `NSFileProtection` or `excludeFromBackup` keys; (2) the settings screen warns users not to share logs without inspecting them for addresses/transaction details; (3) logs are rotated and deleted securely; (4) no future commit logs private keys, seeds, or passwords; and (5) support workflows sanitize or encrypt logs before transmission. End users should only enable verbose logging when instructed and should delete logs afterward.
Security signals we found
New opt-in verbose logging toggle writes wallet operation details to a persistent plaintext file in app data storage
Logged data includes Monero addresses, transaction destinations/amounts, daemon/proxy addresses, fiat API responses, and FFI pointer addresses
Log file is stored in app-accessible external/documents storage on Android, increasing exposure via backups, ADB, or rooted-device access
Info-level logs are gated by the verbose setting, but the same file is written once verbose logging is enabled
Log retention is capped at 30 days via mtime-based deletion at startup
Sensitive credentials (mnemonic, password) are explicitly redacted in the added log statements
No encryption, access controls, or in-app warning about log contents are added in this commit
Evidence from the diff
The patch introduces a new verboseLoggingEnabled SharedPreferences flag, a settings UI toggle, and a log() helper that appends log lines to <app_data>/logs/log_YYYY-MM-DD.txt. Info-level logs are only written when the setting is enabled; warn/error logs are still emitted to debugPrint. The logging helper uses getExternalStorageDirectory() on Android and getApplicationDocumentsDirectory() on iOS. Many wallet operations now log parameters and return values, including Wallet_address, Wallet_createTransactionMultDest (destination address and amount), WalletManager_resolveOpenAlias, fiat API URL/proxy/body, and daemon/proxy addresses during Wallet_init. Sensitive values such as mnemonic and wallet password are explicitly hidden in the added logs. A cleanOldLogFiles() routine deletes log files older than 30 days and is called at app startup. The Tor service start was moved earlier in main() (before runApp), which is a minor lifecycle change but not a security issue on its own.
Changed components
lib/util/logging.dartlib/screens/settings.dartlib/services/shared_preferences_service.dartlib/models/wallet_model.dartlib/models/fiat_rate_model.dartlib/main.dartLocalization files (app_en.arb, app_pt.arb, generated localizations)Inspect captured patch +376 / −31
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index e1e96e8..ecbac48 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -77,6 +77,8 @@
"settingsAppLockLabel": "App Lock",
"settingsAppLockUnlockReason": "Unlock wallet",
"settingsAppLockUnableToAuthError": "Unable to authenticate.",
+ "settingsVerboseLoggingLabel": "Verbose Logging",
+ "settingsVerboseLoggingDescription": "Logs wallet operations to a text file in the app's data folder for debugging purposes.",
"settingsLanguageLabel": "Language",
"settingsDisplayCurrencyLabel": "Display Currency",
"settingsLwsViewKeysLabel": "LWS View Keys",
diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart
index 27c85ce..d0e9118 100644
--- a/lib/l10n/app_localizations.dart
+++ b/lib/l10n/app_localizations.dart
@@ -560,6 +560,18 @@ abstract class AppLocalizations {
/// **'Unable to authenticate.'**
String get settingsAppLockUnableToAuthError;
+ /// No description provided for @settingsVerboseLoggingLabel.
+ ///
+ /// In en, this message translates to:
+ /// **'Verbose Logging'**
+ String get settingsVerboseLoggingLabel;
+
+ /// No description provided for @settingsVerboseLoggingDescription.
+ ///
+ /// In en, this message translates to:
+ /// **'Logs wallet operations to a text file in the app\'s data folder for debugging purposes.'**
+ String get settingsVerboseLoggingDescription;
+
/// No description provided for @settingsLanguageLabel.
///
/// In en, this message translates to:
diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart
index 20e2243..45e9526 100644
--- a/lib/l10n/app_localizations_en.dart
+++ b/lib/l10n/app_localizations_en.dart
@@ -251,6 +251,13 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settingsAppLockUnableToAuthError => 'Unable to authenticate.';
+ @override
+ String get settingsVerboseLoggingLabel => 'Verbose Logging';
+
+ @override
+ String get settingsVerboseLoggingDescription =>
+ 'Logs wallet operations to a text file in the app\'s data folder for debugging purposes.';
+
@override
String get settingsLanguageLabel => 'Language';
diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart
index 499d3e0..db4b2c7 100644
--- a/lib/l10n/app_localizations_pt.dart
+++ b/lib/l10n/app_localizations_pt.dart
@@ -251,6 +251,13 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get settingsAppLockUnableToAuthError => 'Não foi possível autenticar.';
+ @override
+ String get settingsVerboseLoggingLabel => 'Logs Detalhados';
+
+ @override
+ String get settingsVerboseLoggingDescription =>
+ 'Registra operações da carteira em um arquivo de texto na pasta de dados do app para fins de depuração.';
+
@override
String get settingsLanguageLabel => 'Idioma';
diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb
index 68c23f4..d788e0e 100644
--- a/lib/l10n/app_pt.arb
+++ b/lib/l10n/app_pt.arb
@@ -77,6 +77,8 @@
"settingsAppLockLabel": "Desbloqueio com PIN/Biometria",
"settingsAppLockUnlockReason": "Desbloquear carteira",
"settingsAppLockUnableToAuthError": "Não foi possível autenticar.",
+ "settingsVerboseLoggingLabel": "Logs Detalhados",
+ "settingsVerboseLoggingDescription": "Registra operações da carteira em um arquivo de texto na pasta de dados do app para fins de depuração.",
"settingsLanguageLabel": "Idioma",
"settingsDisplayCurrencyLabel": "Moeda Local",
"settingsLwsViewKeysLabel": "Chaves de Visualização do LWS",
diff --git a/lib/main.dart b/lib/main.dart
index 27f314f..1a7e1c9 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -33,8 +33,10 @@ import 'package:skylight_wallet/util/logging.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
- registerPeriodicTasks();
timeago.setLocaleMessages('pt', timeago.PtBrMessages());
+ TorService.sharedInstance.start();
+ registerPeriodicTasks();
+ cleanOldLogFiles();
runApp(MyApp());
}
@@ -93,8 +95,6 @@ class MyApp extends StatelessWidget {
: '/wallet_home'
: '/welcome';
- TorService.sharedInstance.start();
-
if (walletExists) {
(() async {
await wallet.refresh();
diff --git a/lib/models/fiat_rate_model.dart b/lib/models/fiat_rate_model.dart
index 033674b..ca7f7ac 100644
--- a/lib/models/fiat_rate_model.dart
+++ b/lib/models/fiat_rate_model.dart
@@ -79,8 +79,16 @@ class FiatRateModel with ChangeNotifier {
final proxyInfo = TorService.sharedInstance.getProxyInfo();
try {
+ log(LogLevel.info, 'Fetching rate from fiat api:');
+ log(LogLevel.info, ' url: $url');
+ log(LogLevel.info, ' proxyInfo: $proxyInfo');
+
final response = await makeSocksHttpRequest('GET', url, proxyInfo);
+ log(LogLevel.info, 'Fiat api response:');
+ log(LogLevel.info, ' statusCode: ${response.statusCode}');
+ log(LogLevel.info, ' jsonBody: ${response.jsonBody}');
+
if (response.statusCode == 200) {
final rate = response.jsonBody['result']?[pair]?['o'];
@@ -97,8 +105,7 @@ class FiatRateModel with ChangeNotifier {
_hasFailed = true;
}
} catch (error) {
- log(LogLevel.error, 'Failed to get fiat rate.');
- log(LogLevel.error, error.toString());
+ log(LogLevel.error, 'Failed to get fiat rate: ${error.toString()}');
_hasFailed = true;
}
diff --git a/lib/models/wallet_model.dart b/lib/models/wallet_model.dart
index 82b9680..3db2c6a 100644
--- a/lib/models/wallet_model.dart
+++ b/lib/models/wallet_model.dart
@@ -352,28 +352,40 @@ class WalletModel with ChangeNotifier {
bool useSsl = false,
}) async {
final walletFfiAddr = _w2Wallet!.ffiAddress();
+ final daemonAddress = '${useSsl ? 'https://' : 'http://'}$address';
final proxyAddress = proxyPort != '' ? '127.0.0.1:$proxyPort' : '';
+ final lightWallet = true;
- await Isolate.run(
+ log(LogLevel.info, 'Calling Wallet_init with parameters:');
+ log(LogLevel.info, ' daemonAddress: $daemonAddress');
+ log(LogLevel.info, ' proxyAddress: $proxyAddress');
+ log(LogLevel.info, ' useSsl: $useSsl');
+ log(LogLevel.info, ' lightWallet: $lightWallet');
+
+ final initResult = await Isolate.run(
// ignore: deprecated_member_use
() => monero.Wallet_init(
Pointer.fromAddress(walletFfiAddr),
- daemonAddress: '${useSsl ? 'https://' : 'http://'}$address',
+ daemonAddress: daemonAddress,
proxyAddress: proxyAddress,
useSsl: useSsl,
- lightWallet: true,
+ lightWallet: lightWallet,
),
);
- await Isolate.run(
+ log(LogLevel.info, 'Wallet_init result: $initResult');
+
+ final connectResult = await Isolate.run(
// ignore: deprecated_member_use
() => monero.Wallet_connectToDaemon(Pointer.fromAddress(walletFfiAddr)),
);
+ log(LogLevel.info, 'Wallet_connectToDaemon result: $connectResult');
+
final connectError = _w2Wallet!.errorString();
if (connectError != '') {
- log(LogLevel.warn, connectError);
+ log(LogLevel.warn, 'Wallet_connectToDaemon error: $connectError');
}
}
@@ -381,6 +393,11 @@ class WalletModel with ChangeNotifier {
final walletFfiAddr = _w2Wallet!.ffiAddress();
final historyFfiAddr = _w2TxHistory!.ffiAddress();
+ log(
+ LogLevel.info,
+ 'Calling Wallet_startRefresh, Wallet_refresh, and TransactionHistory_refresh',
+ );
+
await Future.wait([
Isolate.run(
// ignore: deprecated_member_use
@@ -397,11 +414,14 @@ class WalletModel with ChangeNotifier {
),
),
]);
+
+ log(LogLevel.info, 'Wallet refresh methods completed successfully');
}
Future<String> create() async {
// ignore: deprecated_member_use
final polyseed = await Isolate.run(() => monero.Wallet_createPolyseed());
+ log(LogLevel.info, 'Wallet_createPolyseed completed');
final currentHeight = await getCurrentBlockchainHeight();
await restoreFromMnemonic(polyseed, currentHeight);
await SharedPreferencesService.set<int>(
@@ -416,8 +436,15 @@ class WalletModel with ChangeNotifier {
}
Future<int> getRestoreHeight() async {
+ log(LogLevel.info, 'Calling Wallet_getRefreshFromBlockHeight');
+
var w2RestoreHeight = _w2Wallet!.getRefreshFromBlockHeight();
+ log(
+ LogLevel.info,
+ 'Wallet_getRefreshFromBlockHeight result: $w2RestoreHeight',
+ );
+
if (w2RestoreHeight > 0) {
return w2RestoreHeight;
}
@@ -428,15 +455,20 @@ class WalletModel with ChangeNotifier {
0;
}
- Future<int> getCurrentHeight() {
+ Future<int> getCurrentHeight() async {
final wmFfiAddr = _w2WalletManager.ffiAddress();
- return Isolate.run(() {
+ log(LogLevel.info, 'Calling WalletManager_blockchainHeight');
+
+ final height = await Isolate.run(() {
// ignore: deprecated_member_use
return monero.WalletManager_blockchainHeight(
Pointer.fromAddress(wmFfiAddr),
);
});
+
+ log(LogLevel.info, 'WalletManager_blockchainHeight result: $height');
+ return height;
}
Future<MoneroWallet> _getWalletFromLegacySeed({
@@ -452,6 +484,13 @@ class WalletModel with ChangeNotifier {
final wmFfiAddr = _w2WalletManager.ffiAddress();
final walletPath = await getWalletPath();
+ log(LogLevel.info, 'Calling WalletManager_recoveryWallet with parameters:');
+ log(LogLevel.info, ' mnemonic: <hidden>');
+ log(LogLevel.info, ' restoreHeight: $restoreHeight');
+ log(LogLevel.info, ' password: <hidden>');
+ log(LogLevel.info, ' path: $walletPath');
+ log(LogLevel.info, ' isDummy: $isDummy');
+
final walletFfiAddr = await Isolate.run(() {
// ignore: deprecated_member_use
return monero.WalletManager_recoveryWallet(
@@ -464,6 +503,8 @@ class WalletModel with ChangeNotifier {
).address;
});
+ log(LogLevel.info, 'WalletManager_recoveryWallet completed');
+
return MoneroWallet(Pointer<Void>.fromAddress(walletFfiAddr));
}
@@ -494,6 +535,8 @@ class WalletModel with ChangeNotifier {
).address;
});
+ log(LogLevel.info, 'WalletManager_createWalletFromPolyseed completed');
+
return MoneroWallet(Pointer<Void>.fromAddress(walletFfiAddr));
}
@@ -559,11 +602,18 @@ class WalletModel with ChangeNotifier {
throw Exception(errorMsg);
}
+ log(LogLevel.info, 'Calling WalletManager_openWallet with parameters:');
+ log(LogLevel.info, ' path: $path');
+ log(LogLevel.info, ' password: <hidden>');
+
_w2Wallet = _w2WalletManager.openWallet(path: path, password: password);
+
+ log(LogLevel.info, 'WalletManager_openWallet completed');
+
final errorString = _w2WalletManager.errorString();
if (errorString != '') {
- final errorMsg = 'Failed to open existing wallet: $errorString';
+ final errorMsg = 'WalletManager_openWallet error: $errorString';
log(LogLevel.error, errorMsg);
throw Exception(errorMsg);
}
@@ -575,10 +625,16 @@ class WalletModel with ChangeNotifier {
Future<bool> store() async {
final walletFfiAddr = _w2Wallet!.ffiAddress();
- return Isolate.run(
+
+ log(LogLevel.info, 'Calling Wallet_store');
+
+ final result = await Isolate.run(
// ignore: deprecated_member_use
() => monero.Wallet_store(Pointer<Void>.fromAddress(walletFfiAddr)),
);
+
+ log(LogLevel.info, 'Wallet_store result: $result');
+ return result;
}
Future delete() async {
@@ -602,11 +658,17 @@ class WalletModel with ChangeNotifier {
}
Future<bool> hasExistingWallet() async {
+ log(LogLevel.info, 'Calling WalletManager_walletExists with parameters:');
+ log(LogLevel.info, ' path: ${await getWalletPath()}');
+
final exists = _w2WalletManager.walletExists(await getWalletPath());
+
+ log(LogLevel.info, 'WalletManager_walletExists result: $exists');
+
final errorString = _w2WalletManager.errorString();
if (errorString != '') {
- log(LogLevel.error, 'Failed to check if wallet exists: $errorString');
+ log(LogLevel.error, 'WalletManager_walletExists error: $errorString');
}
return exists;
@@ -615,61 +677,100 @@ class WalletModel with ChangeNotifier {
Future<bool> getIsConnected() async {
final w2WalletFfiAddr = _w2Wallet!.ffiAddress();
+ log(LogLevel.info, 'Calling Wallet_connected:');
+
final connected = await Isolate.run(
// ignore: deprecated_member_use
() => monero.Wallet_connected(Pointer<Void>.fromAddress(w2WalletFfiAddr)),
);
+ log(LogLevel.info, 'Wallet_connected result: $connected');
+
return connected != 0;
}
Future<void> loadIsSynced() async {
final walletFfiAddr = _w2Wallet!.ffiAddress();
+
+ log(LogLevel.info, 'Calling Wallet_synchronized:');
+
_isSynced = await Isolate.run(
() =>
// ignore: deprecated_member_use
monero.Wallet_synchronized(Pointer<Void>.fromAddress(walletFfiAddr)),
);
+
+ log(LogLevel.info, 'Wallet_synchronized result: $_isSynced');
}
Future<void> loadSyncedHeight() async {
final walletFfiAddr = _w2Wallet!.ffiAddress();
+
+ log(LogLevel.info, 'Calling Wallet_blockChainHeight:');
+
_syncedHeight = await Isolate.run(
// ignore: deprecated_member_use
() => monero.Wallet_blockChainHeight(
Pointer<Void>.fromAddress(walletFfiAddr),
),
);
+
+ log(LogLevel.info, 'Wallet_blockChainHeight result: $_syncedHeight');
}
Future<void> loadTotalBalance() async {
final walletFfiAddr = _w2Wallet!.ffiAddress();
+
+ final accountIndex = 0;
+
+ log(LogLevel.info, 'Calling Wallet_balance with parameters:');
+ log(LogLevel.info, ' accountIndex: $accountIndex');
+
final amount = await Isolate.run(
// ignore: deprecated_member_use
() => monero.Wallet_balance(
Pointer<Void>.fromAddress(walletFfiAddr),
- accountIndex: 0,
+ accountIndex: accountIndex,
),
);
+ log(LogLevel.info, 'Wallet_balance result: $amount');
+
_totalBalance = doubleAmountFromInt(amount);
}
Future<void> loadUnlockedBalance() async {
final walletFfiAddr = _w2Wallet!.ffiAddress();
+
+ final accountIndex = 0;
+
+ log(LogLevel.info, 'Calling Wallet_unlockedBalance with parameters:');
+ log(LogLevel.info, ' accountIndex: $accountIndex');
+
final amount = await Isolate.run(
// ignore: deprecated_member_use
() => monero.Wallet_unlockedBalance(
Pointer<Void>.fromAddress(walletFfiAddr),
- accountIndex: 0,
+ accountIndex: accountIndex,
),
);
+ log(LogLevel.info, 'Wallet_unlockedBalance result: $amount');
+
_unlockedBalance = doubleAmountFromInt(amount);
}
String getPrimaryAddress() {
- return _w2Wallet!.address(accountIndex: 0);
+ final accountIndex = 0;
+
+ log(LogLevel.info, 'Calling Wallet_address with parameters:');
+ log(LogLevel.info, ' accountIndex: $accountIndex');
+
+ final address = _w2Wallet!.address(accountIndex: accountIndex);
+
+ log(LogLevel.info, 'Wallet_address result: $address');
+
+ return address;
}
Future<String> getUnusedSubaddress() async {
@@ -691,7 +792,18 @@ class WalletModel with ChangeNotifier {
nextSubaddrIndex++;
}
- return _w2Wallet!.address(accountIndex: 0, addressIndex: nextSubaddrIndex);
+ log(LogLevel.info, 'Calling Wallet_address with parameters:');
+ log(LogLevel.info, ' accountIndex: 0');
+ log(LogLevel.info, ' addressIndex: $nextSubaddrIndex');
+
+ final address = _w2Wallet!.address(
+ accountIndex: 0,
+ addressIndex: nextSubaddrIndex,
+ );
+
+ log(LogLevel.info, 'Wallet_address result: $address');
+
+ return address;
}
Future<MoneroPendingTransaction> createTx(
@@ -702,24 +814,51 @@ class WalletModel with ChangeNotifier {
final amountInt = _w2Wallet!.amountFromDouble(amount);
final w2WalletFfiAddr = _w2Wallet!.ffiAddress();
+ final dstAddr = [destinationAddress];
+ final amounts = [amountInt];
+ final mixinCount = 15;
+ final pendingTransactionPriority = 0;
+ final subaddrAccount = 0;
+
+ log(
+ LogLevel.info,
+ 'Calling Wallet_createTransactionMultDest with parameters:',
+ );
+ log(LogLevel.info, ' w2WalletFfiAddr: $w2WalletFfiAddr');
+ log(LogLevel.info, ' isSweepAll: $isSweepAll');
+ log(LogLevel.info, ' dstAddr: $dstAddr');
+ log(LogLevel.info, ' amounts: $amounts');
+ log(LogLevel.info, ' mixinCount: $mixinCount');
+ log(
+ LogLevel.info,
+ ' pendingTransactionPriority: $pendingTransactionPriority',
+ );
+ log(LogLevel.info, ' subaddr_account: $subaddrAccount');
+
final txPointer = Pointer<Void>.fromAddress(
await Isolate.run(() {
// ignore: deprecated_member_use
return monero.Wallet_createTransactionMultDest(
Pointer.fromAddress(w2WalletFfiAddr),
isSweepAll: isSweepAll,
- dstAddr: [destinationAddress],
- amounts: [amountInt],
- mixinCount: 15,
- pendingTransactionPriority: 0,
- subaddr_account: 0,
+ dstAddr: dstAddr,
+ amounts: amounts,
+ mixinCount: mixinCount,
+ pendingTransactionPriority: pendingTransactionPriority,
+ subaddr_account: subaddrAccount,
).address;
}),
);
+ log(LogLevel.info, 'Wallet_createTransactionMultDest completed');
+
final pendingTx = MoneroPendingTransaction(txPointer);
if (pendingTx.errorString() != '') {
+ log(
+ LogLevel.error,
+ 'Failed to create transaction: ${pendingTx.errorString()}',
+ );
throw Exception(pendingTx.errorString());
}
@@ -732,7 +871,14 @@ class WalletModel with ChangeNotifier {
) async {
final txFfiAddr = tx.ffiAddress();
- await Isolate.run(() {
+ final filename = '';
+ final overwrite = false;
+
+ log(LogLevel.info, 'Calling PendingTransaction_commit with parameters:');
+ log(LogLevel.info, ' filename: $filename');
+ log(LogLevel.info, ' overwrite: $overwrite');
+
+ final commitResult = await Isolate.run(() {
// ignore: deprecated_member_use
return monero.PendingTransaction_commit(
Pointer.fromAddress(txFfiAddr),
@@ -741,11 +887,12 @@ class WalletModel with ChangeNotifier {
);
});
+ log(LogLevel.info, 'PendingTransaction_commit result: $commitResult');
+
final errorMsg = tx.errorString();
if (errorMsg != '' && errorMsg != 'Schema expected string') {
- log(LogLevel.error, 'Failed to commit transaction');
- log(LogLevel.error, errorMsg);
+ log(LogLevel.error, 'PendingTransaction_commit error: $errorMsg');
throw FormatException(errorMsg);
}
@@ -775,10 +922,26 @@ class WalletModel with ChangeNotifier {
}
String resolveOpenAlias(String address) {
- return _w2WalletManager.resolveOpenAlias(
+ final dnssecValid = true;
+
+ log(
+ LogLevel.info,
+ 'Calling WalletManager_resolveOpenAlias with parameters:',
+ );
+ log(LogLevel.info, ' address: $address');
+ log(LogLevel.info, ' dnssecValid: $dnssecValid');
+
+ final resolvedAddress = _w2WalletManager.resolveOpenAlias(
address: address,
- dnssecValid: true,
+ dnssecValid: dnssecValid,
+ );
+
+ log(
+ LogLevel.info,
+ 'WalletManager_resolveOpenAlias result: $resolvedAddress',
);
+
+ return resolvedAddress;
}
Future<void> addPendingOutgoingTx(TxDetails tx) async {
diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart
index 024a78a..0f26e85 100644
--- a/lib/screens/settings.dart
+++ b/lib/screens/settings.dart
@@ -24,6 +24,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
var _newTxNotificationsEnabled = false;
var _fiatCurrency = 'USD';
var _appLockEnabled = false;
+ var _verboseLoggingEnabled = false;
@override
void initState() {
@@ -50,10 +51,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
) ??
false;
+ final verboseLoggingEnabled =
+ await SharedPreferencesService.get<bool>(
+ SharedPreferencesKeys.verboseLoggingEnabled,
+ ) ??
+ false;
+
setState(() {
_newTxNotificationsEnabled = newTxNotificationsEnabled;
_fiatCurrency = fiatCurrency;
_appLockEnabled = appLockEnabled;
+ _verboseLoggingEnabled = verboseLoggingEnabled;
});
}
@@ -126,6 +134,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
+ void _setVerboseLoggingEnabled(bool value) async {
+ setState(() {
+ _verboseLoggingEnabled = value;
+ });
+
+ await SharedPreferencesService.set<bool>(
+ SharedPreferencesKeys.verboseLoggingEnabled,
+ value,
+ );
+ }
+
void _setFiatCurrency(String? value) async {
if (value == null) return;
@@ -335,6 +354,30 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
],
),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ i18n.settingsVerboseLoggingLabel,
+ style: TextStyle(fontSize: 18),
+ ),
+ Text(
+ i18n.settingsVerboseLoggingDescription,
+ style: TextStyle(fontSize: 13, color: Colors.grey[600]),
+ ),
+ ],
+ ),
+ ),
+ Switch(
+ value: _verboseLoggingEnabled,
+ onChanged: _setVerboseLoggingEnabled,
+ ),
+ ],
+ ),
Container(
margin: EdgeInsetsGeometry.symmetric(vertical: 10),
child: Divider(),
diff --git a/lib/services/shared_preferences_service.dart b/lib/services/shared_preferences_service.dart
index 9553c18..b19b75c 100644
--- a/lib/services/shared_preferences_service.dart
+++ b/lib/services/shared_preferences_service.dart
@@ -6,6 +6,7 @@ class SharedPreferencesKeys {
static const String fiatRate = 'fiatRate';
static const String notificationsEnabled = 'notificationsEnabled';
static const String appLockEnabled = 'appLockEnabled';
+ static const String verboseLoggingEnabled = 'verboseLoggingEnabled';
static const String connectionAddress = 'connectionAddress';
static const String connectionProxyPort = 'connectionProxyPort';
static const String connectionUseTor = 'connectionUseTor';
diff --git a/lib/util/logging.dart b/lib/util/logging.dart
index e4f6195..dbdf10d 100644
--- a/lib/util/logging.dart
+++ b/lib/util/logging.dart
@@ -1,18 +1,119 @@
+import 'dart:io';
import 'package:flutter/foundation.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:skylight_wallet/services/shared_preferences_service.dart';
enum LogLevel { info, warn, error }
String _timestamp() => DateTime.now().toUtc().toIso8601String();
-void log(LogLevel level, String message, [Map<String, dynamic>? meta]) {
+Future<File> _getLogFile() async {
+ Directory? directory;
+
+ // Get external storage directory for Android, or documents directory for iOS
+ if (Platform.isAndroid) {
+ directory = await getExternalStorageDirectory();
+ } else if (Platform.isIOS) {
+ directory = await getApplicationDocumentsDirectory();
+ } else {
+ directory = await getApplicationDocumentsDirectory();
+ }
+
+ if (directory == null) {
+ throw Exception('Could not access storage directory');
+ }
+
+ // Create logs subdirectory
+ final logsDir = Directory('${directory.path}/logs');
+ if (!await logsDir.exists()) {
+ await logsDir.create(recursive: true);
+ }
+
+ // Create filename with current date (YYYY-MM-DD)
+ final dateStr = DateTime.now().toIso8601String().split('T').first;
+ final filePath = '${logsDir.path}/log_$dateStr.txt';
+
+ return File(filePath);
+}
+
+Future<void> cleanOldLogFiles() async {
+ try {
+ Directory? directory;
+
+ if (Platform.isAndroid) {
+ directory = await getExternalStorageDirectory();
+ } else if (Platform.isIOS) {
+ directory = await getApplicationDocumentsDirectory();
+ } else {
+ directory = await getApplicationDocumentsDirectory();
+ }
+
+ if (directory == null) {
+ return;
+ }
+
+ final logsDir = Directory('${directory.path}/logs');
+ if (!await logsDir.exists()) {
+ return;
+ }
+
+ // Get current time and calculate cutoff date
+ const daysToKeep = 30;
+ final now = DateTime.now();
+ final cutoffDate = now.subtract(const Duration(days: daysToKeep));
+
+ // List all files in logs directory
+ final files = await logsDir.list().toList();
+
+ for (var entity in files) {
+ if (entity is File) {
+ final stat = await entity.stat();
+ final modifiedDate = stat.modified;
+
+ // Delete file if it's older than the cutoff date
+ if (modifiedDate.isBefore(cutoffDate)) {
+ await entity.delete();
+ debugPrint('Deleted old log file: ${entity.path}');
+ }
+ }
+ }
+ } catch (error) {
+ debugPrint('Failed to clean old logs: $error');
+ }
+}
+
+void log(LogLevel level, String message, [Map<String, dynamic>? meta]) async {
+ // Check if verbose logging is enabled for info-level logs
+ final verboseLoggingEnabled =
+ await SharedPreferencesService.get<bool>(
+ SharedPreferencesKeys.verboseLoggingEnabled,
+ ) ??
+ false;
+
+ if (level == LogLevel.info && !verboseLoggingEnabled) {
+ if (!verboseLoggingEnabled) {
+ return; // Skip info logs when verbose logging is disabled
+ }
+ }
+
final ts = _timestamp();
final label = level.toString().split('.').last.toUpperCase();
final metaStr = (meta == null || meta.isEmpty) ? '' : ' ${meta.toString()}';
- final output = '[$ts] [$label] $message$metaStr';
+ final output = '[$ts] [$label] $message $metaStr';
if (level == LogLevel.error) {
debugPrint(output);
} else {
if (kDebugMode) debugPrint(output);
}
+
+ if (verboseLoggingEnabled) {
+ _getLogFile()
+ .then((file) {
+ file.writeAsString('$output\n', mode: FileMode.append);
+ })
+ .catchError((error) {
+ debugPrint('Failed to write log to file: $error');
+ });
+ }
}
Why this scored 41/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.