diff --git a/DashWallet.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DashWallet.xcworkspace/xcshareddata/swiftpm/Package.resolved index f21591312..bd6066580 100644 --- a/DashWallet.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DashWallet.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,15 +1,15 @@ { - "originHash" : "2b9d5280ec4645105debdfd588a911137d45333db7102b1ec79310afb20db9cc", - "pins" : [ + "originHash": "2b9d5280ec4645105debdfd588a911137d45333db7102b1ec79310afb20db9cc", + "pins": [ { - "identity" : "dashuikit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/dashpay/DashUIKit", - "state" : { - "branch" : "master", - "revision" : "5b373b141054438e94903af52b9ec324f1efbdb2" + "identity": "dashuikit", + "kind": "remoteSourceControl", + "location": "https://github.com/dashpay/DashUIKit", + "state": { + "branch": "master", + "revision": "83cf65a84834f6a7a1f0a82446ea1258ea86f37d" } } ], - "version" : 3 + "version": 3 } diff --git a/DashWallet/Sources/UI/Explore Dash/UsernameMarketplaceScreen.swift b/DashWallet/Sources/UI/Explore Dash/UsernameMarketplaceScreen.swift index 46534f7d9..806623f8b 100644 --- a/DashWallet/Sources/UI/Explore Dash/UsernameMarketplaceScreen.swift +++ b/DashWallet/Sources/UI/Explore Dash/UsernameMarketplaceScreen.swift @@ -480,7 +480,10 @@ struct UsernameMarketplaceScreen: View { } .sheet(item: $registerCandidate) { candidate in if candidate.isContested { - DashUIKit.BottomSheet(showBackButton: .constant(false)) { + DashUIKit.BottomSheet( + showBackButton: .constant(false), + isDismissalEnabled: .constant(!viewModel.isPerformingAction) + ) { RegisterNameSheet( label: candidate.label, isContested: candidate.isContested, @@ -490,6 +493,7 @@ struct UsernameMarketplaceScreen: View { } else { DashUIKit.BottomSheet.selfSizing( showBackButton: .constant(false), + isDismissalEnabled: .constant(!viewModel.isPerformingAction), fallback: 340, cornerRadius: 24 ) { @@ -1842,7 +1846,6 @@ private struct RegisterNameSheet: View { } } .background(Color.dash.primaryBackground) - .interactiveDismissDisabled(viewModel.isPerformingAction) .overlay { if let activity = viewModel.activityMessage { MarketplaceActivityOverlay(message: activity) diff --git a/DashWallet/Sources/UI/Home/Views/CoinJoinMoveFundsSheet.swift b/DashWallet/Sources/UI/Home/Views/CoinJoinMoveFundsSheet.swift index c446720a2..81525069c 100644 --- a/DashWallet/Sources/UI/Home/Views/CoinJoinMoveFundsSheet.swift +++ b/DashWallet/Sources/UI/Home/Views/CoinJoinMoveFundsSheet.swift @@ -182,16 +182,15 @@ struct CoinJoinMoveFundsSheet: View { } var body: some View { - VStack(spacing: 0) { - dragHandle - .padding(.top, 8) - - Text(NSLocalizedString("Move your mixed coins", comment: "CoinJoin")) - .font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(.dash.primaryText) - .padding(.top, 20) - + DashUIKit.BottomSheet( + title: NSLocalizedString("Move your mixed coins", comment: "CoinJoin"), + showBackButton: .constant(false), + isDismissalEnabled: .constant(!viewModel.isInFlight), + // Supplying `onClose` makes the close button live regardless of + // `isDismissalEnabled`, so the in-flight stages have to gate it here. + isCloseButtonEnabled: !viewModel.isInFlight, + onClose: onDismiss + ) { switch viewModel.stage { case .choice: choiceBody @@ -207,49 +206,55 @@ struct CoinJoinMoveFundsSheet: View { failedBody(message: message, destination: destination) } } - .background(Color.dash.primaryBackground) - .interactiveDismissDisabled(viewModel.isInFlight) } // MARK: Choice private var choiceBody: some View { VStack(spacing: 0) { - DashAmount( - amount: Int64(viewModel.amountDuffs), - font: .largeTitle, - dashSymbolFactor: 0.7, - showDirection: false) - .padding(.top, 14) - - Text(NSLocalizedString( - "CoinJoin is no longer supported — choose where to move your mixed coins.", - comment: "CoinJoin")) - .font(.system(size: 14)) - .foregroundColor(.dash.secondaryText) - .multilineTextAlignment(.center) - .padding(.horizontal, 24) - .padding(.top, 12) - - VStack(spacing: 10) { - destinationCard( - icon: "wallet.pass.fill", - title: NSLocalizedString("Dash Wallet balance", comment: "CoinJoin"), - subtitle: NSLocalizedString( - "Move to your regular spendable balance.", comment: "CoinJoin"), - action: { Task { await viewModel.moveToWallet() } }) - destinationCard( - icon: "shield.fill", - title: NSLocalizedString("Shielded balance", comment: "CoinJoin"), - subtitle: NSLocalizedString( - "Keep these coins private. Network and privacy fees apply.", - comment: "CoinJoin"), - action: { Task { await viewModel.moveToShielded() } }) + // Scrollable: this is the only migrated sheet whose initial detent is + // `.medium`, and the BottomSheet chrome takes 82pt of it. Letting the + // choices scroll keeps "Later" — the only way out that records the + // deferral — reachable on a short screen or a long translation. + ScrollView { + VStack(spacing: 0) { + DashAmount( + amount: Int64(viewModel.amountDuffs), + font: .largeTitle, + dashSymbolFactor: 0.7, + showDirection: false) + .padding(.top, 14) + + Text(NSLocalizedString( + "CoinJoin is no longer supported — choose where to move your mixed coins.", + comment: "CoinJoin")) + .font(.system(size: 14)) + .foregroundColor(.dash.secondaryText) + .multilineTextAlignment(.center) + .padding(.horizontal, 24) + .padding(.top, 12) + + VStack(spacing: 10) { + destinationCard( + icon: "wallet.pass.fill", + title: NSLocalizedString("Dash Wallet balance", comment: "CoinJoin"), + subtitle: NSLocalizedString( + "Move to your regular spendable balance.", comment: "CoinJoin"), + action: { Task { await viewModel.moveToWallet() } }) + destinationCard( + icon: "shield.fill", + title: NSLocalizedString("Shielded balance", comment: "CoinJoin"), + subtitle: NSLocalizedString( + "Keep these coins private. Network and privacy fees apply.", + comment: "CoinJoin"), + action: { Task { await viewModel.moveToShielded() } }) + } + .padding(.horizontal, 16) + .padding(.top, 20) + .padding(.bottom, 12) + } } - .padding(.horizontal, 16) - .padding(.top, 20) - - Spacer(minLength: 12) + .scrollBounceBehavior(.basedOnSize) DashButton( text: NSLocalizedString("Later", comment: "CoinJoin"), @@ -421,10 +426,4 @@ struct CoinJoinMoveFundsSheet: View { // MARK: Pieces - private var dragHandle: some View { - Rectangle() - .fill(Color.dash.grabberFill) - .frame(width: 36, height: 5) - .cornerRadius(2.5) - } } diff --git a/DashWallet/Sources/UI/Menu/Tools/Masternode Withdrawal/EvonodeWithdrawalScreen.swift b/DashWallet/Sources/UI/Menu/Tools/Masternode Withdrawal/EvonodeWithdrawalScreen.swift index 2b20348a8..552b5e4d7 100644 --- a/DashWallet/Sources/UI/Menu/Tools/Masternode Withdrawal/EvonodeWithdrawalScreen.swift +++ b/DashWallet/Sources/UI/Menu/Tools/Masternode Withdrawal/EvonodeWithdrawalScreen.swift @@ -348,19 +348,15 @@ struct EvonodeWithdrawalConfirmSheet: View { let onUnconfirmedAcknowledged: () -> Void var body: some View { - VStack(spacing: 0) { - Rectangle() - .fill(Color.dash.grabberFill) - .frame(width: 36, height: 5) - .cornerRadius(2.5) - .padding(.top, 8) - - Text(NSLocalizedString("Confirm withdrawal", comment: "Evonode withdrawal")) - .font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(.dash.primaryText) - .padding(.top, 20) - + DashUIKit.BottomSheet( + title: NSLocalizedString("Confirm withdrawal", comment: "Evonode withdrawal"), + showBackButton: .constant(false), + isDismissalEnabled: .constant(!(isInFlight || isUnconfirmed)), + // Supplying `onClose` makes the close button live regardless of + // `isDismissalEnabled`, so the protected phases have to gate it here. + isCloseButtonEnabled: !(isInFlight || isUnconfirmed), + onClose: closeAction + ) { switch viewModel.phase { case let .success(remaining): successBody(remainingCredits: remaining) @@ -370,8 +366,6 @@ struct EvonodeWithdrawalConfirmSheet: View { detailsBody } } - .background(Color.dash.primaryBackground) - .interactiveDismissDisabled(isInFlight || isUnconfirmed) } private var isUnconfirmed: Bool { @@ -379,6 +373,21 @@ struct EvonodeWithdrawalConfirmSheet: View { return false } + /// The close button has to do what the visible button of the current phase + /// does. `onCancel` only closes the sheet, so on success it would skip + /// `onWithdrawn(remaining)` and leave the withdrawal screen showing the + /// stale pre-withdrawal claimable balance. + private var closeAction: () -> Void { + switch viewModel.phase { + case let .success(remaining): + return { onCompleted(remaining) } + case .submittedUnconfirmed: + return onUnconfirmedAcknowledged + default: + return onCancel + } + } + private var isInFlight: Bool { switch viewModel.phase { case .authorizing, .submitting: return true diff --git a/DashWallet/Sources/UI/Payments/InternalTransfer/InternalTransferConfirmSheet.swift b/DashWallet/Sources/UI/Payments/InternalTransfer/InternalTransferConfirmSheet.swift index 53208c597..0722047f1 100644 --- a/DashWallet/Sources/UI/Payments/InternalTransfer/InternalTransferConfirmSheet.swift +++ b/DashWallet/Sources/UI/Payments/InternalTransfer/InternalTransferConfirmSheet.swift @@ -12,7 +12,7 @@ import SwiftDashSDK /// `ShieldedTransferCoordinator.phase`: /// - `.idle` → summary card + Cancel/Confirm buttons. /// - in-flight phases → step checklist (Signing / Locking / Proving / -/// Broadcasting). Drag-dismiss is disabled. +/// Broadcasting). Sheet dismissal is disabled. /// - `.success` → green check + amount + Done. /// - `.failed(msg)` → summary card with red error + Try again / Close. /// @@ -51,16 +51,15 @@ struct InternalTransferConfirmSheet: View { @State private var handledPlatformShieldCapacityChange = false var body: some View { - VStack(spacing: 0) { - dragHandle - .padding(.top, 8) - - Text(NSLocalizedString("Confirm", comment: "")) - .font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(.dash.primaryText) - .padding(.top, 20) - + DashUIKit.BottomSheet( + title: NSLocalizedString("Confirm", comment: ""), + showBackButton: .constant(false), + isDismissalEnabled: .constant(!isInFlight), + // Supplying `onClose` makes the close button live regardless of + // `isDismissalEnabled`, so the protected phases have to gate it here. + isCloseButtonEnabled: !isInFlight, + onClose: closeAction + ) { switch coordinator.phase { case .success: successBody @@ -70,8 +69,6 @@ struct InternalTransferConfirmSheet: View { detailsBody } } - .background(Color.dash.primaryBackground) - .interactiveDismissDisabled(isInFlight) .onChange(of: coordinator.phase) { phase in handlePlatformShieldCapacityChange(phase) } @@ -86,6 +83,18 @@ struct InternalTransferConfirmSheet: View { } } + /// The close button has to do what the visible button of the current phase + /// does. In the terminal phases that is `onCompleted`, which tells the + /// transfer screen the transfer finished; `onCancel` only closes the sheet. + private var closeAction: () -> Void { + switch coordinator.phase { + case .success, .submittedUnconfirmed: + return onCompleted + default: + return onCancel + } + } + // MARK: - Idle / in-flight / failed body private var detailsBody: some View { @@ -192,13 +201,6 @@ struct InternalTransferConfirmSheet: View { // MARK: - Pieces - private var dragHandle: some View { - Rectangle() - .fill(Color.dash.grabberFill) - .frame(width: 36, height: 5) - .cornerRadius(2.5) - } - private var secondaryLine: some View { Text(fiatText) .font(.subheadline) @@ -553,16 +555,15 @@ struct ShieldedRecoverySheet: View { @State private var alreadyComplete = false var body: some View { - VStack(spacing: 0) { - dragHandle - .padding(.top, 8) - - Text(NSLocalizedString("Finish shielded transfer", comment: "InternalTransfer recovery")) - .font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(.dash.primaryText) - .padding(.top, 20) - + DashUIKit.BottomSheet( + title: NSLocalizedString("Finish shielded transfer", comment: "InternalTransfer recovery"), + showBackButton: .constant(false), + isDismissalEnabled: .constant(!isInFlight), + // Supplying `onClose` makes the close button live regardless of + // `isDismissalEnabled`, so the protected phases have to gate it here. + isCloseButtonEnabled: !isInFlight, + onClose: onDismiss + ) { switch coordinator.phase { case .success: successBody @@ -578,8 +579,6 @@ struct ShieldedRecoverySheet: View { } } } - .background(Color.dash.primaryBackground) - .interactiveDismissDisabled(isInFlight) } private var isInFlight: Bool { @@ -692,13 +691,6 @@ struct ShieldedRecoverySheet: View { // MARK: - Pieces - private var dragHandle: some View { - Rectangle() - .fill(Color.dash.grabberFill) - .frame(width: 36, height: 5) - .cornerRadius(2.5) - } - private var infoCard: some View { HStack(alignment: .top, spacing: 12) { ZStack { diff --git a/DashWallet/Sources/UI/Payments/Pay/SendScreen.swift b/DashWallet/Sources/UI/Payments/Pay/SendScreen.swift index 55c315098..fcf52c406 100644 --- a/DashWallet/Sources/UI/Payments/Pay/SendScreen.swift +++ b/DashWallet/Sources/UI/Payments/Pay/SendScreen.swift @@ -726,16 +726,15 @@ struct SendConfirmSheet: View { @StateObject private var coordinator = ShieldedTransferCoordinator() var body: some View { - VStack(spacing: 0) { - dragHandle - .padding(.top, 8) - - Text(NSLocalizedString("Confirm", comment: "")) - .font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(.dash.primaryText) - .padding(.top, 20) - + DashUIKit.BottomSheet( + title: NSLocalizedString("Confirm", comment: ""), + showBackButton: .constant(false), + isDismissalEnabled: .constant(!isInFlight), + // Supplying `onClose` makes the close button live regardless of + // `isDismissalEnabled`, so the protected phases have to gate it here. + isCloseButtonEnabled: !isInFlight, + onClose: closeAction + ) { switch coordinator.phase { case .success: successBody @@ -745,8 +744,20 @@ struct SendConfirmSheet: View { detailsBody } } - .background(Color.dash.primaryBackground) - .interactiveDismissDisabled(isInFlight) + } + + /// The close button has to do what the visible button of the current phase + /// does. In the terminal phases that is `onCompleted`, which also unwinds + /// the send flow — `onCancel` only closes the sheet, so routing every phase + /// there would drop the user back on the amount screen with the amount + /// still entered, one tap away from sending it twice. + private var closeAction: () -> Void { + switch coordinator.phase { + case .success, .submittedUnconfirmed: + return onCompleted + default: + return onCancel + } } private var isInFlight: Bool { @@ -850,13 +861,6 @@ struct SendConfirmSheet: View { // MARK: - Pieces - private var dragHandle: some View { - Rectangle() - .fill(Color.dash.grabberFill) - .frame(width: 36, height: 5) - .cornerRadius(2.5) - } - private var summaryCard: some View { VStack(spacing: 0) { summaryRow(