[#126] TCA component for user logs (#526)

- OSLogger for the defined categories
- TCA logger for the TCA logs
- WalletLogger for the secant logs
- SDKLogger passed to the SDK
- unit tests for the loggers
- export category OS logs
- share txt files (sdk, tca, wallet logs) via native share dialog
- timestamp extension so we see even milliseconds
- txt files up to some X size
- simple button enable/disable logic and wrapping the export work in the Task
- TODO for empty catches
- OSLogger refactored to OSLogger_, just temporary change
- export and share divided into business logic and view logic parts
- unit tests for the TCA part
- async let syntax for the export logs
- simple activity indicator so testers know export is in progress
- static date formatters so we don't instantiate it over and over
This commit is contained in:
Lukas Korba 2023-02-01 09:08:22 +01:00 committed by GitHub
parent 0d2d898f4e
commit 1aca887800
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 892 additions and 19 deletions

View File

@ -99,6 +99,11 @@
9E01F8282833CDA0000EFC57 /* ScanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E01F8272833CDA0000EFC57 /* ScanTests.swift */; };
9E02B56A27FED43E005B809B /* FileManagerInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E02B56927FED43E005B809B /* FileManagerInterface.swift */; };
9E02B56C27FED475005B809B /* DatabaseFilesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E02B56B27FED475005B809B /* DatabaseFilesTests.swift */; };
9E0F5741297E7F1D005304FA /* TCALogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0F5740297E7F1C005304FA /* TCALogger.swift */; };
9E0F5743297EB96C005304FA /* TCALoggerReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0F5742297EB96C005304FA /* TCALoggerReducer.swift */; };
9E0F5745297EBA1B005304FA /* LogStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0F5744297EBA1B005304FA /* LogStore.swift */; };
9E0F5747297EE5F3005304FA /* OSLogger_.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0F5746297EE5F3005304FA /* OSLogger_.swift */; };
9E0F574B2980260D005304FA /* LoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0F574A2980260D005304FA /* LoggerTests.swift */; };
9E153A5F2920CE2700112F41 /* MnemonicInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E153A5E2920CD5100112F41 /* MnemonicInterface.swift */; };
9E153A602920CE2700112F41 /* MnemonicLiveKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E153A5C2920CD5100112F41 /* MnemonicLiveKey.swift */; };
9E153A612920CE2700112F41 /* MnemonicMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E153A5B2920CD5100112F41 /* MnemonicMocks.swift */; };
@ -137,6 +142,10 @@
9E5BF648282277BE00BA3F17 /* NotificationCenterInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF647282277BE00BA3F17 /* NotificationCenterInterface.swift */; };
9E5BF64F2823E94900BA3F17 /* TransactionAddressTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF64D2823E94900BA3F17 /* TransactionAddressTextField.swift */; };
9E5BF6502823E94900BA3F17 /* TransactionAddressTextFieldStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF64E2823E94900BA3F17 /* TransactionAddressTextFieldStore.swift */; };
9E612C6F2987A9B100D09B09 /* UIShareDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E612C6E2987A9B100D09B09 /* UIShareDialog.swift */; };
9E612C7229880E9200D09B09 /* LogsHandlerInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E612C7129880E9200D09B09 /* LogsHandlerInterface.swift */; };
9E612C7429880F2200D09B09 /* LogsHandlerLive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E612C7329880F2200D09B09 /* LogsHandlerLive.swift */; };
9E612C7629880FC900D09B09 /* LogsHandlerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E612C7529880FC900D09B09 /* LogsHandlerTest.swift */; };
9E66122A287717A900C75B70 /* HomeCircularProgressSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E661229287717A900C75B70 /* HomeCircularProgressSnapshotTests.swift */; };
9E66122C2877188700C75B70 /* SyncStatusSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E66122B2877188700C75B70 /* SyncStatusSnapshot.swift */; };
9E6612312878337F00C75B70 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 9E6612302878337F00C75B70 /* Lottie */; };
@ -395,6 +404,11 @@
9E01F8272833CDA0000EFC57 /* ScanTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanTests.swift; sourceTree = "<group>"; };
9E02B56927FED43E005B809B /* FileManagerInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManagerInterface.swift; sourceTree = "<group>"; };
9E02B56B27FED475005B809B /* DatabaseFilesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseFilesTests.swift; sourceTree = "<group>"; };
9E0F5740297E7F1C005304FA /* TCALogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCALogger.swift; sourceTree = "<group>"; };
9E0F5742297EB96C005304FA /* TCALoggerReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCALoggerReducer.swift; sourceTree = "<group>"; };
9E0F5744297EBA1B005304FA /* LogStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogStore.swift; sourceTree = "<group>"; };
9E0F5746297EE5F3005304FA /* OSLogger_.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLogger_.swift; sourceTree = "<group>"; };
9E0F574A2980260D005304FA /* LoggerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerTests.swift; sourceTree = "<group>"; };
9E153A5B2920CD5100112F41 /* MnemonicMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MnemonicMocks.swift; sourceTree = "<group>"; };
9E153A5C2920CD5100112F41 /* MnemonicLiveKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MnemonicLiveKey.swift; sourceTree = "<group>"; };
9E153A5D2920CD5100112F41 /* MnemonicTestKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MnemonicTestKey.swift; sourceTree = "<group>"; };
@ -434,6 +448,10 @@
9E5BF647282277BE00BA3F17 /* NotificationCenterInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationCenterInterface.swift; sourceTree = "<group>"; };
9E5BF64D2823E94900BA3F17 /* TransactionAddressTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionAddressTextField.swift; sourceTree = "<group>"; };
9E5BF64E2823E94900BA3F17 /* TransactionAddressTextFieldStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionAddressTextFieldStore.swift; sourceTree = "<group>"; };
9E612C6E2987A9B100D09B09 /* UIShareDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIShareDialog.swift; sourceTree = "<group>"; };
9E612C7129880E9200D09B09 /* LogsHandlerInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsHandlerInterface.swift; sourceTree = "<group>"; };
9E612C7329880F2200D09B09 /* LogsHandlerLive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsHandlerLive.swift; sourceTree = "<group>"; };
9E612C7529880FC900D09B09 /* LogsHandlerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsHandlerTest.swift; sourceTree = "<group>"; };
9E661229287717A900C75B70 /* HomeCircularProgressSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCircularProgressSnapshotTests.swift; sourceTree = "<group>"; };
9E66122B2877188700C75B70 /* SyncStatusSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncStatusSnapshot.swift; sourceTree = "<group>"; };
9E6612322878338C00C75B70 /* LottieAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LottieAnimation.swift; sourceTree = "<group>"; };
@ -962,6 +980,17 @@
path = ScanTests;
sourceTree = "<group>";
};
9E0F573F297E7F00005304FA /* Logging */ = {
isa = PBXGroup;
children = (
9E0F5740297E7F1C005304FA /* TCALogger.swift */,
9E0F5742297EB96C005304FA /* TCALoggerReducer.swift */,
9E0F5744297EBA1B005304FA /* LogStore.swift */,
9E0F5746297EE5F3005304FA /* OSLogger_.swift */,
);
path = Logging;
sourceTree = "<group>";
};
9E153A5A2920CCE700112F41 /* Mnemonic */ = {
isa = PBXGroup;
children = (
@ -1109,6 +1138,24 @@
path = TransactionAddress;
sourceTree = "<group>";
};
9E612C6D2987A96500D09B09 /* UIKitBridge */ = {
isa = PBXGroup;
children = (
9E612C6E2987A9B100D09B09 /* UIShareDialog.swift */,
);
path = UIKitBridge;
sourceTree = "<group>";
};
9E612C7029880E6700D09B09 /* LogsHandler */ = {
isa = PBXGroup;
children = (
9E612C7129880E9200D09B09 /* LogsHandlerInterface.swift */,
9E612C7329880F2200D09B09 /* LogsHandlerLive.swift */,
9E612C7529880FC900D09B09 /* LogsHandlerTest.swift */,
);
path = LogsHandler;
sourceTree = "<group>";
};
9E6612342878341F00C75B70 /* Lotties */ = {
isa = PBXGroup;
children = (
@ -1226,6 +1273,7 @@
9E7FE0BB282D1DC200C374E8 /* Utils */ = {
isa = PBXGroup;
children = (
9E0F573F297E7F00005304FA /* Logging */,
9E7FE0D4282D281800C374E8 /* Array+Chunked.swift */,
F9C165B3274031F600592F76 /* Bindings.swift */,
0DACFA7E27208CE00039EEA5 /* Clamped.swift */,
@ -1260,6 +1308,7 @@
9EB863882922CC0E003D0F8B /* FeedbackGenerator */,
9EB863B52923C4ED003D0F8B /* FileManager */,
9EBDF981291F91B1000A1A05 /* LocalAuthentication */,
9E612C7029880E6700D09B09 /* LogsHandler */,
9E153A5A2920CCE700112F41 /* Mnemonic */,
9EB863B42923C490003D0F8B /* NotificationCenter */,
9EB8638F2922D000003D0F8B /* NumberFormatter */,
@ -1650,6 +1699,7 @@
9E02B56B27FED475005B809B /* DatabaseFilesTests.swift */,
9E39112D283F91600073DD9A /* ZatoshiTests.swift */,
0DB4E0B02881F2DB00947B78 /* WalletBalance+testing.swift */,
9E0F574A2980260D005304FA /* LoggerTests.swift */,
);
path = UtilTests;
sourceTree = "<group>";
@ -1726,6 +1776,7 @@
children = (
F9971A6227680DFE00A2DB75 /* SettingsStore.swift */,
F9971A6427680DFE00A2DB75 /* SettingsView.swift */,
9E612C6D2987A96500D09B09 /* UIKitBridge */,
);
path = Settings;
sourceTree = "<group>";
@ -1996,6 +2047,7 @@
2EB7758727FC67FD00269373 /* TransactionAmountTextFieldStore.swift in Sources */,
669FDAE9272C23B3007B9422 /* CircularFrame.swift in Sources */,
9EF8136027F043CC0075AF48 /* AppDelegate.swift in Sources */,
9E612C7229880E9200D09B09 /* LogsHandlerInterface.swift in Sources */,
9EBDF960291E657B000A1A05 /* DeeplinkTestKey.swift in Sources */,
34E0AF1128DEE5220034CF37 /* Wedge.swift in Sources */,
F96B41E8273B501F0021B49A /* TransactionDetailView.swift in Sources */,
@ -2013,9 +2065,11 @@
0DACFA7F27208CE00039EEA5 /* Clamped.swift in Sources */,
9EAB467A2861EA6A002904A0 /* TransactionRowView.swift in Sources */,
9EB8638C2922CD4A003D0F8B /* FeedbackGeneratorTestKey.swift in Sources */,
9E0F5741297E7F1D005304FA /* TCALogger.swift in Sources */,
0DFE93E3272CA1AA000FCCA5 /* RecoveryPhraseValidationFlowStore.swift in Sources */,
9E2DF99E27CF704D00649636 /* ImportWalletView.swift in Sources */,
9E9ADA7D2938F4C00071767B /* RootInitialization.swift in Sources */,
9E612C7429880F2200D09B09 /* LogsHandlerLive.swift in Sources */,
9EBDF967291ECDA2000A1A05 /* AudioServicesTestKey.swift in Sources */,
0D535FE2271F9476009A9E3E /* EnumeratedChip.swift in Sources */,
9EBDF97E291F7EB0000A1A05 /* AppVersionInterface.swift in Sources */,
@ -2035,6 +2089,7 @@
9E2DF99D27CF704D00649636 /* ImportSeedEditor.swift in Sources */,
F9971A5327680DD000A2DB75 /* ProfileStore.swift in Sources */,
346D41E428DF0B8600963F36 /* CheckCircle.swift in Sources */,
9E0F5745297EBA1B005304FA /* LogStore.swift in Sources */,
9EB863AA29239EB2003D0F8B /* RecoveryPhraseRandomizer.swift in Sources */,
9EB863C52923C8AF003D0F8B /* FileManagerTest.swift in Sources */,
9EB863BF2923C72C003D0F8B /* SecItemLive.swift in Sources */,
@ -2053,6 +2108,7 @@
0D18581B272728D60046B928 /* PhraseChip.swift in Sources */,
9E7FE0F92832824C00C374E8 /* QRCodeScanView.swift in Sources */,
9E153A6F292167FF00112F41 /* ZcashSDKEnvironmentTestKey.swift in Sources */,
9E0F5743297EB96C005304FA /* TCALoggerReducer.swift in Sources */,
0DF482BA2787ADA800EB37D6 /* ConditionalModifier.swift in Sources */,
9E7225F3288AB6DD00DF7F17 /* MultipleLineTextField.swift in Sources */,
3448CB3228E47666006ADEDB /* NotEnoughFreeSpaceView.swift in Sources */,
@ -2110,6 +2166,7 @@
66DC733F271D88CC0053CBB6 /* StandardButtonStyle.swift in Sources */,
663FABA0271D876200E495F8 /* PrimaryButton.swift in Sources */,
663FAB9C271D874D00E495F8 /* ActiveButton.swift in Sources */,
9E612C6F2987A9B100D09B09 /* UIShareDialog.swift in Sources */,
9EBDF956291E5E86000A1A05 /* DatabaseFilesLiveKey.swift in Sources */,
9E2F1C842809B606004E65FE /* DebugMenu.swift in Sources */,
9E153A5F2920CE2700112F41 /* MnemonicInterface.swift in Sources */,
@ -2148,6 +2205,7 @@
F9971A6627680DFE00A2DB75 /* SettingsView.swift in Sources */,
F96B41EB273B50520021B49A /* Strings.swift in Sources */,
9EB863D02923D3FC003D0F8B /* SDKSynchronizerMocks.swift in Sources */,
9E612C7629880FC900D09B09 /* LogsHandlerTest.swift in Sources */,
2EDA07A227EDE1AE00D6F09B /* TextFieldFooter.swift in Sources */,
F9971A5427680DD000A2DB75 /* ProfileView.swift in Sources */,
F9971A6027680DF600A2DB75 /* ScanStore.swift in Sources */,
@ -2174,6 +2232,7 @@
34E5F2F328E46DB700C17E5F /* DiskSpaceChecker.swift in Sources */,
F9C165C42740403600592F76 /* TransactionSentView.swift in Sources */,
9E153A6E292167FF00112F41 /* ZcashSDKEnvironmentLiveKey.swift in Sources */,
9E0F5747297EE5F3005304FA /* OSLogger_.swift in Sources */,
F9971A5927680DDE00A2DB75 /* RequestStore.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -2205,6 +2264,7 @@
9E5BF644281FEC9900BA3F17 /* SendTests.swift in Sources */,
0D1C1AA327611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift in Sources */,
9E9ECC9A28589E150099D5A2 /* RecoveryPhraseValidationFlowSnapshotTests.swift in Sources */,
9E0F574B2980260D005304FA /* LoggerTests.swift in Sources */,
9EAFEB822805793200199FC9 /* RootTests.swift in Sources */,
9E391132284644580073DD9A /* AppInitializationTests.swift in Sources */,
9E9ECC9928589E150099D5A2 /* RecoveryPhraseDisplaySnapshotTests.swift in Sources */,

View File

@ -0,0 +1,19 @@
//
// LogsHandlerInterface.swift
// secant-testnet
//
// Created by Lukáš Korba on 30.01.2023.
//
import Foundation
import ComposableArchitecture
extension DependencyValues {
var logsHandler: LogsHandlerClient {
get { self[LogsHandlerClient.self] }
set { self[LogsHandlerClient.self] = newValue }
}
}
struct LogsHandlerClient {
let exportAndStoreLogs: (URL, URL, URL) async throws -> Void
}

View File

@ -0,0 +1,39 @@
//
// LogsHandlerLive.swift
// secant-testnet
//
// Created by Lukáš Korba on 30.01.2023.
//
import Foundation
import ComposableArchitecture
extension LogsHandlerClient: DependencyKey {
static let liveValue = LogsHandlerClient(
exportAndStoreLogs: { tempSDKDir, tempTCADir, tempWalletDir in
async let sdkLogs = LogsHandlerClient.exportAndStoreLogsFor(key: LoggerConstants.sdkLogs, atURL: tempSDKDir)
async let tcaLogs = LogsHandlerClient.exportAndStoreLogsFor(key: LoggerConstants.tcaLogs, atURL: tempTCADir)
async let walletLogs = LogsHandlerClient.exportAndStoreLogsFor(key: LoggerConstants.walletLogs, atURL: tempWalletDir)
let logs = try await [sdkLogs, tcaLogs, walletLogs]
try logs.forEach { logsHandler in
try logsHandler.result.write(to: logsHandler.dir, atomically: true, encoding: String.Encoding.utf8)
}
}
)
}
private extension LogsHandlerClient {
static func exportAndStoreLogsFor(key: String, atURL: URL) async throws -> (result: String, dir: URL) {
let logsStr = try await LogStore.exportCategory(key)
var result = ""
logsStr?.forEach({ line in
result.append(line)
result.append("\n\n")
})
return (result: result, dir: atURL)
}
}

View File

@ -0,0 +1,15 @@
//
// LogsHandlerTest.swift
// secant-testnet
//
// Created by Lukáš Korba on 30.01.2023.
//
import ComposableArchitecture
import XCTestDynamicOverlay
extension LogsHandlerClient: TestDependencyKey {
static let testValue = Self(
exportAndStoreLogs: XCTUnimplemented("\(Self.self).exportAndStoreLogs")
)
}

View File

@ -25,6 +25,8 @@ extension RootReducer {
Reduce { state, action in
switch action {
case .initialization(.appDelegate(.didFinishLaunching)):
// TODO: [#524] finish all the wallet events according to definition, https://github.com/zcash/secant-ios-wallet/issues/524
LoggerProxy.event(".appDelegate(.didFinishLaunching)")
/// We need to fetch data from keychain, in order to be 100% sure the kecyhain can be read we delay the check a bit
return Effect(value: .initialization(.checkWalletInitialization))
.delay(for: 0.02, scheduler: mainQueue)

View File

@ -139,7 +139,8 @@ extension RootReducer {
spendParamsURL: try databaseFiles.spendParamsURLFor(network),
outputParamsURL: try databaseFiles.outputParamsURLFor(network),
viewingKeys: [viewingKey],
walletBirthday: birthday
walletBirthday: birthday,
loggerProxy: OSLogger_(logLevel: .debug, category: LoggerConstants.sdkLogs)
)
return initializer
@ -173,7 +174,7 @@ extension RootStore {
static var placeholder: RootStore {
RootStore(
initialState: .placeholder,
reducer: RootReducer()._printChanges()
reducer: RootReducer().logging()._printChanges()
)
}
}

View File

@ -10,25 +10,49 @@ struct SettingsReducer: ReducerProtocol {
case backupPhrase
}
var destination: Destination?
var exportLogsDisabled = false
var isSharingLogs = false
var phraseDisplayState: RecoveryPhraseDisplayReducer.State
var rescanDialog: ConfirmationDialogState<SettingsReducer.Action>?
var destination: Destination?
var tempSDKDir: URL {
let tempDir = FileManager.default.temporaryDirectory
let sdkFileName = "sdkLogs.txt"
return tempDir.appendingPathComponent(sdkFileName)
}
var tempTCADir: URL {
let tempDir = FileManager.default.temporaryDirectory
let sdkFileName = "tcaLogs.txt"
return tempDir.appendingPathComponent(sdkFileName)
}
var tempWalletDir: URL {
let tempDir = FileManager.default.temporaryDirectory
let sdkFileName = "walletLogs.txt"
return tempDir.appendingPathComponent(sdkFileName)
}
}
enum Action: Equatable {
case backupWallet
case backupWalletAccessRequest
case cancelRescan
case exportLogs
case fullRescan
case logsExported
case logsShareFinished
case phraseDisplay(RecoveryPhraseDisplayReducer.Action)
case quickRescan
case rescanBlockchain
case updateDestination(SettingsReducer.State.Destination?)
}
@Dependency(\.localAuthentication) var localAuthentication
@Dependency(\.mnemonic) var mnemonic
@Dependency(\.sdkSynchronizer) var sdkSynchronizer
@Dependency(\.logsHandler) var logsHandler
@Dependency(\.walletStorage) var walletStorage
var body: some ReducerProtocol<State, Action> {
@ -57,6 +81,26 @@ struct SettingsReducer: ReducerProtocol {
state.rescanDialog = nil
return .none
case .exportLogs:
state.exportLogsDisabled = true
return .run { [state] send in
do {
try await logsHandler.exportAndStoreLogs(state.tempSDKDir, state.tempTCADir, state.tempWalletDir)
await send(.logsExported)
} catch {
// TODO: [#527] address the error here https://github.com/zcash/secant-ios-wallet/issues/527
}
}
case .logsExported:
state.exportLogsDisabled = false
state.isSharingLogs = true
return .none
case .logsShareFinished:
state.isSharingLogs = false
return .none
case .rescanBlockchain:
state.rescanDialog = .init(
title: TextState("Rescan"),
@ -94,7 +138,7 @@ extension SettingsViewStore {
send: SettingsReducer.Action.updateDestination
)
}
var bindingForBackupPhrase: Binding<Bool> {
self.destinationBinding.map(
extract: { $0 == .backupPhrase },

View File

@ -3,7 +3,7 @@ import ComposableArchitecture
struct SettingsView: View {
let store: SettingsStore
var body: some View {
WithViewStore(store) { viewStore in
VStack {
@ -22,7 +22,26 @@ struct SettingsView: View {
.primaryButtonStyle
.frame(height: 50)
.padding(.horizontal, 30)
Button(
action: { viewStore.send(.exportLogs) },
label: {
if viewStore.exportLogsDisabled {
HStack {
ProgressView()
Text("Exporting...")
}
} else {
Text("Export & share logs")
}
}
)
.primaryButtonStyle
.frame(height: 50)
.padding(.horizontal, 30)
.padding(.top, 30)
.disabled(viewStore.exportLogsDisabled)
Spacer()
}
.navigationTitle("Settings")
@ -37,6 +56,17 @@ struct SettingsView: View {
RecoveryPhraseDisplayView(store: store.backupPhraseStore())
}
)
if viewStore.isSharingLogs {
UIShareDialogView(
activityItems: [viewStore.tempSDKDir, viewStore.tempWalletDir, viewStore.tempTCADir]
) {
viewStore.send(.logsShareFinished)
}
// UIShareDialogView only wraps UIActivityViewController presentation
// so frame is set to 0 to not break SwiftUIs layout
.frame(width: 0, height: 0)
}
}
}
}

View File

@ -0,0 +1,54 @@
//
// UIShareDialog.swift
// secant-testnet
//
// Created by Lukáš Korba on 30.01.2023.
//
import Foundation
import UIKit
import SwiftUI
public class UIShareDialog: UIView {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
super.init(frame: frame)
}
}
extension UIShareDialog {
public func doInitialSetup(activityItems: [Any], completion: @escaping () -> Void) {
DispatchQueue.main.async {
let activityVC = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
UIApplication.shared.connectedScenes.map({ $0 as? UIWindowScene })
.compactMap({ $0 })
.first?.windows.first?.rootViewController?.present(
activityVC,
animated: true,
completion: completion
)
}
}
}
struct UIShareDialogView: UIViewRepresentable {
let activityItems: [Any]
let completion: () -> Void
public func makeUIView(context: UIViewRepresentableContext<UIShareDialogView>) -> UIShareDialog {
let view = UIShareDialog()
view.doInitialSetup(activityItems: activityItems, completion: completion)
return view
}
public func updateUIView(_ uiView: UIShareDialog, context: UIViewRepresentableContext<UIShareDialogView>) {
// We can leave it empty here because the view is just handler how to bridge UIKit's UIActivityViewController
// presentation into SwiftUI. The view itself is not visible, only instantiated, therefore no updates needed.
}
public typealias UIViewType = UIShareDialog
}

View File

@ -20,6 +20,7 @@ final class AppDelegate: NSObject, UIApplicationDelegate {
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
walletLogger = OSLogger_(logLevel: .debug, category: LoggerConstants.walletLogs)
// set the default behavior for the NSDecimalNumber
NSDecimalNumber.defaultBehavior = Zatoshi.decimalHandler
rootViewStore.send(.initialization(.appDelegate(.didFinishLaunching)))

View File

@ -8,12 +8,25 @@
import Foundation
extension Date {
static let timestampFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy/MM/dd HH:mm:ss.SSSS"
return formatter
}()
static let humanReadableFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .short
return formatter
}()
func timestamp() -> String {
return String(format: "%@", Date.timestampFormatter.string(from: self))
}
func asHumanReadable() -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .short
dateFormatter.timeStyle = .short
return dateFormatter.string(from: self)
return Date.humanReadableFormatter.string(from: self)
}
}

View File

@ -0,0 +1,38 @@
//
// LogStore.swift
// secant-testnet
//
// Created by Lukáš Korba on 23.01.2023.
//
import Foundation
import OSLog
enum LogStore {
static func exportCategory(
_ category: String,
hoursToThePast: TimeInterval = 168,
fileSize: Int = 1_000_000
) async throws -> [String]? {
guard let bundle = Bundle.main.bundleIdentifier else { return nil }
let store = try OSLogStore(scope: .currentProcessIdentifier)
let date = Date.now.addingTimeInterval(-hoursToThePast * 3600)
let position = store.position(date: date)
var res: [String] = []
var size = 0
let entries = try store.getEntries(at: position).reversed()
for entry in entries {
guard let logEntry = entry as? OSLogEntryLog else { continue }
guard logEntry.subsystem == bundle && logEntry.category == category else { continue }
guard size < fileSize else { break }
size += logEntry.composedMessage.utf8.count
res.append("[\(logEntry.date.timestamp())] \(logEntry.composedMessage)")
}
return res
}
}

View File

@ -0,0 +1,135 @@
//
// OSLogger_.swift
// secant-testnet
//
// Created by Lukáš Korba on 23.01.2023.
//
import Foundation
import ZcashLightClientKit
import os
enum LoggerConstants {
static let sdkLogs = "sdkLogs"
static let tcaLogs = "tcaLogs"
static let walletLogs = "walletLogs"
}
var walletLogger: ZcashLightClientKit.Logger?
enum LoggerProxy {
static func debug(_ message: String, file: StaticString = #file, function: StaticString = #function, line: Int = #line) {
walletLogger?.debug(message, file: file, function: function, line: line)
}
static func info(_ message: String, file: StaticString = #file, function: StaticString = #function, line: Int = #line) {
walletLogger?.info(message, file: file, function: function, line: line)
}
static func event(_ message: String, file: StaticString = #file, function: StaticString = #function, line: Int = #line) {
walletLogger?.event(message, file: file, function: function, line: line)
}
static func warn(_ message: String, file: StaticString = #file, function: StaticString = #function, line: Int = #line) {
walletLogger?.warn(message, file: file, function: function, line: line)
}
static func error(_ message: String, file: StaticString = #file, function: StaticString = #function, line: Int = #line) {
walletLogger?.error(message, file: file, function: function, line: line)
}
}
// TODO: [#529] the swiftlint rule as well as OSLogger_ will be removed once secant adopts latest SDK changes https://github.com/zcash/secant-ios-wallet/issues/529
// swiftlint:disable:next type_name
class OSLogger_: ZcashLightClientKit.Logger {
enum LogLevel: Int {
case debug
case error
case warning
case event
case info
}
private(set) var oslog: OSLog?
var level: LogLevel
init(logLevel: LogLevel, category: String) {
self.level = logLevel
if let bundleName = Bundle.main.bundleIdentifier {
self.oslog = OSLog(subsystem: bundleName, category: category)
}
}
func debug(
_ message: String,
file: StaticString = #file,
function: StaticString = #function,
line: Int = #line
) {
guard level.rawValue == LogLevel.debug.rawValue else { return }
log(level: "DEBUG 🐞", message: message, file: file, function: function, line: line)
}
func error(
_ message: String,
file: StaticString = #file,
function: StaticString = #function,
line: Int = #line
) {
guard level.rawValue <= LogLevel.error.rawValue else { return }
log(level: "ERROR 💥", message: message, file: file, function: function, line: line)
}
func warn(
_ message: String,
file: StaticString = #file,
function: StaticString = #function,
line: Int = #line
) {
guard level.rawValue <= LogLevel.warning.rawValue else { return }
log(level: "WARNING ⚠️", message: message, file: file, function: function, line: line)
}
func event(
_ message: String,
file: StaticString = #file,
function: StaticString = #function,
line: Int = #line
) {
guard level.rawValue <= LogLevel.event.rawValue else { return }
log(level: "EVENT ⏱", message: message, file: file, function: function, line: line)
}
func info(
_ message: String,
file: StaticString = #file,
function: StaticString = #function,
line: Int = #line
) {
guard level.rawValue <= LogLevel.info.rawValue else { return }
log(level: "INFO ", message: message, file: file, function: function, line: line)
}
private func log(
level: String,
message: String,
file: StaticString = #file,
function: StaticString = #function,
line: Int = #line
) {
guard let oslog else { return }
let fileName = (String(describing: file) as NSString).lastPathComponent
os_log(
"[%{public}@] %{public}@ - %{public}@ - Line: %{public}d -> %{public}@",
log: oslog,
level,
fileName,
String(describing: function),
line,
message
)
}
}

View File

@ -0,0 +1,25 @@
//
// TCALogger.swift
// secant-testnet
//
// Created by Lukáš Korba on 23.01.2023.
//
import Foundation
import os
class TCALogger: OSLogger_ { }
extension TCALogger {
static let live = TCALogger(logLevel: .debug, category: LoggerConstants.tcaLogs)
func tcaDebug(_ message: String) {
guard let oslog else { return }
os_log(
"%{public}@",
log: oslog,
message
)
}
}

View File

@ -0,0 +1,73 @@
//
// TCALoggerReducer.swift
// secant-testnet
//
// Created by Lukáš Korba on 23.01.2023.
//
import ComposableArchitecture
extension ReducerProtocol {
@inlinable
public func logging(
_ logger: ReducerLogger<State, Action>? = .tcaLogger
) -> LogChangesReducer<Self> {
LogChangesReducer<Self>(base: self, logger: logger)
}
}
public struct ReducerLogger<State, Action> {
private let _logChange: (_ receivedAction: Action, _ oldState: State, _ newState: State) -> Void
public init(
logChange: @escaping (_ receivedAction: Action, _ oldState: State, _ newState: State) -> Void
) {
self._logChange = logChange
}
public func logChange(receivedAction: Action, oldState: State, newState: State) {
self._logChange(receivedAction, oldState, newState)
}
}
extension ReducerLogger {
public static var tcaLogger: Self {
Self { receivedAction, oldState, newState in
var target = ""
target.write("received action:\n")
CustomDump.customDump(receivedAction, to: &target, indent: 2)
target.write("\n")
target.write(diff(oldState, newState).map { "\($0)\n" } ?? " (No state changes)\n")
TCALogger.live.tcaDebug("\(target)")
}
}
}
public struct LogChangesReducer<Base: ReducerProtocol>: ReducerProtocol {
@usableFromInline let base: Base
@usableFromInline let logger: ReducerLogger<Base.State, Base.Action>?
@usableFromInline
init(base: Base, logger: ReducerLogger<Base.State, Base.Action>?) {
self.base = base
self.logger = logger
}
@inlinable
public func reduce(
into state: inout Base.State, action: Base.Action
) -> EffectTask<Base.Action> {
guard let logger else {
return self.base.reduce(into: &state, action: action)
}
let oldState = state
let effects = self.base.reduce(into: &state, action: action)
return effects.merge(
with: .fireAndForget { [newState = state] in
logger.logChange(receivedAction: action, oldState: oldState, newState: newState)
}
)
}
}

View File

@ -99,6 +99,7 @@ class SettingsTests: XCTestCase {
func testRescanBlockchain_Cancelling() async throws {
let store = TestStore(
initialState: SettingsReducer.State(
destination: nil,
phraseDisplayState: .init(),
rescanDialog: .init(
title: TextState("Rescan"),
@ -108,8 +109,7 @@ class SettingsTests: XCTestCase {
.default(TextState("Full rescan"), action: .send(.fullRescan)),
.cancel(TextState("Cancel"))
]
),
destination: nil
)
),
reducer: SettingsReducer()
)
@ -122,6 +122,7 @@ class SettingsTests: XCTestCase {
func testRescanBlockchain_QuickRescanClearance() async throws {
let store = TestStore(
initialState: SettingsReducer.State(
destination: nil,
phraseDisplayState: .init(),
rescanDialog: .init(
title: TextState("Rescan"),
@ -131,8 +132,7 @@ class SettingsTests: XCTestCase {
.default(TextState("Full rescan"), action: .send(.fullRescan)),
.cancel(TextState("Cancel"))
]
),
destination: nil
)
),
reducer: SettingsReducer()
)
@ -145,6 +145,7 @@ class SettingsTests: XCTestCase {
func testRescanBlockchain_FullRescanClearance() async throws {
let store = TestStore(
initialState: SettingsReducer.State(
destination: nil,
phraseDisplayState: .init(),
rescanDialog: .init(
title: TextState("Rescan"),
@ -154,8 +155,7 @@ class SettingsTests: XCTestCase {
.default(TextState("Full rescan"), action: .send(.fullRescan)),
.cancel(TextState("Cancel"))
]
),
destination: nil
)
),
reducer: SettingsReducer()
)
@ -164,4 +164,58 @@ class SettingsTests: XCTestCase {
state.rescanDialog = nil
}
}
func testExportLogs_ButtonDisableShareEnable() async throws {
let store = TestStore(
initialState: SettingsReducer.State(
destination: nil,
phraseDisplayState: .init(),
rescanDialog: .init(
title: TextState("Rescan"),
message: TextState("Select the rescan you want"),
buttons: [
.default(TextState("Quick rescan"), action: .send(.quickRescan)),
.default(TextState("Full rescan"), action: .send(.fullRescan)),
.cancel(TextState("Cancel"))
]
)
),
reducer: SettingsReducer()
)
store.dependencies.logsHandler = LogsHandlerClient(exportAndStoreLogs: { _, _, _ in })
_ = await store.send(.exportLogs) { state in
state.exportLogsDisabled = true
}
await store.receive(.logsExported) { state in
state.exportLogsDisabled = false
state.isSharingLogs = true
}
}
func testLogShareFinished() async throws {
let store = TestStore(
initialState: SettingsReducer.State(
destination: nil,
isSharingLogs: true,
phraseDisplayState: .init(),
rescanDialog: .init(
title: TextState("Rescan"),
message: TextState("Select the rescan you want"),
buttons: [
.default(TextState("Quick rescan"), action: .send(.quickRescan)),
.default(TextState("Full rescan"), action: .send(.fullRescan)),
.cancel(TextState("Cancel"))
]
)
),
reducer: SettingsReducer()
)
_ = await store.send(.logsShareFinished) { state in
state.isSharingLogs = false
}
}
}

View File

@ -0,0 +1,270 @@
//
// LoggerTests.swift
// secantTests
//
// Created by Lukáš Korba on 24.01.2023.
//
import XCTest
@testable import secant_testnet
import OSLog
class LoggerTests: XCTestCase {
func testOSLogger_loggingAndExport() throws {
let category = "testOSLogger_loggingAndExport"
let osLogger = OSLogger_(logLevel: .debug, category: category)
let testMessage = "test message"
osLogger.debug(testMessage)
let logs = TestLogStore.exportCategory(category)
XCTAssertNotNil(logs)
guard let logs else { return }
XCTAssertEqual(logs.count, 1)
let loggedMessage = logs[0].osLoggedMessage()
XCTAssertEqual(testMessage, loggedMessage)
}
func testOSLogger_DebugLevel_DebugLog() throws {
let category = "testOSLogger_DebugLevel_DebugLog"
let osLogger = OSLogger_(logLevel: .debug, category: category)
let testMessage = "debug message"
osLogger.debug(testMessage)
let logs = TestLogStore.exportCategory(category)
XCTAssertNotNil(logs)
guard let logs else { return }
XCTAssertEqual(logs.count, 1)
let loggedMessage = logs[0].osLoggedMessage()
XCTAssertEqual(testMessage, loggedMessage)
}
func testOSLogger_ErrorLevel_ErrorLog() throws {
let category = "testOSLogger_ErrorLevel_ErrorLog"
let osLogger = OSLogger_(logLevel: .debug, category: category)
let testMessage = "error message"
osLogger.error(testMessage)
let logs = TestLogStore.exportCategory(category)
XCTAssertNotNil(logs)
guard let logs else { return }
XCTAssertEqual(logs.count, 1)
let loggedMessage = logs[0].osLoggedMessage()
XCTAssertEqual(testMessage, loggedMessage)
}
func testOSLogger_WarningLevel_WarningLog() throws {
let category = "testOSLogger_WarningLevel_WarningLog"
let osLogger = OSLogger_(logLevel: .warning, category: category)
let testMessage = "warning message"
osLogger.warn(testMessage)
let logs = TestLogStore.exportCategory(category)
XCTAssertNotNil(logs)
guard let logs else { return }
XCTAssertEqual(logs.count, 1)
let loggedMessage = logs[0].osLoggedMessage()
XCTAssertEqual(testMessage, loggedMessage)
}
func testOSLogger_EventLevel_EventLog() throws {
let category = "testOSLogger_EventLevel_EventLog"
let osLogger = OSLogger_(logLevel: .event, category: category)
let testMessage = "event message"
osLogger.event(testMessage)
let logs = TestLogStore.exportCategory(category)
XCTAssertNotNil(logs)
guard let logs else { return }
XCTAssertEqual(logs.count, 1)
let loggedMessage = logs[0].osLoggedMessage()
XCTAssertEqual(testMessage, loggedMessage)
}
func testOSLogger_InfoLevel_InfoLog() throws {
let category = "testOSLogger_InfoLevel_InfoLog"
let osLogger = OSLogger_(logLevel: .info, category: category)
let testMessage = "info message"
osLogger.info(testMessage)
let logs = TestLogStore.exportCategory(category)
XCTAssertNotNil(logs)
guard let logs else { return }
XCTAssertEqual(logs.count, 1)
let loggedMessage = logs[0].osLoggedMessage()
XCTAssertEqual(testMessage, loggedMessage)
}
func testOSLogger_DebugLevel_OtherLogs() throws {
let category = "testOSLogger_DebugLevel_OtherLogs"
let osLogger = OSLogger_(logLevel: .debug, category: category)
let testMessage = "debug message"
osLogger.debug(testMessage)
osLogger.error(testMessage)
osLogger.warn(testMessage)
osLogger.event(testMessage)
osLogger.info(testMessage)
let logs = TestLogStore.exportCategory(category)
guard let logs else { return }
XCTAssertEqual(logs.count, 5)
}
func testOSLogger_ErrorLevel_OtherLogs() throws {
let category = "testOSLogger_ErrorLevel_OtherLogs"
let osLogger = OSLogger_(logLevel: .error, category: category)
let testMessage = "debug message"
osLogger.debug(testMessage)
osLogger.error(testMessage)
osLogger.warn(testMessage)
osLogger.event(testMessage)
osLogger.info(testMessage)
let logs = TestLogStore.exportCategory(category)
guard let logs else { return }
XCTAssertEqual(logs.count, 4)
}
func testOSLogger_WarningLevel_OtherLogs() throws {
let category = "testOSLogger_WarningLevel_OtherLogs"
let osLogger = OSLogger_(logLevel: .warning, category: category)
let testMessage = "debug message"
osLogger.debug(testMessage)
osLogger.error(testMessage)
osLogger.warn(testMessage)
osLogger.event(testMessage)
osLogger.info(testMessage)
let logs = TestLogStore.exportCategory(category)
guard let logs else { return }
XCTAssertEqual(logs.count, 3)
}
func testOSLogger_EventLevel_OtherLogs() throws {
let category = "testOSLogger_EventLevel_OtherLogs"
let osLogger = OSLogger_(logLevel: .event, category: category)
let testMessage = "debug message"
osLogger.debug(testMessage)
osLogger.error(testMessage)
osLogger.warn(testMessage)
osLogger.event(testMessage)
osLogger.info(testMessage)
let logs = TestLogStore.exportCategory(category)
guard let logs else { return }
XCTAssertEqual(logs.count, 2)
}
func testOSLogger_InfoLevel_OtherLogs() throws {
let category = "testOSLogger_InfoLevel_OtherLogs"
let osLogger = OSLogger_(logLevel: .info, category: category)
let testMessage = "debug message"
osLogger.debug(testMessage)
osLogger.error(testMessage)
osLogger.warn(testMessage)
osLogger.event(testMessage)
osLogger.info(testMessage)
let logs = TestLogStore.exportCategory(category)
guard let logs else { return }
XCTAssertEqual(logs.count, 1)
}
func testWalletLogger() throws {
let category = "testWalletLogger"
walletLogger = OSLogger_(logLevel: .info, category: category)
let testMessage = "wallet test message"
LoggerProxy.info(testMessage)
let logs = TestLogStore.exportCategory(category)
XCTAssertNotNil(logs)
guard let logs else { return }
XCTAssertEqual(logs.count, 1)
let loggedMessage = logs[0].osLoggedMessage()
XCTAssertEqual(testMessage, loggedMessage)
}
}
extension String {
func osLoggedMessage() -> String? {
let split = components(separatedBy: "-> ")
if split.count == 2 {
return split[1]
}
return nil
}
}
enum TestLogStore {
static func exportCategory(_ category: String, hoursToThePast: TimeInterval = 24) -> [String]? {
guard let bundle = Bundle.main.bundleIdentifier else { return nil }
do {
let store = try OSLogStore(scope: .currentProcessIdentifier)
let date = Date.now.addingTimeInterval(-hoursToThePast * 3600)
let position = store.position(date: date)
var entries: [String] = []
entries = try store
.getEntries(at: position)
.compactMap { $0 as? OSLogEntryLog }
.filter { $0.subsystem == bundle && $0.category == category }
.map { "[\($0.date.formatted())] \($0.composedMessage)" }
return entries
} catch {
return nil
}
}
}