diff --git a/secant.xcodeproj/project.pbxproj b/secant.xcodeproj/project.pbxproj index 494fe1b..922264f 100644 --- a/secant.xcodeproj/project.pbxproj +++ b/secant.xcodeproj/project.pbxproj @@ -98,7 +98,12 @@ F96B41E9273B501F0021B49A /* TransactionHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F96B41E6273B501F0021B49A /* TransactionHistoryView.swift */; }; F96B41EB273B50520021B49A /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = F96B41EA273B50520021B49A /* Strings.swift */; }; F9C165B4274031F600592F76 /* Bindings.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C165B3274031F600592F76 /* Bindings.swift */; }; + F9C165BF2740403600592F76 /* SendStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C165B72740403600592F76 /* SendStore.swift */; }; + F9C165C02740403600592F76 /* ApproveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C165B92740403600592F76 /* ApproveView.swift */; }; + F9C165C22740403600592F76 /* CreateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C165BB2740403600592F76 /* CreateView.swift */; }; + F9C165C42740403600592F76 /* SentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C165BD2740403600592F76 /* SentView.swift */; }; F9EEB8162742C2210032EEB8 /* WithStateBinding.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9EEB8152742C2210032EEB8 /* WithStateBinding.swift */; }; + F9C165CB2741AB5D00592F76 /* SendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C165CA2741AB5D00592F76 /* SendView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -215,7 +220,12 @@ F96B41E6273B501F0021B49A /* TransactionHistoryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionHistoryView.swift; sourceTree = ""; }; F96B41EA273B50520021B49A /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; F9C165B3274031F600592F76 /* Bindings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bindings.swift; sourceTree = ""; }; + F9C165B72740403600592F76 /* SendStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendStore.swift; sourceTree = ""; }; + F9C165B92740403600592F76 /* ApproveView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApproveView.swift; sourceTree = ""; }; + F9C165BB2740403600592F76 /* CreateView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateView.swift; sourceTree = ""; }; + F9C165BD2740403600592F76 /* SentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentView.swift; sourceTree = ""; }; F9EEB8152742C2210032EEB8 /* WithStateBinding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WithStateBinding.swift; sourceTree = ""; }; + F9C165CA2741AB5D00592F76 /* SendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -607,6 +617,7 @@ isa = PBXGroup; children = ( F93874EC273C4DE200F0E875 /* Home */, + F9C165B62740403600592F76 /* Send */, F96B41E2273B501F0021B49A /* TransactionHistory */, 6654C73C2715A3FA00901167 /* Onboarding */, ); @@ -699,6 +710,26 @@ path = Views; sourceTree = ""; }; + F9C165B62740403600592F76 /* Send */ = { + isa = PBXGroup; + children = ( + F9C165B72740403600592F76 /* SendStore.swift */, + F9C165B82740403600592F76 /* Views */, + ); + path = Send; + sourceTree = ""; + }; + F9C165B82740403600592F76 /* Views */ = { + isa = PBXGroup; + children = ( + F9C165CA2741AB5D00592F76 /* SendView.swift */, + F9C165BB2740403600592F76 /* CreateView.swift */, + F9C165B92740403600592F76 /* ApproveView.swift */, + F9C165BD2740403600592F76 /* SentView.swift */, + ); + path = Views; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -939,12 +970,14 @@ 0DA13C9726C186FF00E3B610 /* RestoreWalletScreen.swift in Sources */, 0DACFA8127208D940039EEA5 /* UInt+SuperscriptText.swift in Sources */, 0DF2DC51272344E400FA31E2 /* EmptyChip.swift in Sources */, + F9C165BF2740403600592F76 /* SendStore.swift in Sources */, 0D1922EA26BDD96A00052649 /* ViewModel.swift in Sources */, 0D4E7A0926B364170058B01E /* SecantApp.swift in Sources */, 66DC733F271D88CC0053CBB6 /* StandardButtonStyle.swift in Sources */, 0D864A0926E154FD00A61879 /* InitFailedScreen.swift in Sources */, 663FABA0271D876200E495F8 /* PrimaryButton.swift in Sources */, 663FAB9C271D874D00E495F8 /* ActiveButton.swift in Sources */, + F9C165C02740403600592F76 /* ApproveView.swift in Sources */, 0DF2DC5427235E3E00FA31E2 /* View+InnerShadow.swift in Sources */, 0D32281A26C5864B00262533 /* ProfileScreenViewModel.swift in Sources */, 0D185819272723FF0046B928 /* BlueChip.swift in Sources */, @@ -954,6 +987,7 @@ 66A0807B271993C500118B79 /* OnboardingProgressIndicator.swift in Sources */, 663FAB9E271D875700E495F8 /* CreateButton.swift in Sources */, 0D7DF08C271DCC0E00530046 /* ScreenBackground.swift in Sources */, + F9C165C22740403600592F76 /* CreateView.swift in Sources */, 0DA13C8F26C15D1D00E3B610 /* WelcomeScreen.swift in Sources */, F9C165B4274031F600592F76 /* Bindings.swift in Sources */, 0D32282826C586E000262533 /* RequestZcashScreen.swift in Sources */, @@ -963,8 +997,10 @@ 0DA13CA226C1955600E3B610 /* HomeScreenViewModel.swift in Sources */, 0D32282926C586E000262533 /* RequestZcashScreenViewModel.swift in Sources */, 0D32281926C5864B00262533 /* ProfileScreen.swift in Sources */, + F9C165CB2741AB5D00592F76 /* SendView.swift in Sources */, 6654C7412715A47300901167 /* Onboarding.swift in Sources */, 0D32282426C586A800262533 /* HistoryScreenViewModel.swift in Sources */, + F9C165C42740403600592F76 /* SentView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/secant/Features/Home/HomeStore.swift b/secant/Features/Home/HomeStore.swift index b961a9c..66b8388 100644 --- a/secant/Features/Home/HomeStore.swift +++ b/secant/Features/Home/HomeStore.swift @@ -89,4 +89,13 @@ extension HomeViewStore { } ) } + + var showSendBinding: Binding { + self.binding( + get: { $0.route == .send }, + send: { isActive in + return .updateRoute(isActive ? .send : nil) + } + ) + } } diff --git a/secant/Features/Home/Views/HomeView.swift b/secant/Features/Home/Views/HomeView.swift index a173c1e..4fb68b2 100644 --- a/secant/Features/Home/Views/HomeView.swift +++ b/secant/Features/Home/Views/HomeView.swift @@ -21,6 +21,13 @@ struct HomeView: View { .primaryButtonStyle .frame(height: 50) + Button( + action: { viewStore.send(.updateRoute(.send)) }, + label: { Text("Go to Send") } + ) + .primaryButtonStyle + .frame(height: 50) + Spacer() HStack { @@ -36,6 +43,24 @@ struct HomeView: View { } .padding(.horizontal, 30) .navigationBarTitle("Home", displayMode: .inline) + .navigationLinkEmpty( + isActive: viewStore.showSendBinding, + destination: { + SendView( + store: .init( + initialState: .init( + transaction: .demo, + route: nil + ), + reducer: SendReducer.default( + whenDone: { viewStore.send(.updateRoute(nil)) } + ) + .debug(), + environment: () + ) + ) + } + ) .fullScreenCover( isPresented: viewStore.showHistoryBinding, content: { diff --git a/secant/Features/Send/SendStore.swift b/secant/Features/Send/SendStore.swift new file mode 100644 index 0000000..c6e7b47 --- /dev/null +++ b/secant/Features/Send/SendStore.swift @@ -0,0 +1,89 @@ +import SwiftUI +import ComposableArchitecture + +struct SendState: Equatable { + var transaction: Transaction + var route: SendView.Route? +} + +enum SendAction: Equatable { + case updateTransaction(Transaction) + case updateRoute(SendView.Route?) +} + +// Mark: - SendReducer + +typealias SendReducer = Reducer + +extension SendReducer { + private struct SyncStatusUpdatesID: Hashable {} + + static let `default` = Reducer { state, action, _ in + switch action { + case let .updateTransaction(transaction): + state.transaction = transaction + return .none + case let .updateRoute(route): + state.route = route + return .none + } + } + + static func `default`(whenDone: @escaping () -> Void) -> SendReducer { + SendReducer { state, action, _ in + switch action { + case let .updateRoute(route) where route == .done: + return Effect.fireAndForget(whenDone) + default: + return Self.default.run(&state, action, ()) + } + } + } +} + +// Mark: - SendStore + +typealias SendStore = Store + +// Mark: - SendViewStore + +typealias SendViewStore = ViewStore + +extension SendViewStore { + + var bindingForTransaction: Binding { + self.binding( + get: \.transaction, + send: SendAction.updateTransaction + ) + } + + var routeBinding: Binding { + self.binding( + get: \.route, + send: SendAction.updateRoute + ) + } + + var bindingForApprove: Binding { + self.routeBinding.map( + extract: { $0 == .showApprove || self.bindingForSent.wrappedValue }, + embed: { $0 ? SendView.Route.showApprove : nil } + ) + } + + var bindingForSent: Binding { + self.routeBinding.map( + extract: { $0 == .showSent || self.bindingForDone.wrappedValue }, + embed: { $0 ? SendView.Route.showSent : SendView.Route.showApprove } + ) + } + + var bindingForDone: Binding { + self.routeBinding.map( + extract: { $0 == .done }, + embed: { $0 ? SendView.Route.done : SendView.Route.showSent } + ) + } +} + diff --git a/secant/Features/Send/Views/ApproveView.swift b/secant/Features/Send/Views/ApproveView.swift new file mode 100644 index 0000000..852df4e --- /dev/null +++ b/secant/Features/Send/Views/ApproveView.swift @@ -0,0 +1,38 @@ +import SwiftUI +import ComposableArchitecture + +struct Approve: View { + let transaction: Transaction + @Binding var isComplete: Bool + + var body: some View { + VStack { + Button( + action: { isComplete = true }, + label: { Text("Go to sent") } + ) + .primaryButtonStyle + .frame(height: 50) + .padding() + + Text("\(String(dumping: transaction))") + Text("\(String(dumping: isComplete))") + + Spacer() + } + .navigationTitle(Text("2. Approve")) + } +} + +struct Approve_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + StateContainer(initialState: (Transaction.demo, false)) { + Approve( + transaction: $0.0.wrappedValue, + isComplete: $0.1 + ) + } + } + } +} diff --git a/secant/Features/Send/Views/CreateView.swift b/secant/Features/Send/Views/CreateView.swift new file mode 100644 index 0000000..9e0c704 --- /dev/null +++ b/secant/Features/Send/Views/CreateView.swift @@ -0,0 +1,73 @@ +import SwiftUI +import ComposableArchitecture + +struct Create: View { + @Binding var transaction: Transaction + @Binding var isComplete: Bool + + var body: some View { + VStack { + Button( + action: { isComplete = true }, + label: { Text("Go To Approve") } + ) + .primaryButtonStyle + .frame(height: 50) + .padding() + + TextField( + "Amount", + text: $transaction + .amount + .compactMap( + extract: String.init, + embed: UInt.init + ) + ) + .padding() + + TextField( + "Address", + text: $transaction.toAddress + ) + + Text("\(String(dumping: transaction))") + Text("\(String(dumping: isComplete))") + + Spacer() + } + .padding() + .navigationTitle(Text("1. Create")) + } +} + +// MARK: - Previews + +struct Create_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + StateContainer(initialState: (Transaction.demo, false)) { + Create( + transaction: $0.0, + isComplete: $0.1 + ) + } + .navigationBarTitleDisplayMode(.inline) + } + } +} + +#if DEBUG +extension SendStore { + static var demo: SendStore { + return SendStore( + initialState: .init( + transaction: .demo, + route: nil + ), + reducer: .default, + environment: () + ) + } +} +#endif diff --git a/secant/Features/Send/Views/SendView.swift b/secant/Features/Send/Views/SendView.swift new file mode 100644 index 0000000..72c6a35 --- /dev/null +++ b/secant/Features/Send/Views/SendView.swift @@ -0,0 +1,58 @@ +import SwiftUI +import ComposableArchitecture + +struct SendView: View { + enum Route: Equatable { + case showApprove + case showSent + case done + } + + let store: Store + + var body: some View { + WithViewStore(store) { viewStore in + Create( + transaction: viewStore.bindingForTransaction, + isComplete: viewStore.bindingForApprove + ) + .navigationLinkEmpty( + isActive: viewStore.bindingForApprove, + destination: { + Approve( + transaction: viewStore.transaction, + isComplete: viewStore.bindingForSent + ) + .navigationLinkEmpty( + isActive: viewStore.bindingForSent, + destination: { + Sent( + transaction: viewStore.transaction, + isComplete: viewStore.bindingForDone + ) + } + ) + } + ) + } + } +} + +struct SendView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + SendView( + store: .init( + initialState: .init( + transaction: .demo, + route: nil + ), + reducer: .default, + environment: () + ) + ) + .navigationBarTitleDisplayMode(.inline) + .navigationViewStyle(StackNavigationViewStyle()) + } + } +} diff --git a/secant/Features/Send/Views/SentView.swift b/secant/Features/Send/Views/SentView.swift new file mode 100644 index 0000000..81aa417 --- /dev/null +++ b/secant/Features/Send/Views/SentView.swift @@ -0,0 +1,34 @@ +import SwiftUI +import ComposableArchitecture + +struct Sent: View { + var transaction: Transaction + @Binding var isComplete: Bool + + var body: some View { + VStack { + Button( + action: { + isComplete = true + }, + label: { Text("Done") } + ) + .primaryButtonStyle + .frame(height: 50) + .padding() + + + Text("\(String(dumping: transaction))") + Text("\(String(dumping: isComplete))") + + Spacer() + } + .navigationTitle(Text("3. Sent")) + } +} + +struct Done_Previews: PreviewProvider { + static var previews: some View { + Sent(transaction: .demo, isComplete: .constant(false)) + } +} diff --git a/secant/SecantApp.swift b/secant/SecantApp.swift index dda3f07..2b44a08 100644 --- a/secant/SecantApp.swift +++ b/secant/SecantApp.swift @@ -15,6 +15,7 @@ struct SecantApp: App { NavigationView { HomeView(store: homeStore) } + .navigationViewStyle(StackNavigationViewStyle()) } } } diff --git a/secant/Util/Bindings.swift b/secant/Util/Bindings.swift index a72b88b..847ae24 100644 --- a/secant/Util/Bindings.swift +++ b/secant/Util/Bindings.swift @@ -74,7 +74,14 @@ extension Binding { ) } - func map(extract: @escaping (Value) -> T, embed: @escaping (T) -> Value?) -> Binding { + func map(extract: @escaping (Value) -> T, embed: @escaping (T) -> Value) -> Binding { + Binding( + get: { extract(wrappedValue) }, + set: { wrappedValue = embed($0) } + ) + } + + func compactMap(extract: @escaping (Value) -> T, embed: @escaping (T) -> Value?) -> Binding { Binding( get: { extract(wrappedValue) }, set: {