Step 6: sell and request screen scaffold

This commit is contained in:
Francisco Gindre 2021-02-24 22:44:38 -03:00
parent 614d3ea5e8
commit c4bea8c3f1
10 changed files with 354 additions and 103 deletions

View File

@ -260,3 +260,88 @@ struct HomeScreen: View {
}
````
## Tag: `step-6-Sell-Screen`
We are going to create a sell screen where we will generate a QR code with our address and some instructions for our customer to send us the requested ZEC along some memo.
We are going to move some other elements to the SettingsView, and Create a form in the `SellScreen` view where we will request some amount and enter an order code, that will be shown on the request screen so that the user types that code in the transaction memo.
````
/// SellScreen.swift
var body: some View {
NavigationView {
ZStack {
ZcashBackground()
VStack(alignment: .center, spacing: 20) {
ZcashLogo(width: 50)
Spacer()
ZcashTextField(title: "Zec Amount To Request",
subtitleView: amountSubtitle,
contentType: nil,
keyboardType: .numberPad,
binding: $numberString,
action: nil,
accessoryIcon: nil,
onEditingChanged: { _ in }, onCommit: {})
ZcashTextField(title: "Order Code",
subtitleView: codeSubtitle,
binding: $orderCode) { _ in } onCommit: {}
````
We are going to create the Request Screen. It will allow the user to see the information needed to create the transaction
````
struct RequestZec: View {
@EnvironmentObject var model: ZcashPoSModel
@State var zAddress: String? = nil
@State var alertType: AlertType? = nil
var body: some View {
ZStack {
ZcashBackground()
VStack(alignment: .center, spacing: 40){
Text("To This address:")
.foregroundColor(.white)
Text(zAddress ?? "Error Deriving Address")
.foregroundColor(.white)
Text("$\(model.request.amount.toZecAmount())")
.lineLimit(1)
.minimumScaleFactor(0.5)
.foregroundColor(.white)
.font(
.custom("Zboto", size: 72)
)
Text("Append Memo With this Code")
.foregroundColor(.white)
Text(model.request.code)
.foregroundColor(.white)
.font(.title)
}
}.navigationTitle("Pay with ZEC")
````
The interesting part is this: We can derive a Z-Address from viewing key!
We can do this and many more things with the `DerivationTool` class of the Zcash SDK.
````
.onAppear() {
do {
guard let ivk = model.viewingKey else {
self.alertType = .errorAlert(ZcashPoSModel.PoSError.unableToRetrieveCredentials)
return
}
self.zAddress = try DerivationTool.default.deriveShieldedAddress(viewingKey: ivk)
} catch {
self.alertType = .errorAlert(error)
}
}
````
Unfortunately this screen is really helpful. we need to get some QR code so that the user can scan the address! We will see that on the next step

View File

@ -13,6 +13,8 @@
0D08DE7325E6FE4B00E08533 /* SellScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D08DE7225E6FE4B00E08533 /* SellScreen.swift */; };
0D08DE7625E7077F00E08533 /* ReceivedTransactions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D08DE7525E7077F00E08533 /* ReceivedTransactions.swift */; };
0D08DEAA25E70D8400E08533 /* SettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D08DEA925E70D8400E08533 /* SettingsScreen.swift */; };
0D08DEAD25E71D4500E08533 /* RequestZec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D08DEAC25E71D4500E08533 /* RequestZec.swift */; };
0D08DEB025E732C900E08533 /* Zboto.otf in Resources */ = {isa = PBXBuildFile; fileRef = 0D08DEAF25E732C900E08533 /* Zboto.otf */; };
0D475B2125E3ED600067978E /* CombineSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D475B2025E3ED600067978E /* CombineSynchronizer.swift */; };
0D475B2725E3EEE10067978E /* DetailModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D475B2625E3EEE10067978E /* DetailModel.swift */; };
0D475B2A25E3F04C0067978E /* String+Zcash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D475B2925E3F04C0067978E /* String+Zcash.swift */; };
@ -47,6 +49,8 @@
0D08DE7225E6FE4B00E08533 /* SellScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SellScreen.swift; sourceTree = "<group>"; };
0D08DE7525E7077F00E08533 /* ReceivedTransactions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceivedTransactions.swift; sourceTree = "<group>"; };
0D08DEA925E70D8400E08533 /* SettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreen.swift; sourceTree = "<group>"; };
0D08DEAC25E71D4500E08533 /* RequestZec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestZec.swift; sourceTree = "<group>"; };
0D08DEAF25E732C900E08533 /* Zboto.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Zboto.otf; sourceTree = "<group>"; };
0D475B2025E3ED600067978E /* CombineSynchronizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CombineSynchronizer.swift; sourceTree = "<group>"; };
0D475B2625E3EEE10067978E /* DetailModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailModel.swift; sourceTree = "<group>"; };
0D475B2925E3F04C0067978E /* String+Zcash.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Zcash.swift"; sourceTree = "<group>"; };
@ -116,6 +120,7 @@
0D08DE7225E6FE4B00E08533 /* SellScreen.swift */,
0D08DE7525E7077F00E08533 /* ReceivedTransactions.swift */,
0D08DEA925E70D8400E08533 /* SettingsScreen.swift */,
0D08DEAC25E71D4500E08533 /* RequestZec.swift */,
);
path = Views;
sourceTree = "<group>";
@ -141,6 +146,7 @@
0D5050A625DFEFAC00E7A697 /* accept-zcash-poc */ = {
isa = PBXGroup;
children = (
0D08DEAF25E732C900E08533 /* Zboto.otf */,
0D08DE6825E6C29600E08533 /* sapling-output.params */,
0D08DE6925E6C29700E08533 /* sapling-spend.params */,
0D475B2525E3EEBE0067978E /* Model */,
@ -273,6 +279,7 @@
0D08DE6A25E6C29700E08533 /* sapling-output.params in Resources */,
0D5050AF25DFEFAD00E7A697 /* Preview Assets.xcassets in Resources */,
0D5050AC25DFEFAD00E7A697 /* Assets.xcassets in Resources */,
0D08DEB025E732C900E08533 /* Zboto.otf in Resources */,
0D08DE6B25E6C29700E08533 /* sapling-spend.params in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -332,6 +339,7 @@
0D5051C125E0797C00E7A697 /* ZcashButtonBackground.swift in Sources */,
0DC0819725E53E690028532F /* HomeScreen.swift in Sources */,
0D475B3325E3F4910067978E /* ZcashEnvironment.swift in Sources */,
0D08DEAD25E71D4500E08533 /* RequestZec.swift in Sources */,
0D50519A25E0591B00E7A697 /* ZcashBackground.swift in Sources */,
0D5051B625E076BA00E7A697 /* KeyboardResponder.swift in Sources */,
0D08DE7625E7077F00E08533 /* ReceivedTransactions.swift in Sources */,

View File

@ -46,5 +46,9 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIAppFonts</key>
<array>
<string>Zboto.otf</string>
</array>
</dict>
</plist>

View File

@ -10,9 +10,6 @@ import SwiftUI
struct ZcashLogo: View {
var fillGradient: LinearGradient {
LinearGradient(gradient: Gradient(
colors: [Color.zAmberGradient1,
@ -22,25 +19,18 @@ struct ZcashLogo: View {
endPoint: UnitPoint(x: 0.5, y: 1.0))
}
var width: CGFloat = 100
var body: some View {
ZStack {
Ring()
.stroke(lineWidth: 14)
.fill(fillGradient)
.frame(width: 167, height: 167, alignment: .center)
.padding(20)
VStack (alignment: .center) {
ZcashSymbol()
.fill(fillGradient)
.frame(width: 100, height: 105, alignment: .center)
}
}
ZcashSymbol()
.fill(fillGradient)
.frame(width: width, height: width * 1.05, alignment: .center)
.padding(width * 0.3)
.overlay( Ring()
.stroke(lineWidth: width * 0.14)
.fill(fillGradient)
)
}
}

View File

@ -15,20 +15,23 @@ struct HomeScreen: View {
var body: some View {
TabView {
TabView(selection: $model.tabSelection) {
SellScreen()
.tabItem {
Label("Sell", systemImage: "shield")
}
.tag(AppNavigation.Tab.sell)
ReceivedTransactions()
.tabItem {
Label("History", systemImage: "square.and.pencil")
}
.tag(AppNavigation.Tab.received)
SettingsScreen()
.tabItem {
Label("Settings", systemImage: "list.dash")
}
.tag(AppNavigation.Tab.settings)
}

View File

@ -0,0 +1,71 @@
//
// RequestZec.swift
// accept-zcash-poc
//
// Created by Francisco Gindre on 2/24/21.
//
import SwiftUI
import ZcashLightClientKit
struct RequestZec: View {
@EnvironmentObject var model: ZcashPoSModel
@State var zAddress: String? = nil
@State var alertType: AlertType? = nil
var body: some View {
ZStack {
ZcashBackground()
VStack(alignment: .center, spacing: 40){
Text("To This address:")
.foregroundColor(.white)
Text(zAddress ?? "Error Deriving Address")
.foregroundColor(.white)
Text("$\(model.request.amount.toZecAmount())")
.lineLimit(1)
.minimumScaleFactor(0.5)
.foregroundColor(.white)
.font(
.custom("Zboto", size: 72)
)
Text("Append Memo With this Code")
.foregroundColor(.white)
Text(model.request.code)
.foregroundColor(.white)
.font(.title)
}
}.navigationTitle("Pay with ZEC")
.onAppear() {
do {
guard let ivk = model.viewingKey else {
self.alertType = .errorAlert(ZcashPoSModel.PoSError.unableToRetrieveCredentials)
return
}
self.zAddress = try DerivationTool.default.deriveShieldedAddress(viewingKey: ivk)
} catch {
self.alertType = .errorAlert(error)
}
}
.alert(item: $alertType) { t in
t.buildAlert()
}
}
}
struct RequestZec_Previews: PreviewProvider {
static var previews: some View {
RequestZec()
}
}
extension ZcashPoSModel {
var request: ZECRequest {
self.currentPayment ?? ZECRequest.nullRequest
}
}
extension ZcashPoSModel.ZECRequest {
static var nullRequest: Self {
Self(amount: 0, code: "NO CODE")
}
}

View File

@ -9,75 +9,106 @@ import SwiftUI
import ZcashLightClientKit
struct SellScreen: View {
enum Status {
case ready
case syncing
case offline
}
@EnvironmentObject var model: ZcashPoSModel
@Environment(\.zcashEnvironment) var zcash: ZcashEnvironment
@State var alertType: AlertType? = nil
@State var status: Status = .offline
@State var progress: Int = 0
@State var height: BlockHeight = ZcashSDK.SAPLING_ACTIVATION_HEIGHT
@State var numberString: String = ""
@State var orderCode: String = ""
@State var navigation: AppNavigation.Screen? = nil
var body: some View {
ZStack {
ZcashBackground()
VStack(alignment: .center, spacing: 20) {
ZcashLogo()
switch status {
case .offline:
Button(action: {
start()
}) {
Text("Offline - Tap to Start").foregroundColor(.white)
NavigationView {
ZStack {
ZcashBackground()
VStack(alignment: .center, spacing: 20) {
ZcashLogo(width: 50)
Spacer()
ZcashTextField(title: "Zec Amount To Request",
subtitleView: amountSubtitle,
contentType: nil,
keyboardType: .numberPad,
binding: $numberString,
action: nil,
accessoryIcon: nil,
onEditingChanged: { _ in }, onCommit: {})
ZcashTextField(title: "Order Code",
subtitleView: codeSubtitle,
binding: $orderCode) { _ in } onCommit: {}
NavigationLink(destination: AppNavigation.Screen.request.buildScreen().environmentObject(model), tag: AppNavigation.Screen.request, selection: $model.navigation) {
Button(action: {
guard let amount = NumberFormatter.zecAmountFormatter.number(from: numberString)?.doubleValue,
validOrderCode else {
self.alertType = AlertType.message("Invalid values!")
return
}
model.currentPayment = ZcashPoSModel.ZECRequest(amount: amount, code: orderCode)
model.navigation = .request
}) {
Text("Request ZEC")
.foregroundColor(.black)
.zcashButtonBackground(shape: .rounded(fillStyle: .gradient(gradient: .zButtonGradient)))
.frame(height: 48)
}
.disabled(!validForm)
.opacity(validForm ? 1.0 : 0.6)
}
case .ready:
Text("Ready! Yay!").foregroundColor(.white)
case .syncing:
Text("Syncing \(progress)% Block: \(height)").foregroundColor(.white)
}
.padding(20)
}
.onReceive(zcash.synchronizer.progress) { (p) in
self.progress = Int(p * 100)
.keyboardAdaptive()
.navigationBarTitle(Text("Zcash PoS"), displayMode: .inline)
.navigationBarHidden(false)
.onAppear() {
_zECCWalletNavigationBarLookTweaks()
}
.onReceive(zcash.synchronizer.syncBlockHeight) { (h) in
self.height = h
}
}.onReceive(zcash.synchronizer.status) { (s) in
switch s {
case .disconnected, .stopped:
self.status = .offline
case .synced:
self.status = .ready
case .syncing:
self.status = .syncing
.alert(item: $alertType) { (type) -> Alert in
type.buildAlert()
}
}
.navigationBarTitle(Text("Zcash PoS"), displayMode: .inline)
.navigationBarHidden(false)
.onAppear() {
_zECCWalletNavigationBarLookTweaks()
start()
}
.alert(item: $alertType) { (type) -> Alert in
type.buildAlert()
}
}
func start() {
do {
guard let ivk = model.viewingKey, let bday = model.birthday else {
throw ZcashPoSModel.PoSError.unableToRetrieveCredentials
}
try self.zcash.synchronizer.initializer.initialize(viewingKeys: [ivk], walletBirthday: bday)
try self.zcash.synchronizer.start()
} catch {
self.alertType = AlertType.errorAlert(error)
}
var amountSubtitle: AnyView {
AnyView(
Text(validAmount ? "This is a valid amount" : "Invalid Zec amount")
.foregroundColor(.white)
.font(.caption)
)
}
var codeSubtitle: AnyView {
AnyView(
Text(validOrderCode ? "Valid Order code" : "Please enter an order code")
.foregroundColor(.white)
.font(.caption)
)
}
var validAmount: Bool {
guard let amount = NumberFormatter.zecAmountFormatter.number(from: numberString)?.doubleValue else {
return false
}
return amount > 0
}
var validOrderCode: Bool {
!orderCode.isEmpty && orderCode.count < 6
}
var validForm: Bool {
guard validAmount,
validOrderCode else {
return false
}
return true
}
}
struct SellScreen_Previews: PreviewProvider {

View File

@ -6,18 +6,36 @@
//
import SwiftUI
import ZcashLightClientKit
struct SettingsScreen: View {
enum Status {
case ready
case syncing
case offline
}
@EnvironmentObject var model: ZcashPoSModel
@Environment(\.zcashEnvironment) var zcash: ZcashEnvironment
@State var status: Status = .offline
@State var progress: Int = 0
@State var height: BlockHeight = ZcashSDK.SAPLING_ACTIVATION_HEIGHT
@State var alertType: AlertType? = nil
var body: some View {
ZStack {
ZcashBackground()
VStack {
ZcashLogo()
Text("This is where the settings would be")
.foregroundColor(.white)
switch status {
case .offline:
Button(action: {
start()
}) {
Text("Offline - Tap to Start").foregroundColor(.white)
}
case .ready:
Text("Ready! Yay!").foregroundColor(.white)
case .syncing:
Text("Syncing \(progress)% Block: \(height)").foregroundColor(.white)
}
Button(action: {
zcash.synchronizer.stop()
model.nuke()
@ -30,8 +48,34 @@ struct SettingsScreen: View {
}
}
}
.onReceive(zcash.synchronizer.progress) { (p) in
self.progress = Int(p * 100)
}
.onReceive(zcash.synchronizer.syncBlockHeight) { (h) in
self.height = h
}.onReceive(zcash.synchronizer.status) { (s) in
switch s {
case .disconnected, .stopped:
self.status = .offline
case .synced:
self.status = .ready
case .syncing:
self.status = .syncing
}
}
}
func start() {
do {
guard let ivk = model.viewingKey, let bday = model.birthday else {
throw ZcashPoSModel.PoSError.unableToRetrieveCredentials
}
try self.zcash.synchronizer.initializer.initialize(viewingKeys: [ivk], walletBirthday: bday)
try self.zcash.synchronizer.start()
} catch {
self.alertType = AlertType.errorAlert(error)
}
}
}
struct SettingsScreen_Previews: PreviewProvider {

BIN
accept-zcash-poc/Zboto.otf Normal file

Binary file not shown.

View File

@ -16,14 +16,12 @@ struct accept_zcash_pocApp: App {
var body: some Scene {
WindowGroup {
// we can navigate now!
NavigationView {
// and we need to check what our main screen will be. Is it an empty or an already initialized app?
model.initialScreen()
.environmentObject(model)
.zcashEnvironment(ZcashEnvironment.default)
}
//we need to check what our main screen will be. Is it an empty or an already initialized app?
model.initialScreen()
.environmentObject(model)
.zcashEnvironment(ZcashEnvironment.default)
}
}
}
@ -36,10 +34,17 @@ class ZcashPoSModel: ObservableObject {
case failedToStart(error: Error)
case unableToRetrieveCredentials
}
@Published var tabSelection: AppNavigation.Tab = .history
@Published var status = AppNavigation.AppStatus.current
struct ZECRequest {
var amount: Double
var code: String
}
@Published var tabSelection: AppNavigation.Tab = .sell
@Published var status = AppNavigation.AppStatus.empty
@Published var navigation: AppNavigation.Screen? = nil
@Published var currentPayment: ZECRequest? = nil
@AppStorage("viewingKey") var viewingKey: String?
@AppStorage("walletBirthday") var birthday: BlockHeight?
@ -50,11 +55,13 @@ class ZcashPoSModel: ObservableObject {
return .initialized
}
@ViewBuilder func initialScreen() -> some View{
@ViewBuilder func initialScreen() -> some View{
switch appStatus {
case .empty:
ImportViewingKey()
NavigationView {
ImportViewingKey()
}
case .initialized:
HomeScreen()
}
@ -71,27 +78,35 @@ struct AppNavigation {
enum AppStatus {
case empty
case initialized
static var current: AppStatus {
.empty
}
}
enum Tab {
case receive
case history
case sell
case received
case settings
}
enum Screen {
case importViewingKey
case home
case receivedTransaction
case settings
case sell
case request
@ViewBuilder func buildScreen() -> some View {
switch self {
case .importViewingKey:
ImportViewingKey()
ImportViewingKey()
case .home:
HomeScreen()
HomeScreen()
case .receivedTransaction:
ReceivedTransactions()
case .settings:
SettingsScreen()
case .request:
RequestZec()
case .sell:
SellScreen()
}
}
}