gh-EmergeTools-Pow/Example/Pow Example/Examples/Screens/CheckoutExample.swift
2023-12-26 17:31:16 -08:00

277 lines
9.3 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Pow
import SwiftUI
import SnapshotPreferences
struct CheckoutExample: View, Example {
enum PaymentError: Error {
case unknown
}
@State
var result: Result<Void, Error>?
@State
var quantity = 1
var body: some View {
List {
if case .success = result {
VStack(alignment: .leading) {
Text("Thank You For Your Order")
.font(.title2)
.bold()
Text("We'll notify you when your order has been sent.")
.font(.title3)
.foregroundStyle(.secondary)
}
.listRowSeparator(.hidden)
Spacer()
LabeledContent("Purchase Number", value: "P023121114")
} else {
VStack(alignment: .leading) {
Text("Checkout")
.font(.largeTitle)
.bold()
.accessibility(addTraits: .isHeader)
Text(quantity > 0 ? "1 Item" : "No Items")
.font(.title2)
.foregroundStyle(.secondary)
}
.listRowSeparator(.hidden)
Spacer()
Section {
if quantity > 0 {
CartItem(quantity: $quantity)
}
}
}
Spacer()
Section {
LabeledContent("Address", value: "jane.doe@example.com")
LabeledContent("Payment", value: "VISA")
}
}
.listStyle(.plain)
.navigationTitle("")
.navigationBarTitleDisplayMode(.inline)
.animation(.default, value: quantity != 0)
.toolbar {
if quantity == 0 {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Undo") {
quantity = 1
}
}
}
}
.changeEffect(.feedback(SoundEffect("whop")), value: quantity == 0, isEnabled: quantity == 0)
.changeEffect(.feedback(SoundEffect("wip")), value: quantity != 0, isEnabled: quantity != 0)
.safeAreaInset(edge: .bottom, spacing: 0) {
VStack(spacing: 16) {
VStack(alignment: .leading, spacing: 0) {
LabeledContent("Subtotal", value: 99 * quantity, format: .currency(code: "USD"))
.foregroundStyle(.secondary)
LabeledContent("Shipping", value: 0, format: .currency(code: "USD"))
.foregroundStyle(.secondary)
LabeledContent("Total", value: 99 * quantity, format: .currency(code: "USD"))
.fontWeight(.heavy)
}
PayButton {
let isFirstPayAttempt = result == nil
try? await Task.sleep(nanoseconds: 1_000_000_000)
if isFirstPayAttempt {
throw PaymentError.unknown
}
} completion: { payResult in
withAnimation {
result = payResult
}
}
.disabled(quantity == 0)
.changeEffect(.shine.delay(1), value: quantity != 0, isEnabled: quantity != 0)
}
.padding()
.background(.bar)
}
.labeledContentStyle(CheckoutLabeledContentStyle())
}
static let localPath = LocalPath()
static var icon: Image? {
Image(systemName: "cart")
}
static let newIn0_2_0: Bool = true
}
private struct CartItem: View {
@Binding
var quantity: Int
@State
var lastQuantity: Int = 0
var body: some View {
HStack {
Stepper("Quantity", value: $quantity, in: 0...99)
.labelsHidden()
.alignmentGuide(.listRowSeparatorLeading) { dimensions in
dimensions[.leading]
}
.changeEffect(.feedback(SoundEffect("beep")), value: quantity, isEnabled: quantity > lastQuantity && quantity > 1)
.changeEffect(.feedback(SoundEffect("boop")), value: quantity, isEnabled: quantity < lastQuantity && quantity > 0)
.onChange(of: quantity) { newValue in
lastQuantity = quantity
}
Text(quantity, format: .number).monospacedDigit() + Text("×")
LabeledContent("Pow License", value: 99, format: .currency(code: "USD"))
}
}
}
struct PayButton: View {
var action: () async throws -> Void
var completion: (Result<Void, Error>) -> Void
enum Status {
case initial
case inProgress
case succeeded
case failed
}
@State
var status: Status = .initial
var body: some View {
Button {
status = .inProgress
Task {
do {
try await action()
status = .succeeded
completion(.success(()))
} catch {
status = .failed
try? await Task.sleep(nanoseconds: 1_500_000_000)
status = .initial
completion(.failure(error))
}
}
} label: {
HStack(spacing: 12) {
ZStack {
ProgressView()
.controlSize(.regular)
.tint(.white)
.opacity(status == .inProgress ? 1 : 0)
.animation(.spring(), value: status == .inProgress)
Image(systemName: "exclamationmark.triangle")
.opacity(status == .failed ? 1 : 0)
.animation(.spring(), value: status == .failed)
Checkmark()
.trim(from: 0, to: status == .succeeded ? 1 : 0)
.stroke(style: .init(lineWidth: 3, lineCap: .round, lineJoin: .round))
.padding(4)
.animation(.spring(response: 0.3), value: status == .succeeded)
}
.frame(width: 20, height: 20)
.imageScale(.large)
Spacer()
switch status {
case .initial:
Text("Pay")
case .inProgress:
Text("Paying…")
case .succeeded:
Text("Paid")
case .failed:
Text("Try Again")
}
Color.clear
.frame(width: 20, height: 20)
Spacer()
}
}
.font(.headline)
.buttonStyle(.borderedProminent)
.transformEnvironment(\.backgroundMaterial, transform: { material in
material = nil
})
.controlSize(.large)
.animation(.spring(response: 0.3), value: status == .inProgress)
.tint(status == .failed ? .red : status == .succeeded ? .green : nil)
.allowsHitTesting(status == .initial)
.changeEffect(.shake(rate: .fast), value: status == .failed, isEnabled: status == .failed)
.changeEffect(.feedback(SoundEffect("plop")), value: status == .inProgress, isEnabled: status == .inProgress)
.changeEffect(.feedback(SoundEffect("sparkle")), value: status == .succeeded, isEnabled: status == .succeeded)
.changeEffect(.feedback(SoundEffect("notfound")), value: status == .failed, isEnabled: status == .failed)
}
}
private struct Checkmark: Shape {
func path(in rect: CGRect) -> Path {
let insetFrame = rect
let referenceSize = CGSize(width: 67, height: 68)
let referencePoint1: CGPoint
let referencePoint2: CGPoint
let referencePoint3: CGPoint
referencePoint1 = CGPoint(x: 3.5, y: 36.5)
referencePoint2 = CGPoint(x: 25.5, y: 63.5)
referencePoint3 = CGPoint(x: 63, y: 5.5)
return Path { path in
path.move(to: CGPoint(x: insetFrame.width * referencePoint1.x / referenceSize.width, y: insetFrame.height * referencePoint1.y / referenceSize.height))
path.addLine(to: CGPoint(x: insetFrame.width * referencePoint2.x / referenceSize.width, y: insetFrame.width * referencePoint2.y / referenceSize.height))
path.addLine(to: CGPoint(x: insetFrame.width * referencePoint3.x / referenceSize.width, y: insetFrame.width * referencePoint3.y / referenceSize.height))
}
.offsetBy(dx: insetFrame.origin.x, dy: insetFrame.origin.y)
}
}
private struct CheckoutLabeledContentStyle: LabeledContentStyle {
func makeBody(configuration: Configuration) -> some View {
HStack(alignment: .firstTextBaseline) {
configuration.label
.font(.headline)
Spacer()
configuration.content
.font(.body)
.multilineTextAlignment(.trailing)
}
.padding(.vertical, 8)
.contentShape(Rectangle())
}
}
struct CheckoutExample_Previews: PreviewProvider {
static var previews: some View {
NavigationStack {
CheckoutExample()
.toolbar(.visible, for: .navigationBar)
}
.emergeSnapshotPrecision(0.99)
}
}