[#265] Integrate App Rating Alert (#703)

- async adaptations of the latest sdk
- review request TCA dependency added
- set of rules for the triggering the app rating dialog
- unit tests fixed
- unit tests for the review request client
- app review request logic disconnected from the production for now (added the TODO for the triggering)
This commit is contained in:
Lukas Korba 2023-04-06 08:06:49 +02:00 committed by GitHub
parent 36d2090654
commit a727f49817
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 703 additions and 213 deletions

View File

@ -363,6 +363,9 @@
9E153A7529216EFB00112F41 /* UserDefaultsLiveKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E153A7229216EFB00112F41 /* UserDefaultsLiveKey.swift */; };
9E153A7629216EFB00112F41 /* UserDefaultsInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E153A7329216EFB00112F41 /* UserDefaultsInterface.swift */; };
9E153A7729216EFB00112F41 /* UserDefaultsTestKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E153A7429216EFB00112F41 /* UserDefaultsTestKey.swift */; };
9E2A07B729DAE0A900F2B086 /* ReviewRequestTestKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2A07B429DAE0A900F2B086 /* ReviewRequestTestKey.swift */; };
9E2A07B829DAE0A900F2B086 /* ReviewRequestLiveKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2A07B529DAE0A900F2B086 /* ReviewRequestLiveKey.swift */; };
9E2A07B929DAE0A900F2B086 /* ReviewRequestInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2A07B629DAE0A900F2B086 /* ReviewRequestInterface.swift */; };
9E2AC0FF27D8EC120042AA47 /* MnemonicSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 9E2AC0FE27D8EC120042AA47 /* MnemonicSwift */; };
9E2DF99C27CF704D00649636 /* ImportWalletStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2DF99827CF704D00649636 /* ImportWalletStore.swift */; };
9E2DF99D27CF704D00649636 /* ImportSeedEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2DF99A27CF704D00649636 /* ImportSeedEditor.swift */; };
@ -456,6 +459,16 @@
9E69A24D27FB002800A55317 /* WelcomeStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E69A24C27FB002800A55317 /* WelcomeStore.swift */; };
9E7225F3288AB6DD00DF7F17 /* MultipleLineTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7225F2288AB6DD00DF7F17 /* MultipleLineTextField.swift */; };
9E7225F6288AC71A00DF7F17 /* MultiLineTextFieldStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7225F5288AC71A00DF7F17 /* MultiLineTextFieldStore.swift */; };
9E74CCC529DC04E8003D6E32 /* DateTestKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E74CCC229DC04E8003D6E32 /* DateTestKey.swift */; };
9E74CCC629DC04E8003D6E32 /* DateTestKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E74CCC229DC04E8003D6E32 /* DateTestKey.swift */; };
9E74CCC729DC04E8003D6E32 /* DateInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E74CCC329DC04E8003D6E32 /* DateInterface.swift */; };
9E74CCC829DC04E8003D6E32 /* DateInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E74CCC329DC04E8003D6E32 /* DateInterface.swift */; };
9E74CCC929DC04E8003D6E32 /* DateLiveKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E74CCC429DC04E8003D6E32 /* DateLiveKey.swift */; };
9E74CCCA29DC04E8003D6E32 /* DateLiveKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E74CCC429DC04E8003D6E32 /* DateLiveKey.swift */; };
9E74CCCB29DC04ED003D6E32 /* ReviewRequestInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2A07B629DAE0A900F2B086 /* ReviewRequestInterface.swift */; };
9E74CCCC29DC04ED003D6E32 /* ReviewRequestLiveKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2A07B529DAE0A900F2B086 /* ReviewRequestLiveKey.swift */; };
9E74CCCD29DC04ED003D6E32 /* ReviewRequestTestKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2A07B429DAE0A900F2B086 /* ReviewRequestTestKey.swift */; };
9E74CCD029DC0628003D6E32 /* ReviewRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E74CCCF29DC0628003D6E32 /* ReviewRequestTests.swift */; };
9E7CB6152869E8C300A02233 /* CircularProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7CB6142869E8C300A02233 /* CircularProgress.swift */; };
9E7CB61A287310EC00A02233 /* QRCodeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7CB619287310EC00A02233 /* QRCodeGenerator.swift */; };
9E7CB6202874143800A02233 /* AddressDetailsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7CB61F2874143800A02233 /* AddressDetailsStore.swift */; };
@ -713,6 +726,9 @@
9E153A7429216EFB00112F41 /* UserDefaultsTestKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsTestKey.swift; sourceTree = "<group>"; };
9E207C352966EC77003E2C9B /* AddressDetailsSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressDetailsSnapshotTests.swift; sourceTree = "<group>"; };
9E207C382966EF87003E2C9B /* AddressDetailsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressDetailsTests.swift; sourceTree = "<group>"; };
9E2A07B429DAE0A900F2B086 /* ReviewRequestTestKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReviewRequestTestKey.swift; sourceTree = "<group>"; };
9E2A07B529DAE0A900F2B086 /* ReviewRequestLiveKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReviewRequestLiveKey.swift; sourceTree = "<group>"; };
9E2A07B629DAE0A900F2B086 /* ReviewRequestInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReviewRequestInterface.swift; sourceTree = "<group>"; };
9E2DF99827CF704D00649636 /* ImportWalletStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportWalletStore.swift; sourceTree = "<group>"; };
9E2DF99A27CF704D00649636 /* ImportSeedEditor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportSeedEditor.swift; sourceTree = "<group>"; };
9E2DF99B27CF704D00649636 /* ImportWalletView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportWalletView.swift; sourceTree = "<group>"; };
@ -759,6 +775,10 @@
9E7225F02889539300DF7F17 /* SettingsSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSnapshotTests.swift; sourceTree = "<group>"; };
9E7225F2288AB6DD00DF7F17 /* MultipleLineTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipleLineTextField.swift; sourceTree = "<group>"; };
9E7225F5288AC71A00DF7F17 /* MultiLineTextFieldStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiLineTextFieldStore.swift; sourceTree = "<group>"; };
9E74CCC229DC04E8003D6E32 /* DateTestKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateTestKey.swift; sourceTree = "<group>"; };
9E74CCC329DC04E8003D6E32 /* DateInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateInterface.swift; sourceTree = "<group>"; };
9E74CCC429DC04E8003D6E32 /* DateLiveKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateLiveKey.swift; sourceTree = "<group>"; };
9E74CCCF29DC0628003D6E32 /* ReviewRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewRequestTests.swift; sourceTree = "<group>"; };
9E7CB6112869882D00A02233 /* WalletEventsSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletEventsSnapshotTests.swift; sourceTree = "<group>"; };
9E7CB6142869E8C300A02233 /* CircularProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgress.swift; sourceTree = "<group>"; };
9E7CB619287310EC00A02233 /* QRCodeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeGenerator.swift; sourceTree = "<group>"; };
@ -1030,6 +1050,7 @@
6654C7422715A48E00901167 /* OnboardingTests */,
9E7CB6222874245400A02233 /* ProfileTests */,
0DFE93E4272CB6D0000FCCA5 /* RecoveryPhraseValidationTests */,
9E74CCCE29DC060B003D6E32 /* ReviewRequestTests */,
9EAFEB802805791400199FC9 /* RootTests */,
9E01F8262833CD84000EFC57 /* ScanTests */,
9E5BF642281FEC8700BA3F17 /* SendTests */,
@ -1416,6 +1437,16 @@
path = AddressDetailsTests;
sourceTree = "<group>";
};
9E2A07B329DAE07E00F2B086 /* ReviewRequest */ = {
isa = PBXGroup;
children = (
9E2A07B629DAE0A900F2B086 /* ReviewRequestInterface.swift */,
9E2A07B529DAE0A900F2B086 /* ReviewRequestLiveKey.swift */,
9E2A07B429DAE0A900F2B086 /* ReviewRequestTestKey.swift */,
);
path = ReviewRequest;
sourceTree = "<group>";
};
9E2DF99727CF704D00649636 /* ImportWallet */ = {
isa = PBXGroup;
children = (
@ -1603,6 +1634,24 @@
path = MultiLineTextField;
sourceTree = "<group>";
};
9E74CCC129DC0476003D6E32 /* Date */ = {
isa = PBXGroup;
children = (
9E74CCC329DC04E8003D6E32 /* DateInterface.swift */,
9E74CCC429DC04E8003D6E32 /* DateLiveKey.swift */,
9E74CCC229DC04E8003D6E32 /* DateTestKey.swift */,
);
path = Date;
sourceTree = "<group>";
};
9E74CCCE29DC060B003D6E32 /* ReviewRequestTests */ = {
isa = PBXGroup;
children = (
9E74CCCF29DC0628003D6E32 /* ReviewRequestTests.swift */,
);
path = ReviewRequestTests;
sourceTree = "<group>";
};
9E7CB6102869881300A02233 /* WalletEventsSnapshotTests */ = {
isa = PBXGroup;
children = (
@ -1700,6 +1749,7 @@
9E7FE0BD282D1DE100C374E8 /* Dependencies */ = {
isa = PBXGroup;
children = (
9E74CCC129DC0476003D6E32 /* Date */,
9EBDF978291F7E85000A1A05 /* AppVersion */,
9EBDF962291ECD42000A1A05 /* AudioServices */,
9EBDF969291ECEAC000A1A05 /* CaptureDevice */,
@ -1716,6 +1766,7 @@
9EB8638F2922D000003D0F8B /* NumberFormatter */,
9E153A6329210AF800112F41 /* Pasteboard */,
9EB863A329239D95003D0F8B /* RecoveryPhraseRandomizer */,
9E2A07B329DAE07E00F2B086 /* ReviewRequest */,
9EB863B62923C539003D0F8B /* SDKSynchronizer */,
9EB863B32923C465003D0F8B /* SecItem */,
3467319729AE36F000974482 /* SupportDataGenerator */,
@ -2638,6 +2689,7 @@
0D26AEB8299E8196005260EE /* FileManagerInterface.swift in Sources */,
0D26AEBA299E8196005260EE /* AddressDetailsStore.swift in Sources */,
0D26AEBB299E8196005260EE /* RecoveryPhraseBackupSucceededView.swift in Sources */,
9E74CCCA29DC04E8003D6E32 /* DateLiveKey.swift in Sources */,
0D26AEBC299E8196005260EE /* TCATextFieldStore.swift in Sources */,
0D26AEBE299E8196005260EE /* SecantTextStyles.swift in Sources */,
0D26AEBF299E8196005260EE /* TransactionFailedView.swift in Sources */,
@ -2648,8 +2700,10 @@
0D26AEC5299E8196005260EE /* RecoveryPhraseValidationFlowStore.swift in Sources */,
9E486DFA29BA09C2003E6945 /* UIKit+Extensions.swift in Sources */,
9E33ECDA29D5E30700708DE4 /* OnChangeReducer.swift in Sources */,
9E74CCC629DC04E8003D6E32 /* DateTestKey.swift in Sources */,
0D26AEC6299E8196005260EE /* ImportWalletView.swift in Sources */,
0D26AEC7299E8196005260EE /* RootInitialization.swift in Sources */,
9E74CCCC29DC04ED003D6E32 /* ReviewRequestLiveKey.swift in Sources */,
0D26AEC8299E8196005260EE /* LogsHandlerLive.swift in Sources */,
0D26AEC9299E8196005260EE /* AudioServicesTestKey.swift in Sources */,
0D26AECA299E8196005260EE /* EnumeratedChip.swift in Sources */,
@ -2692,6 +2746,7 @@
0D26AEEB299E8196005260EE /* Previews.swift in Sources */,
0D26AEEC299E8196005260EE /* FeedbackGeneratorInterface.swift in Sources */,
0D26AEED299E8196005260EE /* PhraseChip.swift in Sources */,
9E74CCCD29DC04ED003D6E32 /* ReviewRequestTestKey.swift in Sources */,
0D26AEEE299E8196005260EE /* QRCodeScanView.swift in Sources */,
0D26AEEF299E8196005260EE /* ZcashSDKEnvironmentTestKey.swift in Sources */,
0D26AEF0299E8196005260EE /* TCALoggerReducer.swift in Sources */,
@ -2707,6 +2762,7 @@
0D26AEFA299E8196005260EE /* DesignGuide.swift in Sources */,
0D26AEFB299E8196005260EE /* SensitiveData.swift in Sources */,
0D26AEFC299E8196005260EE /* RootStore.swift in Sources */,
9E74CCCB29DC04ED003D6E32 /* ReviewRequestInterface.swift in Sources */,
0D26AEFD299E8196005260EE /* HomeView.swift in Sources */,
0D26AEFE299E8196005260EE /* NavigationLinks.swift in Sources */,
0D26AEFF299E8196005260EE /* SandboxView.swift in Sources */,
@ -2816,6 +2872,7 @@
0D26AF65299E8196005260EE /* InitializationState.swift in Sources */,
0D26AF66299E8196005260EE /* ZcashSymbol.swift in Sources */,
0D26AF67299E8196005260EE /* UserPreferencesStorageLive.swift in Sources */,
9E74CCC829DC04E8003D6E32 /* DateInterface.swift in Sources */,
9E486DF429B9EEC4003E6945 /* UIResponder+Current.swift in Sources */,
0D26AF68299E8196005260EE /* TransactionAmountTextField.swift in Sources */,
0D26AF69299E8196005260EE /* AddressDetailsView.swift in Sources */,
@ -2836,6 +2893,7 @@
9E7FE0DF282D2DD600C374E8 /* ZcashBadge.swift in Sources */,
34F682F229A764120022C079 /* WalletConfigProviderLiveKey.swift in Sources */,
0D261040298C406F00CC9DE9 /* CrashReporterTestKey.swift in Sources */,
9E74CCC929DC04E8003D6E32 /* DateLiveKey.swift in Sources */,
9EBDF975291F79F9000A1A05 /* DerivationToolInterface.swift in Sources */,
660558F8270C862F009D6954 /* XCAssets+Generated.swift in Sources */,
9EAFEB902808183D00199FC9 /* SandboxStore.swift in Sources */,
@ -2844,6 +2902,7 @@
9E39114A2848EEB90073DD9A /* UserPreferencesStorage.swift in Sources */,
9E153A612920CE2700112F41 /* MnemonicMocks.swift in Sources */,
34DA414728E4385800F8CC61 /* TransactionSendingView.swift in Sources */,
9E74CCC729DC04E8003D6E32 /* DateInterface.swift in Sources */,
F96B41E9273B501F0021B49A /* WalletEventsFlowView.swift in Sources */,
9E4AA4F829BF76BB00752BB3 /* About.swift in Sources */,
9E33ECD429D5D99000708DE4 /* AlertRequest.swift in Sources */,
@ -2876,6 +2935,7 @@
9EAB467A2861EA6A002904A0 /* TransactionRowView.swift in Sources */,
9EB8638C2922CD4A003D0F8B /* FeedbackGeneratorTestKey.swift in Sources */,
9E0F5741297E7F1D005304FA /* TCALogging.swift in Sources */,
9E74CCC529DC04E8003D6E32 /* DateTestKey.swift in Sources */,
0DFE93E3272CA1AA000FCCA5 /* RecoveryPhraseValidationFlowStore.swift in Sources */,
9E9CEA3E29D47BE000599DF5 /* OnChangeReducer.swift in Sources */,
9E486DF929BA09C2003E6945 /* UIKit+Extensions.swift in Sources */,
@ -2885,6 +2945,7 @@
9EBDF967291ECDA2000A1A05 /* AudioServicesTestKey.swift in Sources */,
0D535FE2271F9476009A9E3E /* EnumeratedChip.swift in Sources */,
9EBDF97E291F7EB0000A1A05 /* AppVersionInterface.swift in Sources */,
9E2A07B829DAE0A900F2B086 /* ReviewRequestLiveKey.swift in Sources */,
6654C73E2715A41300901167 /* OnboardingFlowStore.swift in Sources */,
9EB863CB2923CA20003D0F8B /* SDKSynchronizerLive.swift in Sources */,
9EB863A1292398A8003D0F8B /* URIParserInterface.swift in Sources */,
@ -2938,6 +2999,7 @@
0DB8AA81271DC7520035BC9D /* DesignGuide.swift in Sources */,
9E612C7E2991491200D09B09 /* SensitiveData.swift in Sources */,
F9971A4D27680DC400A2DB75 /* RootStore.swift in Sources */,
9E2A07B729DAE0A900F2B086 /* ReviewRequestTestKey.swift in Sources */,
9EAFEB9228081E9400199FC9 /* HomeView.swift in Sources */,
F9322DC0273B555C00C105B5 /* NavigationLinks.swift in Sources */,
9EAFEB8F2808183D00199FC9 /* SandboxView.swift in Sources */,
@ -3024,6 +3086,7 @@
9E612C7629880FC900D09B09 /* LogsHandlerTest.swift in Sources */,
2EDA07A227EDE1AE00D6F09B /* TextFieldFooter.swift in Sources */,
0D26103C298C3E4800CC9DE9 /* CrashReportingInterface.swift in Sources */,
9E2A07B929DAE0A900F2B086 /* ReviewRequestInterface.swift in Sources */,
346731A229AE3A5100974482 /* UIMailDialog.swift in Sources */,
F9971A5427680DD000A2DB75 /* ProfileView.swift in Sources */,
F9971A6027680DF600A2DB75 /* ScanStore.swift in Sources */,
@ -3090,6 +3153,7 @@
9E3451A729C84E9900177D16 /* AppInitializationTests.swift in Sources */,
9E3451B029C855E600177D16 /* SettingsTests.swift in Sources */,
9E34519F29C849D300177D16 /* ImportWalletTests.swift in Sources */,
9E74CCD029DC0628003D6E32 /* ReviewRequestTests.swift in Sources */,
9E3451A029C84A2D00177D16 /* MultiLineTextFieldTests.swift in Sources */,
9E3451B729C8565500177D16 /* DatabaseFilesTests.swift in Sources */,
9E3451B129C8565500177D16 /* UserPreferencesStorageTests.swift in Sources */,
@ -3579,7 +3643,7 @@
repositoryURL = "https://github.com/pointfreeco/swift-url-routing";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.3.1;
minimumVersion = 0.5.0;
};
};
34CE032929C0938600A6626B /* XCRemoteSwiftPackageReference "ZcashLightClientKit" */ = {
@ -3587,7 +3651,7 @@
repositoryURL = "https://github.com/zcash/ZcashLightClientKit.git";
requirement = {
kind = revision;
revision = 52d5e0085094002fb64d5042b191f106bfbe710c;
revision = 16d70cfead166ae214bc813fd245d3eb62588cfd;
};
};
6654C7382715A38000901167 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */ = {
@ -3619,7 +3683,7 @@
repositoryURL = "https://github.com/pointfreeco/swift-parsing";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.9.2;
minimumVersion = 0.12.0;
};
};
/* End XCRemoteSwiftPackageReference section */

View File

@ -284,8 +284,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-parsing",
"state" : {
"revision" : "4bb9192468c1a8be57f46b7d6fd4f561c88b2195",
"version" : "0.11.0"
"revision" : "c6e2241daa46e5c6e5027a93b161bca6ba692bcc",
"version" : "0.12.0"
}
},
{
@ -302,8 +302,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-url-routing",
"state" : {
"revision" : "f54c4f74e7884f7930560c08387817ce28271770",
"version" : "0.4.0"
"revision" : "2f4f0404b3de0a0711feb7190f724d8a80bc1cfd",
"version" : "0.5.0"
}
},
{
@ -338,7 +338,7 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/zcash/ZcashLightClientKit.git",
"state" : {
"revision" : "52d5e0085094002fb64d5042b191f106bfbe710c"
"revision" : "16d70cfead166ae214bc813fd245d3eb62588cfd"
}
}
],

View File

@ -0,0 +1,20 @@
//
// DateClient.swift
// secant-testnet
//
// Created by Lukáš Korba on 04.04.2023.
//
import Foundation
import ComposableArchitecture
extension DependencyValues {
var date: DateClient {
get { self[DateClient.self] }
set { self[DateClient.self] = newValue }
}
}
struct DateClient {
let now: () -> Date
}

View File

@ -0,0 +1,15 @@
//
// DateLiveKey.swift
// secant-testnet
//
// Created by Lukáš Korba on 04.04.2023.
//
import Foundation
import ComposableArchitecture
extension DateClient: DependencyKey {
static let liveValue = Self(
now: { Date.now }
)
}

View File

@ -0,0 +1,16 @@
//
// DateTestKey.swift
// secant-testnet
//
// Created by Lukáš Korba on 15.11.2022.
//
import Foundation
import ComposableArchitecture
import XCTestDynamicOverlay
extension DateClient: TestDependencyKey {
static let testValue = Self(
now: XCTUnimplemented("\(Self.self).now", placeholder: Date.now)
)
}

View File

@ -13,7 +13,7 @@ import ZcashLightClientKit
struct Deeplink {
enum Destination: Equatable {
case home
case send(amount: Int64, address: String, memo: String)
case send(amount: Int, address: String, memo: String)
}
func resolveDeeplinkURL(_ url: URL, isValidZcashAddress: (String) throws -> Bool) throws -> Destination {
@ -25,7 +25,7 @@ struct Deeplink {
return .send(amount: 0, address: address, memo: "")
}
}
// regular URL format zcash://
let appRouter = OneOf {
// GET /home
@ -37,7 +37,7 @@ struct Deeplink {
Route(.case(Destination.send(amount:address:memo:))) {
Path { "home"; "send" }
Query {
Field("amount", default: 0) { Int64.parser() }
Field("amount", default: 0) { Digits() }
Field("address", .string, default: "")
Field("memo", .string, default: "")
}

View File

@ -0,0 +1,22 @@
//
// ReviewRequestInterface.swift
// secant-testnet
//
// Created by Lukáš Korba on 3.4.2023.
//
import ComposableArchitecture
extension DependencyValues {
var reviewRequest: ReviewRequestClient {
get { self[ReviewRequestClient.self] }
set { self[ReviewRequestClient.self] = newValue }
}
}
struct ReviewRequestClient {
let canRequestReview: () -> Bool
let foundTransactions: () async -> Void
let reviewRequested: () async -> Void
let syncFinished: () async -> Void
}

View File

@ -0,0 +1,64 @@
//
// ReviewRequestLiveKey.swift
// secant-testnet
//
// Created by Lukáš Korba on 3.4.2023.
//
import Foundation
import ComposableArchitecture
extension ReviewRequestClient: DependencyKey {
static let liveValue = ReviewRequestClient.live()
static func live(
appVersion: AppVersionClient = .liveValue,
date: DateClient = .liveValue,
userDefaults: UserDefaultsClient = .live()
) -> Self {
Self(
canRequestReview: {
// set of conditions that must be fulfilled in order to trigger review request
// the wallet must be synced
guard userDefaults.objectForKey(Constants.latestSyncKey) != nil else { return false }
// the version is ether nil or latest review is from some older version
let currentVersion = appVersion.appVersion()
if let storedVersion = userDefaults.objectForKey(Constants.versionKey) as? String {
guard currentVersion.compare(storedVersion, options: .numeric) == .orderedDescending else {
return false
}
}
// there has been at least one found transaction since the very first sync
guard userDefaults.objectForKey(Constants.foundTransactionsKey) != nil else { return false }
return true
},
foundTransactions: {
// only if there's the very first sync stored
guard userDefaults.objectForKey(Constants.latestSyncKey) != nil else { return }
await userDefaults.setValue(date.now().timeIntervalSince1970, Constants.foundTransactionsKey)
},
reviewRequested: {
// the review has been requested, update the version and timestamp
await userDefaults.setValue(date.now().timeIntervalSince1970, Constants.reviewRequestedKey)
await userDefaults.setValue(appVersion.appVersion(), Constants.versionKey)
},
syncFinished: {
// synchronizer's sync has been finished successfuly
await userDefaults.setValue(date.now().timeIntervalSince1970, Constants.latestSyncKey)
}
)
}
}
internal extension ReviewRequestClient {
enum Constants: CaseIterable {
static let foundTransactionsKey = "ReviewRequestClient.foundTransactions"
static let latestSyncKey = "ReviewRequestClient.latestSyncKey"
static let reviewRequestedKey = "ReviewRequestClient.reviewRequestedKey"
static let versionKey = "ReviewRequestClient.versionKey"
}
}

View File

@ -0,0 +1,27 @@
//
// ReviewRequestTestKey.swift
// secant-testnet
//
// Created by Lukáš Korba on 3.4.2023.
//
import ComposableArchitecture
import XCTestDynamicOverlay
extension ReviewRequestClient: TestDependencyKey {
static let testValue = Self(
canRequestReview: XCTUnimplemented("\(Self.self).canRequestReview", placeholder: false),
foundTransactions: XCTUnimplemented("\(Self.self).foundTransactions"),
reviewRequested: XCTUnimplemented("\(Self.self).reviewRequested"),
syncFinished: XCTUnimplemented("\(Self.self).syncFinished")
)
}
extension ReviewRequestClient {
static let noOp = Self(
canRequestReview: { false },
foundTransactions: { },
reviewRequested: { },
syncFinished: { }
)
}

View File

@ -33,15 +33,15 @@ struct SDKSynchronizerClient {
let getShieldedBalance: () -> WalletBalance?
let getTransparentBalance: () -> WalletBalance?
let getAllSentTransactions: () -> EffectTask<[WalletEvent]>
let getAllReceivedTransactions: () -> EffectTask<[WalletEvent]>
let getAllClearedTransactions: () -> EffectTask<[WalletEvent]>
let getAllPendingTransactions: () -> EffectTask<[WalletEvent]>
let getAllTransactions: () -> EffectTask<[WalletEvent]>
let getAllSentTransactions: () async throws -> [WalletEvent]
let getAllReceivedTransactions: () async throws -> [WalletEvent]
let getAllClearedTransactions: () async throws -> [WalletEvent]
let getAllPendingTransactions: () async throws -> [WalletEvent]
let getAllTransactions: () async throws -> [WalletEvent]
let getUnifiedAddress: (_ account: Int) -> UnifiedAddress?
let getTransparentAddress: (_ account: Int) -> TransparentAddress?
let getSaplingAddress: (_ accountIndex: Int) async -> SaplingAddress?
let getUnifiedAddress: (_ account: Int) async throws -> UnifiedAddress?
let getTransparentAddress: (_ account: Int) async throws -> TransparentAddress?
let getSaplingAddress: (_ accountIndex: Int) async throws -> SaplingAddress?
let sendTransaction: (UnifiedSpendingKey, Zatoshi, Recipient, Memo?) -> EffectTask<Result<TransactionState, NSError>>

View File

@ -28,7 +28,7 @@ extension SDKSynchronizerClient: DependencyKey {
spendParamsURL: databaseFiles.spendParamsURLFor(network),
outputParamsURL: databaseFiles.outputParamsURLFor(network),
saplingParamsSourceURL: SaplingParamsSourceURL.default,
loggerProxy: OSLogger(logLevel: .debug, category: LoggerConstants.sdkLogs)
logLevel: .debug
)
let synchronizer = SDKSynchronizer(initializer: initializer)
@ -53,79 +53,75 @@ extension SDKSynchronizerClient: DependencyKey {
getShieldedBalance: { synchronizer.latestState.shieldedBalance },
getTransparentBalance: { synchronizer.latestState.transparentBalance },
getAllSentTransactions: {
if let transactions = try? synchronizer.allSentTransactions() {
return EffectTask(value: transactions.map {
let memos = try? synchronizer.getMemos(for: $0)
let transaction = TransactionState.init(transaction: $0, memos: memos)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp)
})
let transactions = try await synchronizer.allSentTransactions()
var walletEvents: [WalletEvent] = []
for sentTransaction in transactions {
let memos = try await synchronizer.getMemos(for: sentTransaction)
let transaction = TransactionState.init(transaction: sentTransaction, memos: memos)
walletEvents.append(WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp))
}
return .none
return walletEvents
},
getAllReceivedTransactions: {
if let transactions = try? synchronizer.allReceivedTransactions() {
return EffectTask(value: transactions.map {
let memos = try? synchronizer.getMemos(for: $0)
let transaction = TransactionState.init(transaction: $0, memos: memos)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp)
})
let transactions = try await synchronizer.allReceivedTransactions()
var walletEvents: [WalletEvent] = []
for receivedTransaction in transactions {
let memos = try await synchronizer.getMemos(for: receivedTransaction)
let transaction = TransactionState.init(transaction: receivedTransaction, memos: memos)
walletEvents.append(WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp))
}
return .none
return walletEvents
},
getAllClearedTransactions: {
if let transactions = try? synchronizer.allClearedTransactions() {
return EffectTask(value: transactions.map {
let memos = try? synchronizer.getMemos(for: $0)
let transaction = TransactionState.init(transaction: $0, memos: memos)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp)
})
let transactions = try await synchronizer.allClearedTransactions()
var walletEvents: [WalletEvent] = []
for clearedTransaction in transactions {
let memos = try await synchronizer.getMemos(for: clearedTransaction)
let transaction = TransactionState.init(transaction: clearedTransaction, memos: memos)
walletEvents.append(WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp))
}
return .none
return walletEvents
},
getAllPendingTransactions: {
if let transactions = try? synchronizer.allPendingTransactions() {
return EffectTask(value: transactions.map {
let transaction = TransactionState.init(pendingTransaction: $0, latestBlockHeight: synchronizer.latestScannedHeight)
return WalletEvent(id: transaction.id, state: .pending(transaction), timestamp: transaction.timestamp)
})
let transactions = try await synchronizer.allPendingTransactions()
var walletEvents: [WalletEvent] = []
for pendingTransaction in transactions {
let transaction = TransactionState.init(
pendingTransaction: pendingTransaction,
latestBlockHeight: synchronizer.latestScannedHeight
)
walletEvents.append(WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp))
}
return .none
return walletEvents
},
getAllTransactions: {
if let pendingTransactions = try? synchronizer.allPendingTransactions(),
let clearedTransactions = try? synchronizer.allClearedTransactions() {
let clearedTxs: [WalletEvent] = clearedTransactions.map {
let transaction = TransactionState.init(transaction: $0)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp)
}
let pendingTxs: [WalletEvent] = pendingTransactions.map {
let transaction = TransactionState.init(pendingTransaction: $0, latestBlockHeight: synchronizer.latestScannedHeight)
return WalletEvent(id: transaction.id, state: .pending(transaction), timestamp: transaction.timestamp)
}
let cTxs = clearedTxs.filter { transaction in
pendingTxs.first { pending in
pending.id == transaction.id
} == nil
}
let pendingTransactions = try await synchronizer.allPendingTransactions()
let clearedTransactions = try await synchronizer.allClearedTransactions()
return .merge(
EffectTask(value: cTxs),
EffectTask(value: pendingTxs)
)
.flatMap(Publishers.Sequence.init(sequence:))
.collect()
.eraseToEffect()
let clearedTxs: [WalletEvent] = clearedTransactions.map {
let transaction = TransactionState.init(transaction: $0)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp)
}
return .none
let pendingTxs: [WalletEvent] = pendingTransactions.map {
let transaction = TransactionState.init(pendingTransaction: $0, latestBlockHeight: synchronizer.latestScannedHeight)
return WalletEvent(id: transaction.id, state: .pending(transaction), timestamp: transaction.timestamp)
}
var cTxs = clearedTxs.filter { transaction in
pendingTxs.first { pending in
pending.id == transaction.id
} == nil
}
cTxs.append(contentsOf: pendingTxs)
return cTxs
},
getUnifiedAddress: { synchronizer.getUnifiedAddress(accountIndex: $0) },
getTransparentAddress: { synchronizer.getTransparentAddress(accountIndex: $0) },
getSaplingAddress: { await synchronizer.getSaplingAddress(accountIndex: $0) },
getUnifiedAddress: { try await synchronizer.getUnifiedAddress(accountIndex: $0) },
getTransparentAddress: { try await synchronizer.getTransparentAddress(accountIndex: $0) },
getSaplingAddress: { try await synchronizer.getSaplingAddress(accountIndex: $0) },
sendTransaction: { spendingKey, amount, recipient, memo in
return .run { send in
do {

View File

@ -24,11 +24,11 @@ extension SDKSynchronizerClient: TestDependencyKey {
rewind: XCTUnimplemented("\(Self.self).rewind", placeholder: Fail(error: "Error").eraseToAnyPublisher()),
getShieldedBalance: XCTUnimplemented("\(Self.self).getShieldedBalance", placeholder: WalletBalance.zero),
getTransparentBalance: XCTUnimplemented("\(Self.self).getTransparentBalance", placeholder: WalletBalance.zero),
getAllSentTransactions: XCTUnimplemented("\(Self.self).getAllSentTransactions", placeholder: .none),
getAllReceivedTransactions: XCTUnimplemented("\(Self.self).getAllReceivedTransactions", placeholder: .none),
getAllClearedTransactions: XCTUnimplemented("\(Self.self).getAllClearedTransactions", placeholder: .none),
getAllPendingTransactions: XCTUnimplemented("\(Self.self).getAllPendingTransactions", placeholder: .none),
getAllTransactions: XCTUnimplemented("\(Self.self).getAllTransactions", placeholder: .none),
getAllSentTransactions: XCTUnimplemented("\(Self.self).getAllSentTransactions", placeholder: []),
getAllReceivedTransactions: XCTUnimplemented("\(Self.self).getAllReceivedTransactions", placeholder: []),
getAllClearedTransactions: XCTUnimplemented("\(Self.self).getAllClearedTransactions", placeholder: []),
getAllPendingTransactions: XCTUnimplemented("\(Self.self).getAllPendingTransactions", placeholder: []),
getAllTransactions: XCTUnimplemented("\(Self.self).getAllTransactions", placeholder: []),
getUnifiedAddress: XCTUnimplemented("\(Self.self).getUnifiedAddress", placeholder: nil),
getTransparentAddress: XCTUnimplemented("\(Self.self).getTransparentAddress", placeholder: nil),
getSaplingAddress: XCTUnimplemented("\(Self.self).getSaplingAddress", placeholder: nil),
@ -52,11 +52,11 @@ extension SDKSynchronizerClient {
rewind: { _ in return Empty<Void, Error>().eraseToAnyPublisher() },
getShieldedBalance: { .zero },
getTransparentBalance: { .zero },
getAllSentTransactions: { EffectTask(value: []) },
getAllReceivedTransactions: { EffectTask(value: []) },
getAllClearedTransactions: { EffectTask(value: []) },
getAllPendingTransactions: { EffectTask(value: []) },
getAllTransactions: { EffectTask(value: []) },
getAllSentTransactions: { [] },
getAllReceivedTransactions: { [] },
getAllClearedTransactions: { [] },
getAllPendingTransactions: { [] },
getAllTransactions: { [] },
getUnifiedAddress: { _ in return nil },
getTransparentAddress: { _ in return nil },
getSaplingAddress: { _ in return nil },
@ -82,7 +82,7 @@ extension SDKSynchronizerClient {
rewind: @escaping (RewindPolicy) -> AnyPublisher<Void, Error> = { _ in return Empty<Void, Error>().eraseToAnyPublisher() },
getShieldedBalance: @escaping () -> WalletBalance? = { WalletBalance(verified: Zatoshi(12345000), total: Zatoshi(12345000)) },
getTransparentBalance: @escaping () -> WalletBalance? = { WalletBalance(verified: Zatoshi(12345000), total: Zatoshi(12345000)) },
getAllSentTransactions: @escaping () -> EffectTask<[WalletEvent]> = {
getAllSentTransactions: @escaping () -> [WalletEvent] = {
let mocked: [TransactionStateMockHelper] = [
TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid(success: false), uuid: "aa11"),
TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(2), uuid: "bb22"),
@ -91,22 +91,20 @@ extension SDKSynchronizerClient {
TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(5), uuid: "ee55")
]
return EffectTask(
value:
mocked.map {
let transaction = TransactionState.placeholder(
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.status,
timestamp: $0.date,
uuid: $0.uuid
)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp ?? 0)
}
)
return mocked
.map {
let transaction = TransactionState.placeholder(
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.status,
timestamp: $0.date,
uuid: $0.uuid
)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp ?? 0)
}
},
getAllReceivedTransactions: @escaping () -> EffectTask<[WalletEvent]> = {
getAllReceivedTransactions: @escaping () -> [WalletEvent] = {
let mocked: [TransactionStateMockHelper] = [
TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid(success: false), uuid: "aa11"),
TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(2), uuid: "bb22"),
@ -115,22 +113,20 @@ extension SDKSynchronizerClient {
TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(5), uuid: "ee55")
]
return EffectTask(
value:
mocked.map {
let transaction = TransactionState.placeholder(
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.status,
timestamp: $0.date,
uuid: $0.uuid
)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp ?? 0)
}
)
return mocked
.map {
let transaction = TransactionState.placeholder(
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.status,
timestamp: $0.date,
uuid: $0.uuid
)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp ?? 0)
}
},
getAllClearedTransactions: @escaping () -> EffectTask<[WalletEvent]> = {
getAllClearedTransactions: @escaping () -> [WalletEvent] = {
let mocked: [TransactionStateMockHelper] = [
TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid(success: false), uuid: "aa11"),
TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(2), uuid: "bb22"),
@ -139,22 +135,20 @@ extension SDKSynchronizerClient {
TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(5), uuid: "ee55")
]
return EffectTask(
value:
mocked.map {
let transaction = TransactionState.placeholder(
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.status,
timestamp: $0.date,
uuid: $0.uuid
)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp ?? 0)
}
)
return mocked
.map {
let transaction = TransactionState.placeholder(
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.status,
timestamp: $0.date,
uuid: $0.uuid
)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp ?? 0)
}
},
getAllPendingTransactions: @escaping () -> EffectTask<[WalletEvent]> = {
getAllPendingTransactions: @escaping () -> [WalletEvent] = {
let mocked: [TransactionStateMockHelper] = [
TransactionStateMockHelper(
date: 1651039606,
@ -167,23 +161,21 @@ extension SDKSynchronizerClient {
TransactionStateMockHelper(date: 1651039808, amount: Zatoshi(9), uuid: "ii99")
]
return EffectTask(
value:
mocked.map {
let transaction = TransactionState.placeholder(
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.amount.amount > 5 ? .pending : $0.status,
timestamp: $0.date,
uuid: $0.uuid
)
return WalletEvent(id: transaction.id, state: .pending(transaction), timestamp: transaction.timestamp)
}
)
return mocked
.map {
let transaction = TransactionState.placeholder(
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.amount.amount > 5 ? .pending : $0.status,
timestamp: $0.date,
uuid: $0.uuid
)
return WalletEvent(id: transaction.id, state: .pending(transaction), timestamp: transaction.timestamp)
}
},
getAllTransactions: @escaping () -> EffectTask<[WalletEvent]> = {
let mockerCleared: [TransactionStateMockHelper] = [
getAllTransactions: @escaping () -> [WalletEvent] = {
let mockedCleared: [TransactionStateMockHelper] = [
TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid(success: false), uuid: "aa11"),
TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(2), uuid: "bb22"),
TransactionStateMockHelper(date: 1651039000, amount: Zatoshi(3), status: .paid(success: true), uuid: "cc33"),
@ -191,21 +183,19 @@ extension SDKSynchronizerClient {
TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(5), uuid: "ee55")
]
let clearedTransactionsEffect = EffectTask(
value:
mockerCleared.map {
let transaction = TransactionState.placeholder(
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.status,
timestamp: $0.date,
uuid: $0.uuid
)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp ?? 0)
}
)
var clearedTransactions = mockedCleared
.map {
let transaction = TransactionState.placeholder(
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.status,
timestamp: $0.date,
uuid: $0.uuid
)
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp ?? 0)
}
let mockedPending: [TransactionStateMockHelper] = [
TransactionStateMockHelper(
date: 1651039606,
@ -218,28 +208,22 @@ extension SDKSynchronizerClient {
TransactionStateMockHelper(date: 1651039808, amount: Zatoshi(9), uuid: "ii99")
]
let pendingTransactionsEffect = EffectTask(
value:
mockedPending.map {
let transaction = TransactionState.placeholder(
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.amount.amount > 5 ? .pending : $0.status,
timestamp: $0.date,
uuid: $0.uuid
)
return WalletEvent(id: transaction.id, state: .pending(transaction), timestamp: transaction.timestamp)
}
)
let pendingTransactions = mockedPending
.map {
let transaction = TransactionState.placeholder(
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.amount.amount > 5 ? .pending : $0.status,
timestamp: $0.date,
uuid: $0.uuid
)
return WalletEvent(id: transaction.id, state: .pending(transaction), timestamp: transaction.timestamp)
}
clearedTransactions.append(contentsOf: pendingTransactions)
return .merge(
clearedTransactionsEffect,
pendingTransactionsEffect
)
.flatMap(Publishers.Sequence.init(sequence:))
.collect()
.eraseToEffect()
return clearedTransactions
},
getUnifiedAddress: @escaping (_ account: Int) -> UnifiedAddress? = { _ in
// swiftlint:disable force_try

View File

@ -20,11 +20,21 @@ struct AddressDetailsReducer: ReducerProtocol {
}
var transparentAddress: String {
uAddress?.transparentReceiver()?.stringEncoded ?? L10n.AddressDetails.Error.cantExtractTransparentAddress
do {
let address = try uAddress?.transparentReceiver().stringEncoded ?? L10n.AddressDetails.Error.cantExtractTransparentAddress
return address
} catch {
return L10n.AddressDetails.Error.cantExtractTransparentAddress
}
}
var saplingAddress: String {
uAddress?.saplingReceiver()?.stringEncoded ?? L10n.AddressDetails.Error.cantExtractSaplingAddress
do {
let address = try uAddress?.saplingReceiver().stringEncoded ?? L10n.AddressDetails.Error.cantExtractSaplingAddress
return address
} catch {
return L10n.AddressDetails.Error.cantExtractSaplingAddress
}
}
}

View File

@ -10,6 +10,7 @@ typealias HomeViewStore = ViewStore<HomeReducer.State, HomeReducer.Action>
struct HomeReducer: ReducerProtocol {
private enum CancelId {}
private enum CancelEventsId {}
struct State: Equatable {
enum Destination: Equatable {
@ -23,6 +24,7 @@ struct HomeReducer: ReducerProtocol {
var balanceBreakdownState: BalanceBreakdownReducer.State
var destination: Destination?
var canRequestReview = false
var profileState: ProfileReducer.State
var requiredTransactionConfirmations = 0
var scanState: ScanReducer.State
@ -67,9 +69,12 @@ struct HomeReducer: ReducerProtocol {
case onAppear
case onDisappear
case profile(ProfileReducer.Action)
case resolveReviewRequest
case reviewRequestFinished
case send(SendFlowReducer.Action)
case settings(SettingsReducer.Action)
case syncFailed(String)
case foundTransactions
case synchronizerStateChanged(SynchronizerState)
case walletEvents(WalletEventsFlowReducer.Action)
case updateDestination(HomeReducer.State.Destination?)
@ -81,6 +86,7 @@ struct HomeReducer: ReducerProtocol {
@Dependency(\.audioServices) var audioServices
@Dependency(\.diskSpaceChecker) var diskSpaceChecker
@Dependency(\.mainQueue) var mainQueue
@Dependency(\.reviewRequest) var reviewRequest
@Dependency(\.sdkSynchronizer) var sdkSynchronizer
@Dependency(\.zcashSDKEnvironment) var zcashSDKEnvironment
@ -109,20 +115,37 @@ struct HomeReducer: ReducerProtocol {
switch action {
case .onAppear:
state.requiredTransactionConfirmations = zcashSDKEnvironment.requiredTransactionConfirmations
if diskSpaceChecker.hasEnoughFreeSpaceForSync() {
let syncEffect = sdkSynchronizer.stateStream()
.throttle(for: .seconds(0.2), scheduler: mainQueue, latest: true)
.map(HomeReducer.Action.synchronizerStateChanged)
.eraseToEffect()
.cancellable(id: CancelId.self, cancelInFlight: true)
return .concatenate(EffectTask(value: .updateDestination(nil)), syncEffect)
return .merge(
EffectTask(value: .updateDestination(nil)),
syncEffect
)
} else {
return EffectTask(value: .updateDestination(.notEnoughFreeDiskSpace))
}
case .onDisappear:
return .cancel(id: CancelId.self)
return .merge(
.cancel(id: CancelId.self),
.cancel(id: CancelEventsId.self)
)
case .resolveReviewRequest:
if reviewRequest.canRequestReview() {
state.canRequestReview = true
return .fireAndForget { await reviewRequest.reviewRequested() }
}
return .none
case .reviewRequestFinished:
state.canRequestReview = false
return .none
case .updateWalletEvents:
return .none
@ -140,10 +163,17 @@ struct HomeReducer: ReducerProtocol {
switch snapshot.syncStatus {
case .error:
return EffectTask(value: .showSynchronizerErrorAlert(snapshot))
case .synced:
return .fireAndForget { await reviewRequest.syncFinished() }
default:
return .none
}
case .foundTransactions:
return .fireAndForget { await reviewRequest.foundTransactions() }
case .updateDestination(.profile):
state.profileState.destination = nil
state.destination = .profile

View File

@ -1,9 +1,10 @@
import SwiftUI
import ComposableArchitecture
import StoreKit
struct HomeView: View {
let store: Store<HomeReducer.State, HomeReducer.Action>
var body: some View {
WithViewStore(store) { viewStore in
VStack {
@ -27,8 +28,18 @@ struct HomeView: View {
.navigationTitle(L10n.Home.title)
.navigationBarItems(trailing: settingsButton(viewStore))
.navigationBarTitleDisplayMode(.inline)
.onAppear(perform: { viewStore.send(.onAppear) })
.onDisappear(perform: { viewStore.send(.onDisappear) })
.onAppear {
viewStore.send(.onAppear)
}
.onChange(of: viewStore.canRequestReview) { canRequestReview in
if canRequestReview {
if let currentScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
SKStoreReviewController.requestReview(in: currentScene)
}
viewStore.send(.reviewRequestFinished)
}
}
.onDisappear { viewStore.send(.onDisappear) }
.navigationLinkEmpty(
isActive: viewStore.bindingForDestination(.balanceBreakdown),
destination: { BalanceBreakdownView(store: store.balanceBreakdownStore()) }

View File

@ -27,6 +27,7 @@ struct ProfileReducer: ReducerProtocol {
case back
case copyUnifiedAddressToPastboard
case onAppear
case uAddressChanged(UnifiedAddress?)
case updateDestination(ProfileReducer.State.Destination?)
}
@ -43,12 +44,17 @@ struct ProfileReducer: ReducerProtocol {
Reduce { state, action in
switch action {
case .onAppear:
state.addressDetailsState.uAddress = self.sdkSynchronizer.getUnifiedAddress(0)
state.appBuild = appVersion.appBuild()
state.appVersion = appVersion.appVersion()
state.sdkVersion = zcashSDKEnvironment.sdkVersion
return .none
return .task {
return .uAddressChanged(try? await sdkSynchronizer.getUnifiedAddress(0))
}
case .uAddressChanged(let uAddress):
state.addressDetailsState.uAddress = uAddress
return .none
case .back:
return .none

View File

@ -23,6 +23,7 @@ extension RootReducer {
case flagUpdated
case fullRescan
case quickRescan
case rateTheApp
case rescanBlockchain
case rewindDone(String?, RootReducer.Action)
case testCrashReporter // this will crash the app if live.
@ -95,6 +96,9 @@ extension RootReducer {
case .debug(.cantStartSync(let errorMessage)):
return EffectTask(value: .alert(.root(.cantStartSync(errorMessage))))
case .debug(.rateTheApp):
return .none
default: return .none
}
}

View File

@ -141,7 +141,7 @@ private extension RootReducer {
case .home:
return .destination(.deeplinkHome)
case let .send(amount, address, memo):
return .destination(.deeplinkSend(Zatoshi(amount), address, memo))
return .destination(.deeplinkSend(Zatoshi(Int64(amount)), address, memo))
}
}
}

View File

@ -32,6 +32,8 @@ extension RootReducer {
Reduce { state, action in
switch action {
case .initialization(.appDelegate(.didFinishLaunching)):
// TODO: [#704], trigger the review request logic when approved by the team,
// https://github.com/zcash/secant-ios-wallet/issues/704
return EffectTask(value: .initialization(.checkWalletConfig))
.delay(for: 0.02, scheduler: mainQueue)
.eraseToEffect()

View File

@ -164,6 +164,13 @@ private extension RootView {
}
.disabled(viewStore.exportLogsState.exportLogsDisabled)
Button(L10n.Root.Debug.Option.appReview) {
viewStore.send(.debug(.rateTheApp))
if let currentScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
SKStoreReviewController.requestReview(in: currentScene)
}
}
Button(L10n.Root.Debug.Option.rescanBlockchain) {
viewStore.send(.debug(.rescanBlockchain))
}

View File

@ -58,10 +58,9 @@ struct WalletEventsFlowReducer: ReducerProtocol {
case .synchronizerStateChanged(.synced):
state.latestMinedHeight = sdkSynchronizer.latestScannedHeight()
return sdkSynchronizer.getAllTransactions()
.receive(on: mainQueue)
.map(WalletEventsFlowReducer.Action.updateWalletEvents)
.eraseToEffect()
return .task {
return .updateWalletEvents(try await sdkSynchronizer.getAllTransactions())
}
case .synchronizerStateChanged:
return .none

View File

@ -353,6 +353,8 @@ internal enum L10n {
}
}
internal enum Option {
/// Rate the app
internal static let appReview = L10n.tr("Localizable", "root.debug.option.appReview", fallback: "Rate the app")
/// Export logs
internal static let exportLogs = L10n.tr("Localizable", "root.debug.option.exportLogs", fallback: "Export logs")
/// Go To Onboarding

View File

@ -242,6 +242,7 @@
"root.debug.option.rescanBlockchain" = "Rescan Blockchain";
"root.debug.option.nukeWallet" = "[Be careful] Nuke Wallet";
"root.debug.option.exportLogs" = "Export logs";
"root.debug.option.appReview" = "Rate the app";
"root.debug.featureFlags" = "Feature flags";
"root.debug.dialog.rescan.title" = "Rescan";
"root.debug.dialog.rescan.message" = "Select the rescan you want";

View File

@ -27,7 +27,7 @@ class AddressDetailsTests: XCTestCase {
store.send(.copySaplingAddressToPastboard)
let expectedAddress = uAddress.saplingReceiver()?.stringEncoded ?? "could not extract sapling receiver from UA"
let expectedAddress = try uAddress.saplingReceiver().stringEncoded
XCTAssertEqual(
testPasteboard.getString()?.data,
@ -49,7 +49,7 @@ class AddressDetailsTests: XCTestCase {
store.send(.copyTransparentAddressToPastboard)
let expectedAddress = uAddress.transparentReceiver()?.stringEncoded ?? "could not extract transparent receiver from UA"
let expectedAddress = try uAddress.transparentReceiver().stringEncoded
XCTAssertEqual(
testPasteboard.getString()?.data,

View File

@ -53,6 +53,7 @@ class HomeTests: XCTestCase {
store.dependencies.mainQueue = .immediate
store.dependencies.diskSpaceChecker = .mockEmptyDisk
store.dependencies.sdkSynchronizer = .mocked()
store.dependencies.reviewRequest = .noOp
store.send(.onAppear) { state in
state.requiredTransactionConfirmations = 10
@ -60,6 +61,7 @@ class HomeTests: XCTestCase {
// expected side effects as a result of .onAppear registration
store.receive(.updateDestination(nil))
store.receive(.resolveReviewRequest)
store.receive(.synchronizerStateChanged(.zero)) { state in
state.synchronizerStatusSnapshot = SyncStatusSnapshot.snapshotFor(state: .unprepared)
}
@ -76,6 +78,7 @@ class HomeTests: XCTestCase {
)
store.dependencies.diskSpaceChecker = .mockFullDisk
store.dependencies.reviewRequest = .noOp
store.send(.onAppear) { state in
state.requiredTransactionConfirmations = 10
@ -86,6 +89,8 @@ class HomeTests: XCTestCase {
state.destination = .notEnoughFreeDiskSpace
}
store.receive(.resolveReviewRequest)
// long-living (cancelable) effects need to be properly canceled.
// the .onDisappear action cancels the observer of the synchronizer status change.
store.send(.onDisappear)

View File

@ -29,11 +29,14 @@ class ProfileTests: XCTestCase {
)
await store.send(.onAppear) { state in
state.addressDetailsState.uAddress = uAddress
state.appVersion = "0.0.1"
state.appBuild = "31"
state.sdkVersion = "0.18.1-beta"
}
await store.receive(.uAddressChanged(uAddress)) { state in
state.addressDetailsState.uAddress = uAddress
}
}
func testCopyUnifiedAddressToPasteboard() throws {

View File

@ -0,0 +1,203 @@
//
// ReviewRequestTests.swift
// secantTests
//
// Created by Lukáš Korba on 04.04.2023.
//
import XCTest
import ComposableArchitecture
import ZcashLightClientKit
@testable import secant_testnet
@MainActor
final class ReviewRequestTests: XCTestCase {
func testSyncFinishedPersistency() async throws {
guard let userDefaults = UserDefaults.init(suiteName: "testSyncFinishedPersistency") else {
XCTFail("Review Request: UserDefaults failed to initialize")
return
}
let store = TestStore(
initialState: .placeholder,
reducer: HomeReducer()
)
let now = Date.now
let userDefaultsClient: UserDefaultsClient = .live(userDefaults: userDefaults)
store.dependencies.reviewRequest =
.live(
appVersion: .mock,
date: DateClient(
now: { now }
),
userDefaults: userDefaultsClient
)
var syncState: SynchronizerState = .zero
syncState.syncStatus = .synced
let snapshot = SyncStatusSnapshot.snapshotFor(state: syncState.syncStatus)
await store.send(.synchronizerStateChanged(syncState)) { state in
state.synchronizerStatusSnapshot = snapshot
}
let storedDate = userDefaultsClient.objectForKey(ReviewRequestClient.Constants.latestSyncKey) as? TimeInterval
XCTAssertEqual(now.timeIntervalSince1970, storedDate, "Review Request: stored date doesn't match the input.")
}
func testFoundTransactionsPersistency() async throws {
guard let userDefaults = UserDefaults.init(suiteName: "testFoundTransactionsPersistency") else {
XCTFail("Review Request: UserDefaults failed to initialize")
return
}
let store = TestStore(
initialState: .placeholder,
reducer: HomeReducer()
)
let now = Date.now
let userDefaultsClient: UserDefaultsClient = .live(userDefaults: userDefaults)
await userDefaultsClient.setValue("any value", ReviewRequestClient.Constants.latestSyncKey)
store.dependencies.reviewRequest =
.live(
appVersion: .mock,
date: DateClient(
now: { now }
),
userDefaults: userDefaultsClient
)
await store.send(.foundTransactions)
let storedDate = userDefaultsClient.objectForKey(ReviewRequestClient.Constants.foundTransactionsKey) as? TimeInterval
XCTAssertEqual(now.timeIntervalSince1970, storedDate, "Review Request: stored date doesn't match the input.")
}
func testCanRequestReview_FirstTime() async throws {
guard let userDefaults = UserDefaults.init(suiteName: "testCanRequestReview_FirstTime") else {
XCTFail("Review Request: UserDefaults failed to initialize")
return
}
let now = Date.now
let userDefaultsClient: UserDefaultsClient = .live(userDefaults: userDefaults)
await userDefaultsClient.setValue("any value", ReviewRequestClient.Constants.latestSyncKey)
await userDefaultsClient.setValue("any value", ReviewRequestClient.Constants.foundTransactionsKey)
let reviewRequest = ReviewRequestClient.live(
appVersion: .mock,
date: DateClient(
now: { now }
),
userDefaults: userDefaultsClient
)
XCTAssertTrue(reviewRequest.canRequestReview())
}
func testCanRequestReview_NewerVersion() async throws {
guard let userDefaults = UserDefaults.init(suiteName: "testCanRequestReview_NewerVersion") else {
XCTFail("Review Request: UserDefaults failed to initialize")
return
}
let now = Date.now
let userDefaultsClient: UserDefaultsClient = .live(userDefaults: userDefaults)
await userDefaultsClient.setValue("any value", ReviewRequestClient.Constants.latestSyncKey)
await userDefaultsClient.setValue("any value", ReviewRequestClient.Constants.foundTransactionsKey)
await userDefaultsClient.setValue("0.0.1", ReviewRequestClient.Constants.versionKey)
let reviewRequest = ReviewRequestClient.live(
appVersion: AppVersionClient(
appVersion: { "0.0.2" },
appBuild: { "1" }
),
date: DateClient(
now: { now }
),
userDefaults: userDefaultsClient
)
XCTAssertTrue(reviewRequest.canRequestReview())
}
func testCanRequestReview_OlderVersion() async throws {
guard let userDefaults = UserDefaults.init(suiteName: "testCanRequestReview_OlderVersion") else {
XCTFail("Review Request: UserDefaults failed to initialize")
return
}
let now = Date.now
let userDefaultsClient: UserDefaultsClient = .live(userDefaults: userDefaults)
await userDefaultsClient.setValue("any value", ReviewRequestClient.Constants.latestSyncKey)
await userDefaultsClient.setValue("any value", ReviewRequestClient.Constants.foundTransactionsKey)
await userDefaultsClient.setValue("0.0.2", ReviewRequestClient.Constants.versionKey)
let reviewRequest = ReviewRequestClient.live(
appVersion: AppVersionClient(
appVersion: { "0.0.1" },
appBuild: { "1" }
),
date: DateClient(
now: { now }
),
userDefaults: userDefaultsClient
)
XCTAssertFalse(reviewRequest.canRequestReview())
}
func testCanRequestReview_MissingSync() async throws {
guard let userDefaults = UserDefaults.init(suiteName: "testCanRequestReview_MissingSync") else {
XCTFail("Review Request: UserDefaults failed to initialize")
return
}
let now = Date.now
let userDefaultsClient: UserDefaultsClient = .live(userDefaults: userDefaults)
let reviewRequest = ReviewRequestClient.live(
appVersion: .mock,
date: DateClient(
now: { now }
),
userDefaults: userDefaultsClient
)
XCTAssertFalse(reviewRequest.canRequestReview())
}
func testCanRequestReview_MissingTransaction() async throws {
guard let userDefaults = UserDefaults.init(suiteName: "testCanRequestReview_MissingTransaction") else {
XCTFail("Review Request: UserDefaults failed to initialize")
return
}
let now = Date.now
let userDefaultsClient: UserDefaultsClient = .live(userDefaults: userDefaults)
await userDefaultsClient.setValue("any value", ReviewRequestClient.Constants.latestSyncKey)
await userDefaultsClient.setValue("0.0.1", ReviewRequestClient.Constants.versionKey)
let reviewRequest = ReviewRequestClient.live(
appVersion: AppVersionClient(
appVersion: { "0.0.2" },
appBuild: { "1" }
),
date: DateClient(
now: { now }
),
userDefaults: userDefaultsClient
)
XCTAssertFalse(reviewRequest.canRequestReview())
}
}

View File

@ -51,6 +51,7 @@ class HomeSnapshotTests: XCTestCase {
.dependency(\.diskSpaceChecker, .mockEmptyDisk)
.dependency(\.sdkSynchronizer, .noOp)
.dependency(\.mainQueue, .immediate)
.dependency(\.reviewRequest, .noOp)
)
// landing home screen

View File

@ -11,8 +11,6 @@ import ComposableArchitecture
import ZcashLightClientKit
class WalletEventsTests: XCTestCase {
static let testScheduler = DispatchQueue.test
func testSynchronizerSubscription() throws {
let store = TestStore(
initialState: WalletEventsFlowReducer.State(
@ -36,7 +34,7 @@ class WalletEventsTests: XCTestCase {
store.send(.onDisappear)
}
func testSynchronizerStateChanged2Synced() throws {
@MainActor func testSynchronizerStateChanged2Synced() async throws {
let mocked: [TransactionStateMockHelper] = [
TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid(success: false), uuid: "aa11"),
TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(2), uuid: "bb22"),
@ -81,16 +79,14 @@ class WalletEventsTests: XCTestCase {
reducer: WalletEventsFlowReducer()
)
store.dependencies.mainQueue = Self.testScheduler.eraseToAnyScheduler()
store.dependencies.mainQueue = .immediate
store.dependencies.sdkSynchronizer = .mocked()
store.send(.synchronizerStateChanged(.synced)) { state in
await store.send(.synchronizerStateChanged(.synced)) { state in
state.latestMinedHeight = 0
}
Self.testScheduler.advance(by: 0.01)
store.receive(.updateWalletEvents(walletEvents)) { state in
await store.receive(.updateWalletEvents(walletEvents)) { state in
let receivedWalletEvents = IdentifiedArrayOf(
uniqueElements:
walletEvents
@ -104,6 +100,8 @@ class WalletEventsTests: XCTestCase {
state.walletEvents = receivedWalletEvents
}
await store.finish()
}
func testCopyToPasteboard() throws {