Skip to content

Commit e95d40c

Browse files
feat(giftcard): refactor confirmation and details sheet flow
1 parent d64385a commit e95d40c

20 files changed

Lines changed: 2492 additions & 1350 deletions

DashWallet.xcodeproj/project.pbxproj

Lines changed: 418 additions & 350 deletions
Large diffs are not rendered by default.
Lines changed: 188 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//
1+
//
22
// Created by Andrei Ashikhmin
33
// Copyright © 2025 Dash Core Group. All rights reserved.
44
//
@@ -19,143 +19,205 @@ import SwiftUI
1919
import SDWebImageSwiftUI
2020

2121
struct DashSpendConfirmationDialog: View {
22-
let amount: String
2322
let merchantName: String
2423
let merchantIconUrl: String
2524
let originalPrice: Decimal
2625
let discount: Decimal
26+
let quantities: [Decimal: Int]?
2727
let onConfirm: () -> Void
2828
let onCancel: () -> Void
29-
30-
@Environment(\.presentationMode) private var presentationMode
31-
29+
@Binding var contentHeight: CGFloat
30+
3231
private let fiatFormatter = NumberFormatter.fiatFormatter(currencyCode: kDefaultCurrencyCode)
33-
34-
var body: some View {
35-
VStack(spacing: 40) {
36-
HStack {
37-
Text(fiatFormatter.currencySymbol + amount)
38-
.font(.system(size: 32, weight: .medium))
39-
.foregroundColor(.primaryText)
32+
private var youPayAmount: Decimal { originalPrice * (1 - discount) }
33+
34+
private var formattedPayAmount: String {
35+
let hasCents = youPayAmount != Decimal(Int(truncating: NSDecimalNumber(decimal: youPayAmount)))
36+
let formatter = NumberFormatter.fiatFormatter(currencyCode: kDefaultCurrencyCode)
37+
if !hasCents {
38+
formatter.minimumFractionDigits = 0
39+
formatter.maximumFractionDigits = 0
40+
}
41+
return formatter.string(from: NSDecimalNumber(decimal: youPayAmount))?.strippingCurrencySymbol(formatter) ?? ""
42+
}
43+
44+
private var quantityLines: [String] {
45+
guard let quantities = quantities else { return [] }
46+
return quantities
47+
.filter { $0.value > 0 }
48+
.sorted { $0.key < $1.key }
49+
.map { denomination, count in
50+
let amount = fiatFormatter.string(from: NSDecimalNumber(decimal: denomination)) ?? "$\(denomination)"
51+
return "\(count) x \(amount)"
4052
}
41-
42-
// Details
43-
VStack(spacing: 0) {
44-
HStack(spacing: 8) {
45-
Text(NSLocalizedString("From", comment: "DashSpend"))
46-
.font(.subhead)
47-
.fontWeight(.medium)
48-
.foregroundColor(.tertiaryText)
49-
50-
Spacer()
51-
52-
Image("image.explore.dash.wts.dash")
53-
.resizable()
54-
.frame(width: 24, height: 24)
55-
56-
Text(NSLocalizedString("Dash Wallet", comment: "DashSpend"))
57-
.font(.subhead)
58-
.foregroundColor(.primaryText)
59-
}
60-
.padding(.horizontal, 12)
61-
.frame(height: 50)
62-
63-
HStack(spacing: 8) {
64-
Text(NSLocalizedString("To", comment: "DashSpend"))
65-
.font(.subhead)
66-
.fontWeight(.medium)
67-
.foregroundColor(.tertiaryText)
68-
69-
Spacer()
70-
71-
WebImage(url: URL(string: merchantIconUrl))
72-
.resizable()
73-
.indicator(.activity)
74-
.transition(.fade(duration: 0.3))
75-
.scaledToFit()
76-
.frame(width: 24, height: 24)
77-
.clipShape(Circle())
78-
79-
Text(merchantName)
80-
.font(.subhead)
81-
.foregroundColor(.primaryText)
82-
}
83-
.padding(.horizontal, 12)
84-
.frame(height: 50)
85-
86-
HStack {
87-
Text(NSLocalizedString("Gift card total", comment: "DashSpend"))
88-
.font(.subhead)
89-
.fontWeight(.medium)
90-
.foregroundColor(.tertiaryText)
91-
92-
Spacer()
93-
94-
Text(fiatFormatter.string(from: NSDecimalNumber(decimal: originalPrice)) ?? "")
95-
.font(.subhead)
96-
.foregroundColor(.primaryText)
97-
}
98-
.padding(.horizontal, 12)
99-
.frame(height: 50)
100-
101-
HStack {
102-
Text(NSLocalizedString("Discount", comment: "DashSpend"))
103-
.font(.subhead)
104-
.fontWeight(.medium)
105-
.foregroundColor(.tertiaryText)
106-
107-
Spacer()
108-
109-
Text(PercentageFormatter.format(percent: NSDecimalNumber(decimal: discount * 100).doubleValue))
110-
.font(.subhead)
111-
.foregroundColor(.primaryText)
53+
}
54+
55+
var body: some View {
56+
VStack(spacing: 0) {
57+
Capsule()
58+
.fill(Color(red: 0.69, green: 0.71, blue: 0.74).opacity(0.5))
59+
.frame(width: 36, height: 5)
60+
.padding(.top, 6)
61+
.padding(.bottom, 13)
62+
63+
Text(NSLocalizedString("Confirm", comment: "DashSpend"))
64+
.font(.calloutMedium)
65+
.foregroundColor(.primaryText)
66+
.frame(height: 44)
67+
68+
VStack(spacing: 20) {
69+
DashSpendAmountView(
70+
currencySymbol: fiatFormatter.currencySymbol,
71+
amount: formattedPayAmount
72+
)
73+
.frame(height: 85)
74+
75+
VStack(alignment: .leading, spacing: 2) {
76+
detailsRow(title: NSLocalizedString("From", comment: "DashSpend")) {
77+
HStack(spacing: 8) {
78+
Image("image.explore.dash.wts.dash")
79+
.resizable()
80+
.frame(width: 20, height: 20)
81+
.clipShape(RoundedRectangle(cornerRadius: 7))
82+
Text(NSLocalizedString("Dash Wallet", comment: "DashSpend"))
83+
.font(.subhead)
84+
.foregroundColor(.primaryText)
85+
}
86+
}
87+
88+
detailsRow(title: NSLocalizedString("To", comment: "DashSpend")) {
89+
HStack(spacing: 8) {
90+
WebImage(url: URL(string: merchantIconUrl))
91+
.resizable()
92+
.indicator(.activity)
93+
.transition(.fade(duration: 0.3))
94+
.scaledToFit()
95+
.frame(width: 20, height: 20)
96+
.clipShape(RoundedRectangle(cornerRadius: 7))
97+
Text(merchantName)
98+
.font(.subhead)
99+
.foregroundColor(.primaryText)
100+
}
101+
}
102+
103+
detailsRow(title: NSLocalizedString("Gift card", comment: "DashSpend")) {
104+
Text(fiatFormatter.string(from: NSDecimalNumber(decimal: originalPrice)) ?? "")
105+
.font(.subhead)
106+
.foregroundColor(.primaryText)
107+
}
108+
109+
if !quantityLines.isEmpty {
110+
detailsRow(title: NSLocalizedString("Quantity", comment: "DashSpend")) {
111+
VStack(alignment: .trailing, spacing: 2) {
112+
ForEach(quantityLines, id: \.self) { line in
113+
Text(line)
114+
.font(.subhead)
115+
.foregroundColor(.primaryText)
116+
}
117+
}
118+
}
119+
}
120+
121+
detailsRow(title: NSLocalizedString("Discount", comment: "DashSpend")) {
122+
Text(PercentageFormatter.format(percent: NSDecimalNumber(decimal: discount * 100).doubleValue))
123+
.font(.subhead)
124+
.foregroundColor(.primaryText)
125+
}
126+
127+
detailsRow(title: NSLocalizedString("You pay", comment: "DashSpend")) {
128+
Text(fiatFormatter.string(from: NSDecimalNumber(decimal: youPayAmount)) ?? "")
129+
.font(.subhead)
130+
.foregroundColor(.primaryText)
131+
}
112132
}
113-
.padding(.horizontal, 12)
114-
.frame(height: 50)
115-
116-
HStack {
117-
Text(NSLocalizedString("You pay", comment: "DashSpend"))
118-
.font(.subhead)
119-
.fontWeight(.medium)
120-
.foregroundColor(.tertiaryText)
121-
122-
Spacer()
123-
124-
Text(fiatFormatter.string(from: NSDecimalNumber(decimal: originalPrice * (1 - discount))) ?? "")
125-
.font(.subhead)
126-
.foregroundColor(.primaryText)
133+
.padding(6)
134+
.background(Color.secondaryBackground)
135+
.cornerRadius(20)
136+
.shadow(color: Color(red: 0.72, green: 0.76, blue: 0.8).opacity(0.1), radius: 20, x: 0, y: 5)
137+
138+
HStack(spacing: 20) {
139+
DashButton(
140+
text: NSLocalizedString("Cancel", comment: "DashSpend"),
141+
action: onCancel
142+
)
143+
.overrideForegroundColor(.primaryText)
144+
.overrideBackgroundColor(.gray300Alpha10)
145+
146+
DashButton(
147+
text: NSLocalizedString("Confirm", comment: "DashSpend"),
148+
action: onConfirm
149+
)
127150
}
128-
.padding(.horizontal, 12)
129-
.frame(height: 50)
151+
.padding(.horizontal, 10)
130152
}
131-
.background(Color.secondaryBackground)
132-
.cornerRadius(12)
133-
.shadow(color: Color.shadow, radius: 10, x: 0, y: 5)
134-
135-
HStack(spacing: 20) {
136-
Button(action: onCancel) {
137-
Text(NSLocalizedString("Cancel", comment: "DashSpend"))
138-
.font(.system(size: 16, weight: .semibold))
139-
.foregroundColor(.primaryText)
140-
.frame(maxWidth: .infinity)
141-
.frame(height: 46)
142-
}
143-
.background(Color(UIColor.systemGray5))
144-
.cornerRadius(12)
145-
146-
Button(action: onConfirm) {
147-
Text(NSLocalizedString("Confirm", comment: "DashSpend"))
148-
.font(.system(size: 16, weight: .semibold))
149-
.foregroundColor(.white)
150-
}
151-
.frame(maxWidth: .infinity)
152-
.frame(height: 46)
153-
.background(Color.dashBlue)
154-
.cornerRadius(12)
153+
.padding(.horizontal, 20)
154+
.padding(.bottom, 20)
155+
}
156+
.background(
157+
GeometryReader { proxy in
158+
Color.clear
159+
.onAppear { contentHeight = proxy.size.height }
160+
.onChange(of: proxy.size.height) { contentHeight = $0 }
161+
}
162+
)
163+
}
164+
165+
private func detailsRow(title: String, @ViewBuilder value: () -> some View) -> some View {
166+
HStack(alignment: .top) {
167+
Text(title)
168+
.font(.subhead)
169+
.fontWeight(.medium)
170+
.foregroundColor(.tertiaryText)
171+
172+
Spacer()
173+
174+
value()
175+
}
176+
.padding(.horizontal, 14)
177+
.frame(minHeight: 46)
178+
}
179+
}
180+
181+
private extension String {
182+
func strippingCurrencySymbol(_ formatter: NumberFormatter) -> String {
183+
replacingOccurrences(of: formatter.currencySymbol, with: "").trimmingCharacters(in: .whitespaces)
184+
}
185+
}
186+
187+
#Preview {
188+
DashSpendConfirmationDialogPreview()
189+
}
190+
191+
private struct DashSpendConfirmationDialogPreview: View {
192+
@State private var isPresented = true
193+
@State private var contentHeight: CGFloat = 0
194+
195+
var body: some View {
196+
VStack {
197+
Text("Tap to open")
198+
.onTapGesture { isPresented = true }
199+
}
200+
.sheet(isPresented: $isPresented) {
201+
let content = DashSpendConfirmationDialog(
202+
merchantName: "Amazon",
203+
merchantIconUrl: "",
204+
originalPrice: 75.70,
205+
discount: 0.10,
206+
quantities: [50: 1, 25: 2],
207+
onConfirm: {},
208+
onCancel: {},
209+
contentHeight: $contentHeight
210+
)
211+
212+
if #available(iOS 18.0, *) {
213+
content
214+
.presentationBackground(Color.primaryBackground)
215+
.presentationDetents([.height(contentHeight > 0 ? contentHeight : 550)])
216+
.presentationCornerRadius(32)
217+
.presentationDragIndicator(.hidden)
218+
} else {
219+
content
155220
}
156221
}
157-
.padding(.top, 15)
158-
.padding(.horizontal, 20)
159-
.edgesIgnoringSafeArea(.bottom)
160222
}
161223
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//
2+
// Created by Andrei Ashikhmin
3+
// Copyright © 2025 Dash Core Group. All rights reserved.
4+
//
5+
// Licensed under the MIT License (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// https://opensource.org/licenses/MIT
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
//
17+
18+
import SwiftUI
19+
20+
struct GiftCardDetailsHowToUseSection: View {
21+
var body: some View {
22+
VStack(alignment: .leading, spacing: 30) {
23+
Text(NSLocalizedString("How to use your gift card", comment: "DashSpend"))
24+
.font(.subhead)
25+
.fontWeight(.medium)
26+
.foregroundColor(.tertiaryText)
27+
.frame(maxWidth: .infinity, alignment: .leading)
28+
29+
FeatureSingleItem(
30+
iconName: .custom("dp_user_generic"),
31+
title: NSLocalizedString("Self-checkout", comment: "DashSpend"),
32+
description: NSLocalizedString("Request assistance and show the barcode on your screen for scanning.", comment: "DashSpend")
33+
)
34+
35+
FeatureSingleItem(
36+
iconName: .custom("image.dashspend.shop"),
37+
title: NSLocalizedString("In store", comment: "DashSpend"),
38+
description: NSLocalizedString("Tell the cashier that you'd like to pay with a gift card and share the card number and pin.", comment: "DashSpend")
39+
)
40+
41+
FeatureSingleItem(
42+
iconName: .custom("image.dashspend.online"),
43+
title: NSLocalizedString("Online", comment: "DashSpend"),
44+
description: NSLocalizedString("In the payment section of your checkout, select \"gift card\" and enter your card number and pin.", comment: "DashSpend")
45+
)
46+
}
47+
.padding(20)
48+
.background(Color.secondaryBackground)
49+
.cornerRadius(20)
50+
}
51+
}
52+
53+
#Preview {
54+
GiftCardDetailsHowToUseSection()
55+
.padding()
56+
.background(Color.primaryBackground)
57+
}

0 commit comments

Comments
 (0)