[#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.
This commit is contained in:
Michal Fousek 2022-09-27 19:14:52 +02:00 committed by GitHub
parent 92289bb036
commit d807f86d49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 897 additions and 65 deletions

View File

@ -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 = "<group>"; };
2EDA07A127EDE1AE00D6F09B /* TextFieldFooter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldFooter.swift; sourceTree = "<group>"; };
2EDA07A327EDE2A900D6F09B /* DebugFrame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugFrame.swift; sourceTree = "<group>"; };
346715A428E2027D0035F7C4 /* CheckCircleStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckCircleStore.swift; sourceTree = "<group>"; };
346715A728E20FE40035F7C4 /* TransactionConfirmationSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionConfirmationSnapshotTests.swift; sourceTree = "<group>"; };
346D41E328DF0B8600963F36 /* CheckCircle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckCircle.swift; sourceTree = "<group>"; };
34E0AF0E28DEE4C70034CF37 /* HoldToSendButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoldToSendButton.swift; sourceTree = "<group>"; };
34E0AF1028DEE5220034CF37 /* Wedge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wedge.swift; sourceTree = "<group>"; };
660558E8270C7A54009D6954 /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = "<group>"; };
660558F5270C862F009D6954 /* Fonts+Generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Fonts+Generated.swift"; sourceTree = "<group>"; };
660558F6270C862F009D6954 /* XCAssets+Generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "XCAssets+Generated.swift"; sourceTree = "<group>"; };
@ -488,6 +498,7 @@
0D0781C5278776B90083ACD7 /* Shapes */ = {
isa = PBXGroup;
children = (
34E0AF1028DEE5220034CF37 /* Wedge.swift */,
0D0781C7278776D20083ACD7 /* ZcashSymbol.swift */,
);
path = Shapes;
@ -707,6 +718,23 @@
path = Components;
sourceTree = "<group>";
};
346715A628E20FB30035F7C4 /* SendSnapshotTests */ = {
isa = PBXGroup;
children = (
346715A728E20FE40035F7C4 /* TransactionConfirmationSnapshotTests.swift */,
);
path = SendSnapshotTests;
sourceTree = "<group>";
};
346D41E228DF0B0900963F36 /* CheckCircle */ = {
isa = PBXGroup;
children = (
346D41E328DF0B8600963F36 /* CheckCircle.swift */,
346715A428E2027D0035F7C4 /* CheckCircleStore.swift */,
);
path = CheckCircle;
sourceTree = "<group>";
};
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 = "<group>";
@ -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 = "<group>";
@ -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" */ = {

View File

@ -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"
}
}
],

View File

@ -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(),

View File

@ -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,

View File

@ -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()
}
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x65",
"green" : "0x65",
"red" : "0x65"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x65",
"green" : "0x65",
"red" : "0x65"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x00",
"green" : "0x94",
"red" : "0xFF"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x00",
"green" : "0x94",
"red" : "0xFF"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x00",
"green" : "0x94",
"red" : "0xFF"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x00",
"green" : "0x94",
"red" : "0xFF"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x1E",
"green" : "0x1B",
"red" : "0x19"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x1E",
"green" : "0x1B",
"red" : "0x19"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x3D",
"green" : "0x38",
"red" : "0x33"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x3D",
"green" : "0x38",
"red" : "0x33"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -23,9 +23,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x45",
"green" : "0x1F",
"red" : "0x13"
"blue" : "69",
"green" : "31",
"red" : "19"
}
},
"idiom" : "universal"

View File

@ -23,9 +23,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x00",
"green" : "0x00",
"red" : "0x00"
"blue" : "0",
"green" : "0",
"red" : "0"
}
},
"idiom" : "universal"

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x97",
"green" : "0x97",
"red" : "0x97"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x97",
"green" : "0x97",
"red" : "0x97"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "0.200",
"blue" : "0x36",
"green" : "0x2C",
"red" : "0x27"
"blue" : "54",
"green" : "44",
"red" : "39"
}
},
"idiom" : "universal"
@ -23,9 +23,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "0.250",
"blue" : "0x00",
"green" : "0x00",
"red" : "0x00"
"blue" : "0",
"green" : "0",
"red" : "0"
}
},
"idiom" : "universal"

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0xFF",
"red" : "0xFF"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0xFF",
"red" : "0xFF"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "0.827",
"blue" : "0.623",
"green" : "0.589",
"red" : "0.560"
"blue" : "0x9E",
"green" : "0x96",
"red" : "0x8E"
}
},
"idiom" : "universal"
@ -23,9 +23,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.290",
"green" : "0.275",
"red" : "0.259"
"blue" : "0x49",
"green" : "0x46",
"red" : "0x42"
}
},
"idiom" : "universal"

View File

@ -35,6 +35,7 @@ internal enum Asset {
}
internal enum Icons {
internal static let bank = ImageAsset(name: "bank")
internal static let checkmark = ImageAsset(name: "checkmark")
internal static let list = ImageAsset(name: "list")
internal static let profile = ImageAsset(name: "profile")
internal static let qrCode = ImageAsset(name: "qr-code")
@ -67,6 +68,10 @@ internal enum Asset {
internal static let secondaryButton = ColorAsset(name: "SecondaryButton")
internal static let secondaryButtonPressed = ColorAsset(name: "SecondaryButtonPressed")
}
internal enum CheckCircle {
internal static let externalRing = ColorAsset(name: "externalRing")
internal static let internalRing = ColorAsset(name: "internalRing")
}
internal enum Cursor {
internal static let bar = ColorAsset(name: "Bar")
}
@ -86,11 +91,14 @@ internal enum Asset {
internal static let gradientLeft = ColorAsset(name: "GradientLeft")
internal static let gradientRight = ColorAsset(name: "GradientRight")
internal static let negativeSpace = ColorAsset(name: "NegativeSpace")
internal static let holdToSendButton = ColorAsset(name: "holdToSendButton")
}
internal enum QRScan {
internal static let frame = ColorAsset(name: "frame")
}
internal enum ScreenBackground {
internal static let gradientDarkEnd = ColorAsset(name: "gradientDarkEnd")
internal static let gradientDarkStart = ColorAsset(name: "gradientDarkStart")
internal static let gradientEnd = ColorAsset(name: "gradientEnd")
internal static let gradientStart = ColorAsset(name: "gradientStart")
internal static let greenGradientEnd = ColorAsset(name: "greenGradientEnd")
@ -104,6 +112,7 @@ internal enum Asset {
internal enum Shadow {
internal static let drawerShadow = ColorAsset(name: "drawerShadow")
internal static let emptyChipInnerShadow = ColorAsset(name: "emptyChipInnerShadow")
internal static let holdToSendButtonShadow = ColorAsset(name: "holdToSendButtonShadow")
internal static let numberedTextShadow = ColorAsset(name: "numberedTextShadow")
}
internal enum Text {
@ -124,6 +133,7 @@ internal enum Asset {
internal static let balanceText = ColorAsset(name: "balanceText")
internal static let captionText = ColorAsset(name: "captionText")
internal static let captionTextShadow = ColorAsset(name: "captionTextShadow")
internal static let forDarkBackground = ColorAsset(name: "forDarkBackground")
internal static let highlightedSuperscriptText = ColorAsset(name: "highlightedSuperscriptText")
internal static let moreInfoText = ColorAsset(name: "moreInfoText")
}

View File

@ -32,6 +32,7 @@ extension ScreenBackground {
]
)
}
struct ScreenBackgroundModifier: ViewModifier {
var colors: [Color]
var darkGradientEndPointY = 1.0
@ -97,6 +98,17 @@ extension View {
)
)
}
func applyDarkScreenBackground() -> some View {
self.modifier(
ScreenBackgroundModifier(
colors: [
Asset.Colors.ScreenBackground.gradientDarkStart.color,
Asset.Colors.ScreenBackground.gradientDarkEnd.color
]
)
)
}
}
struct ScreenBackground_Previews: PreviewProvider {

View File

@ -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()
}
}

View File

@ -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<Bool, CheckCircleAction, Void>
typealias CheckCircleStore = Store<Bool, CheckCircleAction>
typealias CheckCircleViewStore = ViewStore<Bool, CheckCircleAction>
// 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()
)
}

View File

@ -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<CGFloat, CGFloat> {
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)
}
}

View File

@ -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
}()
}

View File

@ -405,7 +405,7 @@ class MockWrappedSDKSynchronizer: WrappedSDKSynchronizer {
) -> Effect<Result<TransactionState, NSError>, Never> {
let transactionState = TransactionState(
expirationHeight: 40,
memo: "test",
memo: memo,
minedHeight: 50,
shielded: true,
zAddress: "tteafadlamnelkqe",

View File

@ -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:

View File

@ -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))
}
}