From d807f86d49994f87f340fd51979f2d2d29bf8634 Mon Sep 17 00:00:00 2001 From: Michal Fousek Date: Tue, 27 Sep 2022 19:14:52 +0200 Subject: [PATCH] [#81] Update Send Confirmation screen (#426) Closes #81 - Ported ZcashHoldToSendButton from ECC referecne wallet and make it work with TCA. And then use it on Send Confirmation screen. - Added colors that are used by HoldToSendButton to asset catalogue. - Update Send confirmation screen look according to designs. - Ported ZcashCheckCircle from ECC reference wallet and use it on Send confirmation screen. - Added colors used by CheckCircle to assets catalogue. - Add test for the case when not including memo in transaction. - Updated ZcashLightClientKit to 0.16.10-beta to fix transaction failure when memo is nil. --- secant.xcodeproj/project.pbxproj | 48 +++++++- .../xcshareddata/swiftpm/Package.resolved | 4 +- secant/Features/SendFlow/SendFlowStore.swift | 32 +++-- secant/Features/SendFlow/SendFlowView.swift | 3 +- .../SendFlow/Views/HoldToSendButton.swift | 114 ++++++++++++++++++ .../Views/TransactionConfirmationView.swift | 61 +++++----- .../Icons/checkmark.imageset/Contents.json | 21 ++++ .../Icons/checkmark.imageset/checkmark.pdf | Bin 0 -> 3991 bytes .../Colors.xcassets/CheckCircle/Contents.json | 6 + .../externalRing.colorset/Contents.json | 38 ++++++ .../internalRing.colorset/Contents.json | 38 ++++++ .../holdToSendButton.colorset/Contents.json | 38 ++++++ .../gradientDarkEnd.colorset/Contents.json | 38 ++++++ .../gradientDarkStart.colorset/Contents.json | 38 ++++++ .../drawerShadow.colorset/Contents.json | 6 +- .../Contents.json | 6 +- .../Contents.json | 38 ++++++ .../numberedTextShadow.colorset/Contents.json | 12 +- .../forDarkBackground.colorset/Contents.json | 38 ++++++ .../Underline/Gray.colorset/Contents.json | 12 +- .../Generated/XCAssets+Generated.swift | 10 ++ .../Backgrounds/ScreenBackground.swift | 12 ++ .../CheckCircle/CheckCircle.swift | 91 ++++++++++++++ .../CheckCircle/CheckCircleStore.swift | 54 +++++++++ secant/UI Components/Shapes/Wedge.swift | 39 ++++++ .../MultiLineTextFieldStore.swift | 6 +- secant/Wrappers/WrappedSDKSynchronizer.swift | 2 +- secantTests/SendTests/SendTests.swift | 69 +++++++++++ ...TransactionConfirmationSnapshotTests.swift | 88 ++++++++++++++ 29 files changed, 897 insertions(+), 65 deletions(-) create mode 100644 secant/Features/SendFlow/Views/HoldToSendButton.swift create mode 100644 secant/Resources/Assets.xcassets/Icons/checkmark.imageset/Contents.json create mode 100644 secant/Resources/Assets.xcassets/Icons/checkmark.imageset/checkmark.pdf create mode 100644 secant/Resources/Colors.xcassets/CheckCircle/Contents.json create mode 100644 secant/Resources/Colors.xcassets/CheckCircle/externalRing.colorset/Contents.json create mode 100644 secant/Resources/Colors.xcassets/CheckCircle/internalRing.colorset/Contents.json create mode 100644 secant/Resources/Colors.xcassets/ProgressIndicator/holdToSendButton.colorset/Contents.json create mode 100644 secant/Resources/Colors.xcassets/ScreenBackground/gradientDarkEnd.colorset/Contents.json create mode 100644 secant/Resources/Colors.xcassets/ScreenBackground/gradientDarkStart.colorset/Contents.json create mode 100644 secant/Resources/Colors.xcassets/Shadow/holdToSendButtonShadow.colorset/Contents.json create mode 100644 secant/Resources/Colors.xcassets/Text/forDarkBackground.colorset/Contents.json create mode 100644 secant/UI Components/CheckCircle/CheckCircle.swift create mode 100644 secant/UI Components/CheckCircle/CheckCircleStore.swift create mode 100644 secant/UI Components/Shapes/Wedge.swift create mode 100644 secantTests/SnapshotTests/SendSnapshotTests/TransactionConfirmationSnapshotTests.swift diff --git a/secant.xcodeproj/project.pbxproj b/secant.xcodeproj/project.pbxproj index c6b9a3b..88ce944 100644 --- a/secant.xcodeproj/project.pbxproj +++ b/secant.xcodeproj/project.pbxproj @@ -68,6 +68,11 @@ 2EDA07A027EDE18C00D6F09B /* TCATextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EDA079F27EDE18C00D6F09B /* TCATextField.swift */; }; 2EDA07A227EDE1AE00D6F09B /* TextFieldFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EDA07A127EDE1AE00D6F09B /* TextFieldFooter.swift */; }; 2EDA07A427EDE2A900D6F09B /* DebugFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EDA07A327EDE2A900D6F09B /* DebugFrame.swift */; }; + 346715A528E2027D0035F7C4 /* CheckCircleStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346715A428E2027D0035F7C4 /* CheckCircleStore.swift */; }; + 346715A828E20FE40035F7C4 /* TransactionConfirmationSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346715A728E20FE40035F7C4 /* TransactionConfirmationSnapshotTests.swift */; }; + 346D41E428DF0B8600963F36 /* CheckCircle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346D41E328DF0B8600963F36 /* CheckCircle.swift */; }; + 34E0AF0F28DEE4C70034CF37 /* HoldToSendButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34E0AF0E28DEE4C70034CF37 /* HoldToSendButton.swift */; }; + 34E0AF1128DEE5220034CF37 /* Wedge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34E0AF1028DEE5220034CF37 /* Wedge.swift */; }; 660558E9270C7A54009D6954 /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 660558E8270C7A54009D6954 /* Colors.xcassets */; }; 660558F7270C862F009D6954 /* Fonts+Generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 660558F5270C862F009D6954 /* Fonts+Generated.swift */; }; 660558F8270C862F009D6954 /* XCAssets+Generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 660558F6270C862F009D6954 /* XCAssets+Generated.swift */; }; @@ -302,6 +307,11 @@ 2EDA079F27EDE18C00D6F09B /* TCATextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCATextField.swift; sourceTree = ""; }; 2EDA07A127EDE1AE00D6F09B /* TextFieldFooter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldFooter.swift; sourceTree = ""; }; 2EDA07A327EDE2A900D6F09B /* DebugFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugFrame.swift; sourceTree = ""; }; + 346715A428E2027D0035F7C4 /* CheckCircleStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckCircleStore.swift; sourceTree = ""; }; + 346715A728E20FE40035F7C4 /* TransactionConfirmationSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionConfirmationSnapshotTests.swift; sourceTree = ""; }; + 346D41E328DF0B8600963F36 /* CheckCircle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckCircle.swift; sourceTree = ""; }; + 34E0AF0E28DEE4C70034CF37 /* HoldToSendButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoldToSendButton.swift; sourceTree = ""; }; + 34E0AF1028DEE5220034CF37 /* Wedge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wedge.swift; sourceTree = ""; }; 660558E8270C7A54009D6954 /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = ""; }; 660558F5270C862F009D6954 /* Fonts+Generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Fonts+Generated.swift"; sourceTree = ""; }; 660558F6270C862F009D6954 /* XCAssets+Generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "XCAssets+Generated.swift"; sourceTree = ""; }; @@ -488,6 +498,7 @@ 0D0781C5278776B90083ACD7 /* Shapes */ = { isa = PBXGroup; children = ( + 34E0AF1028DEE5220034CF37 /* Wedge.swift */, 0D0781C7278776D20083ACD7 /* ZcashSymbol.swift */, ); path = Shapes; @@ -707,6 +718,23 @@ path = Components; sourceTree = ""; }; + 346715A628E20FB30035F7C4 /* SendSnapshotTests */ = { + isa = PBXGroup; + children = ( + 346715A728E20FE40035F7C4 /* TransactionConfirmationSnapshotTests.swift */, + ); + path = SendSnapshotTests; + sourceTree = ""; + }; + 346D41E228DF0B0900963F36 /* CheckCircle */ = { + isa = PBXGroup; + children = ( + 346D41E328DF0B8600963F36 /* CheckCircle.swift */, + 346715A428E2027D0035F7C4 /* CheckCircleStore.swift */, + ); + path = CheckCircle; + sourceTree = ""; + }; 663FAB9A271D873300E495F8 /* Buttons */ = { isa = PBXGroup; children = ( @@ -866,17 +894,18 @@ 9E391162284E3ECF0073DD9A /* SnapshotTests */ = { isa = PBXGroup; children = ( + 9E92AF0728530EBF007367AD /* View+UIImage.swift */, 9E94C62128AA7ECD008256E9 /* BalanceBreakdownSnapshotTests */, - 9E7225EF2889537E00DF7F17 /* SettingsSnapshotTests */, - 9E7CB6252874267B00A02233 /* ProfileSnapshotTests */, - 9E7CB6102869881300A02233 /* WalletEventsSnapshotTests */, 9E9ECC8B28589E150099D5A2 /* HomeSnapshotTests */, 9E9ECC9328589E150099D5A2 /* ImportWalletSnapshotTests */, 9E9ECC9528589E150099D5A2 /* OnboardingSnapshotTests */, + 9E7CB6252874267B00A02233 /* ProfileSnapshotTests */, 9E9ECC8F28589E150099D5A2 /* RecoveryPhraseDisplaySnapshotTests */, 9E9ECC9128589E150099D5A2 /* RecoveryPhraseValidationFlowSnapshotTests */, + 346715A628E20FB30035F7C4 /* SendSnapshotTests */, + 9E7225EF2889537E00DF7F17 /* SettingsSnapshotTests */, + 9E7CB6102869881300A02233 /* WalletEventsSnapshotTests */, 9E9ECC8D28589E150099D5A2 /* WelcomeSnapshotTests */, - 9E92AF0728530EBF007367AD /* View+UIImage.swift */, ); path = SnapshotTests; sourceTree = ""; @@ -1073,6 +1102,7 @@ 9E7FE0BE282D1DFE00C374E8 /* UI Components */ = { isa = PBXGroup; children = ( + 346D41E228DF0B0900963F36 /* CheckCircle */, 9E7CB6132869E8A700A02233 /* CircularProgress */, 0DF2DC5227235E1F00FA31E2 /* Extensions */, 0DB8AA80271DC7520035BC9D /* DesignGuide.swift */, @@ -1355,8 +1385,9 @@ children = ( F9C165BB2740403600592F76 /* CreateTransactionView.swift */, F9C165B92740403600592F76 /* TransactionConfirmationView.swift */, - F9C165BD2740403600592F76 /* TransactionSentView.swift */, 9E5BF640281FD7B600BA3F17 /* TransactionFailedView.swift */, + F9C165BD2740403600592F76 /* TransactionSentView.swift */, + 34E0AF0E28DEE4C70034CF37 /* HoldToSendButton.swift */, ); path = Views; sourceTree = ""; @@ -1588,6 +1619,7 @@ 2EB7758727FC67FD00269373 /* TransactionAmountTextFieldStore.swift in Sources */, 669FDAE9272C23B3007B9422 /* CircularFrame.swift in Sources */, 9EF8136027F043CC0075AF48 /* AppDelegate.swift in Sources */, + 34E0AF1128DEE5220034CF37 /* Wedge.swift in Sources */, F96B41E8273B501F0021B49A /* TransactionDetailView.swift in Sources */, 9E02B56A27FED43E005B809B /* WrappedFileManager.swift in Sources */, 663FABA2271D876C00E495F8 /* SecondaryButton.swift in Sources */, @@ -1617,6 +1649,7 @@ 9E2DF99D27CF704D00649636 /* ImportSeedEditor.swift in Sources */, F9971A5327680DD000A2DB75 /* ProfileStore.swift in Sources */, 9E39114D2848EEB90073DD9A /* WalletStorage.swift in Sources */, + 346D41E428DF0B8600963F36 /* CheckCircle.swift in Sources */, 669FDAEB272C23C2007B9422 /* CircularFrameBadge.swift in Sources */, 9E39113B2848D5180073DD9A /* WrappedNumberFormatter.swift in Sources */, 2E8719CD27FB0D3B0082C926 /* CurrencySelectionView.swift in Sources */, @@ -1671,6 +1704,7 @@ 663FAB9C271D874D00E495F8 /* ActiveButton.swift in Sources */, 9E2F1C842809B606004E65FE /* DebugMenu.swift in Sources */, 9E02B5C3280458D2005B809B /* WrappedDerivationTool.swift in Sources */, + 34E0AF0F28DEE4C70034CF37 /* HoldToSendButton.swift in Sources */, F9C165C02740403600592F76 /* TransactionConfirmationView.swift in Sources */, 0DF2DC5427235E3E00FA31E2 /* View+InnerShadow.swift in Sources */, 9E39113F2848EC360073DD9A /* WrappedRecoveryPhraseRandomizer.swift in Sources */, @@ -1689,6 +1723,7 @@ 66A0807B271993C500118B79 /* OnboardingProgressIndicator.swift in Sources */, 0D7DF08C271DCC0E00530046 /* ScreenBackground.swift in Sources */, 9E01F8242833C0D8000EFC57 /* WrappedURIParser.swift in Sources */, + 346715A528E2027D0035F7C4 /* CheckCircleStore.swift in Sources */, F9C165C22740403600592F76 /* CreateTransactionView.swift in Sources */, F9C165B4274031F600592F76 /* Bindings.swift in Sources */, 2E35F99A27B3E99C00EB79CD /* TextFieldTitleAccessoryButtonStyle.swift in Sources */, @@ -1747,6 +1782,7 @@ 9E391132284644580073DD9A /* AppInitializationTests.swift in Sources */, 9E9ECC9928589E150099D5A2 /* RecoveryPhraseDisplaySnapshotTests.swift in Sources */, 9E391124283E4CAC0073DD9A /* ImportWalletTests.swift in Sources */, + 346715A828E20FE40035F7C4 /* TransactionConfirmationSnapshotTests.swift in Sources */, 9E5BF63F2819542C00BA3F17 /* WalletEventsTests.swift in Sources */, 0D4E7A1B26B364180058B01E /* secantTests.swift in Sources */, 0DFE93E6272CB6F7000FCCA5 /* RecoveryPhraseValidationTests.swift in Sources */, @@ -2082,7 +2118,7 @@ repositoryURL = "https://github.com/zcash/ZcashLightClientKit/"; requirement = { kind = exactVersion; - version = "0.16.6-beta"; + version = "0.16.10-beta"; }; }; 6654C7382715A38000901167 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */ = { diff --git a/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index f8a897f..a2d4847 100644 --- a/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -203,8 +203,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/zcash/ZcashLightClientKit", "state" : { - "revision" : "a37c140441d78fa6ad9c89e2fbb539f1ddb7e5a1", - "version" : "0.16.6-beta" + "revision" : "9e41fb43757fd7b0eb8e817320537230774258cf", + "version" : "0.16.10-beta" } } ], diff --git a/secant/Features/SendFlow/SendFlowStore.swift b/secant/Features/SendFlow/SendFlowStore.swift index dae3b8e..82444ab 100644 --- a/secant/Features/SendFlow/SendFlowStore.swift +++ b/secant/Features/SendFlow/SendFlowStore.swift @@ -23,6 +23,7 @@ struct SendFlowState: Equatable { case done } + var addMemoState: Bool var isSendingTransaction = false var memoState: MultiLineTextFieldState var route: Route? @@ -74,6 +75,7 @@ struct SendFlowState: Equatable { // MARK: - Action enum SendFlowAction: Equatable { + case addMemo(CheckCircleAction) case memo(MultiLineTextFieldAction) case onAppear case onDisappear @@ -107,12 +109,16 @@ extension SendFlowReducer { sendReducer, transactionAddressInputReducer, transactionAmountInputReducer, - memoReducer + memoReducer, + addMemoReducer ] ) private static let sendReducer = SendFlowReducer { state, action, environment in switch action { + case .addMemo: + return .none + case .updateRoute(.failure): state.route = .failure state.isSendingTransaction = false @@ -146,7 +152,7 @@ extension SendFlowReducer { with: spendingKey, zatoshi: state.amount, to: state.address, - memo: state.memoState.text, + memo: state.addMemoState ? state.memoState.text : nil, from: 0 ) .receive(on: environment.scheduler) @@ -196,6 +202,12 @@ extension SendFlowReducer { } } + private static let addMemoReducer: SendFlowReducer = CheckCircleReducer.default.pullback( + state: \SendFlowState.addMemoState, + action: /SendFlowAction.addMemo, + environment: { _ in Void() } + ) + private static let transactionAddressInputReducer: SendFlowReducer = TransactionAddressTextFieldReducer.default.pullback( state: \SendFlowState.transactionAddressInputState, action: /SendFlowAction.transactionAddressInput, @@ -237,6 +249,13 @@ extension SendFlowReducer { // MARK: - Store extension SendFlowStore { + func addMemoStore() -> CheckCircleStore { + self.scope( + state: \.addMemoState, + action: SendFlowAction.addMemo + ) + } + func memoStore() -> MultiLineTextFieldStore { self.scope( state: \.memoState, @@ -289,6 +308,7 @@ extension SendFlowViewStore { extension SendFlowState { static var placeholder: Self { .init( + addMemoState: true, memoState: .placeholder, route: nil, transactionAddressInputState: .placeholder, @@ -298,6 +318,7 @@ extension SendFlowState { static var emptyPlaceholder: Self { .init( + addMemoState: true, memoState: .placeholder, route: nil, transactionAddressInputState: .placeholder, @@ -310,12 +331,7 @@ extension SendFlowState { extension SendFlowStore { static var placeholder: SendFlowStore { return SendFlowStore( - initialState: .init( - memoState: .placeholder, - route: nil, - transactionAddressInputState: .placeholder, - transactionAmountInputState: .placeholder - ), + initialState: .emptyPlaceholder, reducer: .default, environment: SendFlowEnvironment( derivationTool: .live(), diff --git a/secant/Features/SendFlow/SendFlowView.swift b/secant/Features/SendFlow/SendFlowView.swift index b939135..54ebc21 100644 --- a/secant/Features/SendFlow/SendFlowView.swift +++ b/secant/Features/SendFlow/SendFlowView.swift @@ -19,7 +19,7 @@ struct SendFlowView: View { .navigationLinkEmpty( isActive: viewStore.bindingForConfirmation, destination: { - TransactionConfirmation(viewStore: viewStore) + TransactionConfirmation(store: store) } ) } @@ -34,6 +34,7 @@ struct SendFLowView_Previews: PreviewProvider { SendFlowView( store: .init( initialState: .init( + addMemoState: true, memoState: .placeholder, route: nil, transactionAddressInputState: .placeholder, diff --git a/secant/Features/SendFlow/Views/HoldToSendButton.swift b/secant/Features/SendFlow/Views/HoldToSendButton.swift new file mode 100644 index 0000000..2916078 --- /dev/null +++ b/secant/Features/SendFlow/Views/HoldToSendButton.swift @@ -0,0 +1,114 @@ +// +// ZcashSendButton.swift +// wallet +// +// Created by Francisco Gindre on 1/9/20. +// Copyright © 2020 Francisco Gindre. All rights reserved. +// + +import SwiftUI +import ComposableArchitecture + +struct HoldToSendButton: View { + var minimumDuration: TimeInterval = 5 + let innerCircleScale: CGFloat = 0.8 + var completionStrokeWidth: CGFloat = 16.0 + @State var isPressing = false + @State var startAngle: CGFloat = -90 + @State var endAngle: CGFloat = -90 + + var longPressSucceded: () -> Void + + var body: some View { + ZStack(alignment: .center) { + GeometryReader { geometry in + Circle() + .size(geometry.size) + .fill(Color.black) + .shadow(color: Asset.Colors.Shadow.holdToSendButtonShadow.color, radius: 2, x: 0, y: 2) + + Circle() + .size(geometry.size) + .fill(Asset.Colors.Shadow.holdToSendButtonShadow.color) + .scaleEffect(self.innerCircleScale) + .opacity(0.35) + + Wedge( + startAngle: self.startAngle, + endAngle: self.endAngle, + clockwise: false + ) + .stroke(Asset.Colors.ProgressIndicator.holdToSendButton.color, lineWidth: self.completionStrokeWidth) + .frame( + width: geometry.size.width - self.completionStrokeWidth, + height: geometry.size.height - self.completionStrokeWidth + ) + .offset(x: self.completionStrokeWidth / 2, y: self.completionStrokeWidth / 2) + + Text("Press and hold\nto send ZEC") + .foregroundColor(.white) + .multilineTextAlignment(.center) + .frame( + minWidth: geometry.size.width, + idealWidth: geometry.size.width, + maxWidth: geometry.size.width, + minHeight: geometry.size.height, + idealHeight: geometry.size.height, + maxHeight: geometry.size.height, + alignment: .center + ) + } + } + .frame( + width: 167, + height: 167, + alignment: .center + ) + .onLongPressGesture( + minimumDuration: minimumDuration, + maximumDistance: 167, + pressing: { isPressing in + if !self.isPressing && isPressing { + self.isPressing = isPressing + withAnimation(.linear(duration: self.minimumDuration)) { + self.startAnimation() + } + } else if self.isPressing && !isPressing { + self.isPressing = isPressing + withAnimation(.easeOut(duration: 0.3)) { + self.cancelAnimation() + } + } + }, + perform: { + self.endAnimation() + self.isPressing = false + longPressSucceded() + } + ) + } + + func startAnimation() { + self.startAngle = -90 + self.endAngle = 270 + } + + func endAnimation() { + self.startAngle = -90 + self.endAngle = 270 + } + + func cancelAnimation() { + self.startAngle = -90 + self.endAngle = -90 + } +} + +struct HoldToSendButton_Previews: PreviewProvider { + static var previews: some View { + ZStack { + HoldToSendButton(longPressSucceded: { }) + .applyDarkScreenBackground() + } + } +} diff --git a/secant/Features/SendFlow/Views/TransactionConfirmationView.swift b/secant/Features/SendFlow/Views/TransactionConfirmationView.swift index 7bf0acc..2a1640d 100644 --- a/secant/Features/SendFlow/Views/TransactionConfirmationView.swift +++ b/secant/Features/SendFlow/Views/TransactionConfirmationView.swift @@ -2,37 +2,45 @@ import SwiftUI import ComposableArchitecture struct TransactionConfirmation: View { - let viewStore: SendFlowViewStore + let store: SendFlowStore var body: some View { - VStack { - Text("Send \(viewStore.amount.decimalString()) ZEC") - .padding() + WithViewStore(store) { viewStore in + VStack { + Text("Send \(viewStore.amount.decimalString()) ZEC to") + .padding() + .foregroundColor(Asset.Colors.Text.forDarkBackground.color) - Text("To \(viewStore.address)") - .padding() + Text("\(viewStore.address)?") + .truncationMode(.middle) + .lineLimit(1) + .padding() + .foregroundColor(Asset.Colors.Text.forDarkBackground.color) - Spacer() + HStack { + CheckCircle(viewStore: ViewStore(store.addMemoStore())) + Text("Includes memo") + .foregroundColor(Asset.Colors.Text.forDarkBackground.color) + } - Button( - action: { viewStore.send(.sendConfirmationPressed) }, - label: { Text("Confirm") } + Spacer() + + HoldToSendButton { + viewStore.send(.sendConfirmationPressed) + } + + Spacer() + } + .applyDarkScreenBackground() + .navigationLinkEmpty( + isActive: viewStore.bindingForSuccess, + destination: { TransactionSent(viewStore: viewStore) } + ) + .navigationLinkEmpty( + isActive: viewStore.bindingForFailure, + destination: { TransactionFailed(viewStore: viewStore) } ) - .activeButtonStyle - .frame(height: 50) - .padding() - - Spacer() } - .applyScreenBackground() - .navigationLinkEmpty( - isActive: viewStore.bindingForSuccess, - destination: { TransactionSent(viewStore: viewStore) } - ) - .navigationLinkEmpty( - isActive: viewStore.bindingForFailure, - destination: { TransactionFailed(viewStore: viewStore) } - ) } } @@ -44,11 +52,8 @@ struct Confirmation_Previews: PreviewProvider { StateContainer( initialState: (false) ) { _ in - TransactionConfirmation( - viewStore: ViewStore(.placeholder) - ) + TransactionConfirmation(store: .placeholder) } } - .preferredColorScheme(.dark) } } diff --git a/secant/Resources/Assets.xcassets/Icons/checkmark.imageset/Contents.json b/secant/Resources/Assets.xcassets/Icons/checkmark.imageset/Contents.json new file mode 100644 index 0000000..190415a --- /dev/null +++ b/secant/Resources/Assets.xcassets/Icons/checkmark.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "checkmark.pdf", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/secant/Resources/Assets.xcassets/Icons/checkmark.imageset/checkmark.pdf b/secant/Resources/Assets.xcassets/Icons/checkmark.imageset/checkmark.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e3b89e8fc0a130f2ee2e4ff742f01a261ae20808 GIT binary patch literal 3991 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 some View { + self.modifier( + ScreenBackgroundModifier( + colors: [ + Asset.Colors.ScreenBackground.gradientDarkStart.color, + Asset.Colors.ScreenBackground.gradientDarkEnd.color + ] + ) + ) + } } struct ScreenBackground_Previews: PreviewProvider { diff --git a/secant/UI Components/CheckCircle/CheckCircle.swift b/secant/UI Components/CheckCircle/CheckCircle.swift new file mode 100644 index 0000000..b032d07 --- /dev/null +++ b/secant/UI Components/CheckCircle/CheckCircle.swift @@ -0,0 +1,91 @@ +// +// ZcashCheckCircle.swift +// wallet +// +// Created by Francisco Gindre on 1/7/20. +// Copyright © 2020 Francisco Gindre. All rights reserved. +// + +import ComposableArchitecture +import SwiftUI + +struct CheckCircle: View { + let viewStore: CheckCircleViewStore + + var externalRingColor: Color = Asset.Colors.CheckCircle.externalRing.color + var internalRingColor: Color = Asset.Colors.CheckCircle.internalRing.color + + var backgroundColor: Color = .clear + + func backgroundShape(size: CGSize) -> some View { + Path { path in + path.addArc( + center: CGPoint( + x: size.width / 2, + y: size.height / 2 + ), + radius: size.width / 2, + startAngle: Angle(degrees: 0), + endAngle: Angle(degrees: 360), + clockwise: true + ) + } + .fill(self.backgroundColor) + } + + var body: some View { + GeometryReader { geometry in + ZStack(alignment: .center) { + self.backgroundShape(size: geometry.size) + self.ring(size: geometry.size, color: self.externalRingColor, lineWidth: 2) + self.ring( + size: CGSize(width: geometry.size.width, height: geometry.size.height), + color: self.internalRingColor, + lineWidth: 4 + ) + .scaleEffect(0.9, anchor: UnitPoint(x: 0.5, y: 0.5)) + .opacity(self.viewStore.state ? 1 : 0) + + Image("checkmark") + .opacity(self.viewStore.state ? 1 : 0) + } + .gesture( + TapGesture() + .onEnded { _ in self.viewStore.send(.updateIsChecked) } + ) + } + .frame(width: 30, height: 30, alignment: .center) + } + + func ringPath(size: CGSize) -> Path { + Path { path in + path.addArc( + center: CGPoint( + x: size.width / 2, + y: size.height / 2 + ), + radius: size.width / 2, + startAngle: Angle(degrees: 0), + endAngle: Angle(degrees: 360), + clockwise: true + ) + } + } + + func ring(size: CGSize, color: Color, lineWidth: CGFloat) -> some View { + ringPath(size: size) + .stroke(color, lineWidth: lineWidth) + } +} + +struct ZcashCheckCircle_Previews: PreviewProvider { + static var previews: some View { + VStack { + Spacer() + CheckCircle(viewStore: ViewStore(CheckCircleStore.mock(isChecked: true))) + CheckCircle(viewStore: ViewStore(CheckCircleStore.mock(isChecked: false))) + Spacer() + } + .applyDarkScreenBackground() + } +} diff --git a/secant/UI Components/CheckCircle/CheckCircleStore.swift b/secant/UI Components/CheckCircle/CheckCircleStore.swift new file mode 100644 index 0000000..c25d573 --- /dev/null +++ b/secant/UI Components/CheckCircle/CheckCircleStore.swift @@ -0,0 +1,54 @@ +// +// CheckCircleStore.swift +// secant-testnet +// +// Created by Michal Fousek on 26.09.2022. +// + +import Foundation +import ComposableArchitecture +import SwiftUI + +typealias CheckCircleReducer = Reducer +typealias CheckCircleStore = Store +typealias CheckCircleViewStore = ViewStore + +// MARK: - Action + +enum CheckCircleAction: Equatable { + case updateIsChecked +} + +// MARK: - Reducer + +extension CheckCircleReducer { + static let `default` = CheckCircleReducer { state, action, _ in + switch action { + case .updateIsChecked: + state.toggle() + return .none + } + } +} + +// MARK: - Store + +extension CheckCircleStore { + static func mock(isChecked: Bool) -> CheckCircleStore { + return CheckCircleStore( + initialState: isChecked, + reducer: .default, + environment: Void() + ) + } +} + +// MARK: - ViewStore + +extension CheckCircleViewStore { + static let placeholder = CheckCircleStore( + initialState: true, + reducer: .default, + environment: Void() + ) +} diff --git a/secant/UI Components/Shapes/Wedge.swift b/secant/UI Components/Shapes/Wedge.swift new file mode 100644 index 0000000..ae2a809 --- /dev/null +++ b/secant/UI Components/Shapes/Wedge.swift @@ -0,0 +1,39 @@ +// +// Wedge.swift +// secant-testnet +// +// Created by Michal Fousek on 24.09.2022. +// + +import SwiftUI + +struct Wedge: Shape { + var startAngle: CGFloat + var endAngle: CGFloat + var clockwise = true + + var animatableData: AnimatablePair { + get { AnimatablePair(startAngle, endAngle) } + set { + startAngle = newValue.first + endAngle = newValue.second + } + } + + func path(in rect: CGRect) -> Path { + let callback: (inout Path) -> Void = { path in + path.addArc( + center: CGPoint( + x: rect.midX, + y: rect.midY + ), + radius: rect.width / 2 , + startAngle: Angle(degrees: Double(startAngle)), + endAngle: Angle(degrees: Double(endAngle)), + clockwise: clockwise + ) + } + + return Path(callback) + } +} diff --git a/secant/UI Components/TextFields/MultiLineTextField/MultiLineTextFieldStore.swift b/secant/UI Components/TextFields/MultiLineTextField/MultiLineTextFieldStore.swift index e7c26b8..c8f9c77 100644 --- a/secant/UI Components/TextFields/MultiLineTextField/MultiLineTextFieldStore.swift +++ b/secant/UI Components/TextFields/MultiLineTextField/MultiLineTextFieldStore.swift @@ -86,5 +86,9 @@ extension MultiLineTextFieldStore { // MARK: - Placeholders extension MultiLineTextFieldState { - static let placeholder = MultiLineTextFieldState() + static let placeholder: MultiLineTextFieldState = { + var state = MultiLineTextFieldState() + state.text = "test" + return state + }() } diff --git a/secant/Wrappers/WrappedSDKSynchronizer.swift b/secant/Wrappers/WrappedSDKSynchronizer.swift index 8898e80..8331853 100644 --- a/secant/Wrappers/WrappedSDKSynchronizer.swift +++ b/secant/Wrappers/WrappedSDKSynchronizer.swift @@ -405,7 +405,7 @@ class MockWrappedSDKSynchronizer: WrappedSDKSynchronizer { ) -> Effect, Never> { let transactionState = TransactionState( expirationHeight: 40, - memo: "test", + memo: memo, minedHeight: 50, shielded: true, zAddress: "tteafadlamnelkqe", diff --git a/secantTests/SendTests/SendTests.swift b/secantTests/SendTests/SendTests.swift index 246af87..318cbff 100644 --- a/secantTests/SendTests/SendTests.swift +++ b/secantTests/SendTests/SendTests.swift @@ -84,6 +84,67 @@ class SendTests: XCTestCase { state.route = .success } } + + func testSendSucceededWithoutMemo() throws { + // the test needs to pass the exportWallet() so we simulate some in the keychain + try storage.importWallet(bip39: "one two three", birthday: nil) + + // setup the store and environment to be fully mocked + let testScheduler = DispatchQueue.test + + let testEnvironment = SendFlowEnvironment( + derivationTool: .live(), + mnemonic: .mock, + numberFormatter: .live(), + SDKSynchronizer: MockWrappedSDKSynchronizer(), + scheduler: testScheduler.eraseToAnyScheduler(), + walletStorage: .live(walletStorage: storage), + zcashSDKEnvironment: .testnet + ) + + var state = SendFlowState.placeholder + state.addMemoState = false + + let store = TestStore( + initialState: state, + reducer: SendFlowReducer.default, + environment: testEnvironment + ) + + // simulate the sending confirmation button to be pressed + store.send(.sendConfirmationPressed) { state in + // once sending is confirmed, the attemts to try to send again by pressing the button + // needs to be eliminated, indicated by the flag `isSendingTransaction`, need to be true + state.isSendingTransaction = true + } + + testScheduler.advance(by: 0.01) + + let transactionState = TransactionState( + expirationHeight: 40, + memo: nil, + minedHeight: 50, + shielded: true, + zAddress: "tteafadlamnelkqe", + fee: Zatoshi(10), + id: "id", + status: .paid(success: true), + timestamp: 1234567, + zecAmount: Zatoshi(10) + ) + + // check the success transaction to be received back + store.receive(.sendTransactionResult(Result.success(transactionState))) { state in + // from this moment on the sending next transaction is allowed again + // the 'isSendingTransaction' needs to be false again + state.isSendingTransaction = false + } + + // all went well, the success screen is triggered + store.receive(.updateRoute(.success)) { state in + state.route = .success + } + } func testSendFailed() throws { // the test needs to pass the exportWallet() so we simulate some in the keychain @@ -235,6 +296,7 @@ class SendTests: XCTestCase { func testFundsSufficiency() throws { let sendState = SendFlowState( + addMemoState: true, memoState: .placeholder, transactionAddressInputState: .placeholder, transactionAmountInputState: @@ -313,6 +375,7 @@ class SendTests: XCTestCase { let store = TestStore( initialState: .init( + addMemoState: true, memoState: .placeholder, route: nil, transactionAddressInputState: .placeholder, @@ -346,6 +409,7 @@ class SendTests: XCTestCase { func testValidForm() throws { let sendState = SendFlowState( + addMemoState: true, memoState: .placeholder, transactionAddressInputState: .placeholder, transactionAmountInputState: @@ -394,6 +458,7 @@ class SendTests: XCTestCase { func testInvalidForm_InsufficientFunds() throws { let sendState = SendFlowState( + addMemoState: true, memoState: .placeholder, transactionAddressInputState: .placeholder, transactionAmountInputState: @@ -441,6 +506,7 @@ class SendTests: XCTestCase { func testInvalidForm_AddressFormat() throws { let sendState = SendFlowState( + addMemoState: true, memoState: .placeholder, transactionAddressInputState: .placeholder, transactionAmountInputState: @@ -488,6 +554,7 @@ class SendTests: XCTestCase { func testInvalidForm_AmountFormat() throws { let sendState = SendFlowState( + addMemoState: true, memoState: .placeholder, transactionAddressInputState: .placeholder, transactionAmountInputState: @@ -535,6 +602,7 @@ class SendTests: XCTestCase { func testInvalidForm_ExceededMemoCharLimit() throws { let sendState = SendFlowState( + addMemoState: true, memoState: MultiLineTextFieldState(charLimit: 3), shieldedBalance: WalletBalance(verified: Zatoshi(1), total: Zatoshi(1)), transactionAddressInputState: @@ -588,6 +656,7 @@ class SendTests: XCTestCase { func testMemoCharLimitSet() throws { let sendState = SendFlowState( + addMemoState: true, memoState: .placeholder, transactionAddressInputState: .placeholder, transactionAmountInputState: diff --git a/secantTests/SnapshotTests/SendSnapshotTests/TransactionConfirmationSnapshotTests.swift b/secantTests/SnapshotTests/SendSnapshotTests/TransactionConfirmationSnapshotTests.swift new file mode 100644 index 0000000..275bf69 --- /dev/null +++ b/secantTests/SnapshotTests/SendSnapshotTests/TransactionConfirmationSnapshotTests.swift @@ -0,0 +1,88 @@ +// +// TransactionConfirmationSnapshotTests.swift +// secantTests +// +// Created by Michal Fousek on 26.09.2022. +// + +import XCTest +@testable import secant_testnet +import ComposableArchitecture +import SwiftUI +import ZcashLightClientKit + +class TransactionConfirmationSnapshotTests: XCTestCase { + func testTransactionConfirmationSnapshot_addMemo() throws { + let testEnvironment = SendFlowEnvironment( + derivationTool: .live(derivationTool: DerivationTool(networkType: .testnet)), + mnemonic: .mock, + numberFormatter: .live(), + SDKSynchronizer: MockWrappedSDKSynchronizer(), + scheduler: DispatchQueue.main.eraseToAnyScheduler(), + walletStorage: .live(), + zcashSDKEnvironment: .testnet + ) + + var state = SendFlowState.placeholder + state.addMemoState = true + state.transactionAddressInputState = TransactionAddressTextFieldState( + textFieldState: TCATextFieldState( + validationType: nil, + text: "ztestmockeddestinationaddress" + ) + ) + state.transactionAmountInputState = TransactionAmountTextFieldState( + currencySelectionState: CurrencySelectionState(), + textFieldState: TCATextFieldState( + validationType: nil, + text: "2.91" + ) + ) + + let store = Store( + initialState: state, + reducer: SendFlowReducer.default, + environment: testEnvironment + ) + + ViewStore(store).send(.onAppear) + addAttachments(TransactionConfirmation(store: store)) + } + + func testTransactionConfirmationSnapshot_dontAddMemo() throws { + let testEnvironment = SendFlowEnvironment( + derivationTool: .live(derivationTool: DerivationTool(networkType: .testnet)), + mnemonic: .mock, + numberFormatter: .live(), + SDKSynchronizer: MockWrappedSDKSynchronizer(), + scheduler: DispatchQueue.main.eraseToAnyScheduler(), + walletStorage: .live(), + zcashSDKEnvironment: .testnet + ) + + var state = SendFlowState.placeholder + state.addMemoState = true + state.transactionAddressInputState = TransactionAddressTextFieldState( + textFieldState: TCATextFieldState( + validationType: nil, + text: "ztestmockeddestinationaddress" + ) + ) + state.transactionAmountInputState = TransactionAmountTextFieldState( + currencySelectionState: CurrencySelectionState(), + textFieldState: TCATextFieldState( + validationType: nil, + text: "2.91" + ) + ) + + let store = Store( + initialState: state, + reducer: SendFlowReducer.default, + environment: testEnvironment + ) + + ViewStore(store).send(.onAppear) + addAttachments(TransactionConfirmation(store: store)) + } +}