diff --git a/DuckDuckGo/SyncSettingsViewController.swift b/DuckDuckGo/SyncSettingsViewController.swift index d9b8f441b3..3d846ddb3e 100644 --- a/DuckDuckGo/SyncSettingsViewController.swift +++ b/DuckDuckGo/SyncSettingsViewController.swift @@ -29,6 +29,10 @@ class SyncSettingsViewController: UIHostingController { lazy var authenticator = Authenticator() + static let fakeCode = "eyAicmVjb3ZlcnkiOiB7ICJ1c2VyX2lkIjogIjY4RTc4OTlBLTQ5OTQtNEUzMi04MERDLT" + + "gyNzNFMDc1MUExMSIsICJwcmltYXJ5X2tleSI6ICJNVEl6TkRVMk56ZzVN" + + "REV5TXpRMU5qYzRPVEF4TWpNME5UWTNPRGt3TVRJPSIgfSB9" + convenience init() { self.init(rootView: SyncSettingsScreenView(model: SyncSettingsScreenViewModel())) @@ -99,22 +103,53 @@ extension SyncSettingsViewController: SyncManagementViewModelDelegate { } func shareRecoveryPDF() { - guard let view = navigationController?.visibleViewController?.view, - let url = Bundle.main.url(forResource: "DuckDuckGo Recovery Document", withExtension: "pdf") else { - return + let pdfController = UIHostingController(rootView: RecoveryKeyPDFView(code: Self.fakeCode)) + pdfController.loadView() + + let pdfRect = CGRect(x: 0, y: 0, width: 612, height: 792) + pdfController.view.frame = CGRect(x: 0, y: 0, width: pdfRect.width, height: pdfRect.height + 100) + pdfController.view.insetsLayoutMarginsFromSafeArea = false + + let rootVC = UIApplication.shared.windows.first?.rootViewController + rootVC?.addChild(pdfController) + rootVC?.view.insertSubview(pdfController.view, at: 0) + defer { + pdfController.view.removeFromSuperview() } - navigationController?.visibleViewController?.presentShareSheet(withItems: [url], - fromView: view) { [weak self] _, success, _, _ in - if success { - self?.navigationController?.visibleViewController?.dismiss(animated: true) - } + let format = UIGraphicsPDFRendererFormat() + format.documentInfo = [ + kCGPDFContextTitle as String: "DuckDuckGo Sync Recovery Code" + ] + + let renderer = UIGraphicsPDFRenderer(bounds: pdfRect, format: format) + let data = renderer.pdfData { context in + context.beginPage() + context.cgContext.translateBy(x: 0, y: -100) + pdfController.view.layer.render(in: context.cgContext) + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineHeightMultiple = 1.55 + + let code = Self.fakeCode + code.draw(in: CGRect(x: 240, y: 380, width: 294, height: 1000), withAttributes: [ + .font: UIFont.monospacedSystemFont(ofSize: 13, weight: .regular), + .foregroundColor: UIColor.black, + .paragraphStyle: paragraphStyle, + .kern: 2 + ]) } + let pdf = RecoveryCodeItem(data: data) + navigationController?.visibleViewController?.presentShareSheet(withItems: [pdf], + fromView: view) { [weak self] _, success, _, _ in + guard success else { return } + self?.navigationController?.visibleViewController?.dismiss(animated: true) + } } func showDeviceConnected() { - let model = SaveRecoveryKeyViewModel { [weak self] in + let model = SaveRecoveryKeyViewModel(key: Self.fakeCode) { [weak self] in self?.shareRecoveryPDF() } let controller = UIHostingController(rootView: DeviceConnectedView(saveRecoveryKeyViewModel: model)) @@ -122,11 +157,10 @@ extension SyncSettingsViewController: SyncManagementViewModelDelegate { self.rootView.model.showDevices() self.rootView.model.appendDevice(.init(id: UUID().uuidString, name: "Another Device", isThisDevice: false)) } - } func showRecoveryPDF() { - let model = SaveRecoveryKeyViewModel { [weak self] in + let model = SaveRecoveryKeyViewModel(key: Self.fakeCode) { [weak self] in self?.shareRecoveryPDF() } let controller = UIHostingController(rootView: SaveRecoveryKeyView(model: model)) @@ -234,3 +268,22 @@ private class PortraitNavigationController: UINavigationController { } } + +private class RecoveryCodeItem: NSObject, UIActivityItemSource { + + let data: Data + + init(data: Data) { + self.data = data + super.init() + } + + func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any { + return URL(fileURLWithPath: "DuckDuckGo Sync Recovery Code.pdf") + } + + func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? { + data + } + +} diff --git a/LocalPackages/DuckUI/Sources/DuckUI/Button.swift b/LocalPackages/DuckUI/Sources/DuckUI/Button.swift index b4310d55d8..30f60af54e 100644 --- a/LocalPackages/DuckUI/Sources/DuckUI/Button.swift +++ b/LocalPackages/DuckUI/Sources/DuckUI/Button.swift @@ -46,22 +46,36 @@ public struct PrimaryButtonStyle: ButtonStyle { public struct SecondaryButtonStyle: ButtonStyle { @Environment(\.colorScheme) private var colorScheme - - public init() {} + + let compact: Bool + + public init(compact: Bool = false) { + self.compact = compact + } private var backgoundColor: Color { colorScheme == .light ? Color.white : .gray70 } + private var foregroundColor: Color { colorScheme == .light ? .blueBase : .white } - + + @ViewBuilder + func compactPadding(view: some View) -> some View { + if compact { + view + } else { + view.padding() + } + } + public func makeBody(configuration: Configuration) -> some View { - configuration.label - .font(Font(UIFont.boldAppFont(ofSize: Consts.fontSize))) + compactPadding(view: configuration.label) + .font(Font(UIFont.boldAppFont(ofSize: compact ? Consts.fontSize - 1 : Consts.fontSize))) .foregroundColor(configuration.isPressed ? foregroundColor.opacity(Consts.pressedOpacity) : foregroundColor.opacity(1)) .padding() - .frame(minWidth: 0, maxWidth: .infinity, maxHeight: Consts.height) + .frame(minWidth: 0, maxWidth: .infinity, maxHeight: compact ? Consts.height - 10 : Consts.height) .cornerRadius(Consts.cornerRadius) } } diff --git a/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/SaveRecoveryKeyViewModel.swift b/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/SaveRecoveryKeyViewModel.swift index 87a4eba8db..ced8831904 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/SaveRecoveryKeyViewModel.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/ViewModels/SaveRecoveryKeyViewModel.swift @@ -25,9 +25,7 @@ public class SaveRecoveryKeyViewModel: ObservableObject { let key: String let showRecoveryPDFAction: () -> Void - public init(key: String = "eyJyZWNvdmVyeSI6eyJ1c2ViNjgwRDQ1QjUtNUU2RS00MzQ3jZGQkU4MEZDNEE3IiwicHJpbWFyeV9rZXkiOiJBUBUUVCQVFFQkFRRUJBUUVCBUUVCQVFFPSJ9fQ==", - - showRecoveryPDFAction: @escaping () -> Void) { + public init(key: String, showRecoveryPDFAction: @escaping () -> Void) { self.key = key self.showRecoveryPDFAction = showRecoveryPDFAction } diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/DeviceConnectedView.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/DeviceConnectedView.swift index 2088bb3097..48668f3eee 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/DeviceConnectedView.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/DeviceConnectedView.swift @@ -22,6 +22,12 @@ import DuckUI public struct DeviceConnectedView: View { + @Environment(\.verticalSizeClass) var verticalSizeClass + + var isCompact: Bool { + verticalSizeClass == .compact + } + @State var showRecoveryPDF = false let saveRecoveryKeyViewModel: SaveRecoveryKeyViewModel @@ -32,35 +38,37 @@ public struct DeviceConnectedView: View { @ViewBuilder func deviceSyncedView() -> some View { - VStack(spacing: 0) { - Image("SyncSuccess") - .padding(.bottom, 20) + UnderflowContainer { + VStack(spacing: 0) { + Image("SyncSuccess") + .padding(.bottom, 20) - Text(UserText.deviceSyncedTitle) - .font(.system(size: 28, weight: .bold)) - .padding(.bottom, 24) + Text(UserText.deviceSyncedTitle) + .font(.system(size: 28, weight: .bold)) + .padding(.bottom, 24) - ZStack { - RoundedRectangle(cornerRadius: 8) - .stroke(.black.opacity(0.14)) + ZStack { + RoundedRectangle(cornerRadius: 8) + .stroke(.black.opacity(0.14)) - HStack(spacing: 0) { - Image(systemName: "checkmark.circle") - .padding(.horizontal, 18) - Text("WIP: Another Device") - Spacer() + HStack(spacing: 0) { + Image(systemName: "checkmark.circle") + .padding(.horizontal, 18) + Text("WIP: Another Device") + Spacer() + } } - } - .frame(height: 44) - .padding(.horizontal, 20) - .padding(.bottom, 20) - - Text(UserText.deviceSyncedMessage) - .lineLimit(nil) - .multilineTextAlignment(.center) + .frame(height: 44) + .padding(.horizontal, 20) + .padding(.bottom, 20) - Spacer() + Text(UserText.deviceSyncedMessage) + .lineLimit(nil) + .multilineTextAlignment(.center) + Spacer() + } + } foreground: { Button { withAnimation { self.showRecoveryPDF = true @@ -69,9 +77,10 @@ public struct DeviceConnectedView: View { Text(UserText.nextButtonTitle) } .buttonStyle(PrimaryButtonStyle()) + .frame(maxWidth: 360) + .padding(.horizontal, 30) } - .padding(.top, 56) - .padding(.horizontal) + .padding(.top, isCompact ? 0 : 56) .padding(.bottom) } @@ -80,6 +89,7 @@ public struct DeviceConnectedView: View { SaveRecoveryKeyView(model: saveRecoveryKeyViewModel) .transition(.move(edge: .trailing)) } else { + // TODO apply underflow deviceSyncedView() .transition(.move(edge: .leading)) } diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/QRCodeView.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/QRCodeView.swift index b9085cca67..ca1939e9e4 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/QRCodeView.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/QRCodeView.swift @@ -45,7 +45,7 @@ struct QRCodeView: View { Image(uiImage: generateQRCode(from: string, size: size)) .resizable() .aspectRatio(contentMode: .fit) - .frame(maxHeight: size) + .frame(height: size) } func generateQRCode(from text: String, size: CGFloat) -> UIImage { diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/UnderflowContainer.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/UnderflowContainer.swift new file mode 100644 index 0000000000..6e4764fb15 --- /dev/null +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/UnderflowContainer.swift @@ -0,0 +1,90 @@ +// +// UnderflowContainer.swift +// DuckDuckGo +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct UnderflowContainer: View { + + let space = CoordinateSpace.named("overContent") + + @Environment(\.verticalSizeClass) var verticalSizeClass + + var isCompact: Bool { + verticalSizeClass == .compact + } + + @State var minHeight = 0.0 { + didSet { + print("***", minHeight) + } + } + + let background: () -> BackgroundContent + let foreground: () -> ForegroundContent + + var body: some View { + ZStack { + ScrollView { + VStack { + background() + Spacer() + ZStack { + EmptyView() + } + .frame(minHeight: minHeight) + } + } + + VStack { + Spacer() + foreground() + .modifier(SizeModifier()) + .padding(.top, isCompact ? 8 : 0) + .frame(maxWidth: .infinity) + .ignoresSafeArea(.container) + .applyUnderflowBackgroundOnPhone(isCompact: isCompact) + } + } + .onPreferenceChange(SizePreferenceKey.self) { self.minHeight = $0.height + 8 } + } + +} + +struct SizePreferenceKey: PreferenceKey { + static var defaultValue: CGSize = .zero + + static func reduce(value: inout CGSize, nextValue: () -> CGSize) { + print(#function, value) + if value.height == 0 || value.width == 0 { + value = nextValue() + } + } +} + +struct SizeModifier: ViewModifier { + private var sizeView: some View { + GeometryReader { geometry in + Color.clear.preference(key: SizePreferenceKey.self, value: geometry.size) + } + } + + func body(content: Content) -> some View { + content.background(sizeView) + } +} diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/ViewExtensions.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/ViewExtensions.swift index 1be90f3cd0..cb38358306 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/ViewExtensions.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/ViewExtensions.swift @@ -49,6 +49,15 @@ extension View { } } + @ViewBuilder + func thinMaterialBackground() -> some View { + if #available(iOS 15.0, *) { + self.background(.ultraThinMaterial) + } else { + self.background(Rectangle().foregroundColor(.black.opacity(0.9))) + } + } + @ViewBuilder func monospaceSystemFont(ofSize size: Double) -> some View { if #available(iOS 15.0, *) { @@ -67,4 +76,12 @@ extension View { } } + @ViewBuilder + func applyUnderflowBackgroundOnPhone(isCompact: Bool) -> some View { + if UIDevice.current.userInterfaceIdiom == .phone && isCompact { + self.thinMaterialBackground() + } else { + self + } + } } diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/RecoveryKeyPDFView.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/RecoveryKeyPDFView.swift new file mode 100644 index 0000000000..d8e64f0d08 --- /dev/null +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/RecoveryKeyPDFView.swift @@ -0,0 +1,181 @@ +// +// RecoveryKeyPDFView.swift +// DuckDuckGo +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +// Will externalise strings once we work out how to share this with macOS +public struct RecoveryKeyPDFView: View { + + let code: String + + public init(code: String) { + self.code = code + } + + @ViewBuilder + func codeBoxHeader() -> some View { + VStack(spacing: 0) { + Text("Keep this information safe and secure.") + .font(.system(size: 13, weight: .semibold)) + .lineSpacing(1.03) + .foregroundColor(.white) + .padding(.top, 6) + + Text("Anyone with access to this code can access your synced data.") + .font(.system(size: 10, weight: .regular)) + .lineSpacing(1.09) + .foregroundColor(.white) + .padding(.top, 2) + } + } + + @ViewBuilder + func headerImage() -> some View { + Image("SyncSuccess") + .padding(.top, 28) + } + + @ViewBuilder + func title() -> some View { + Text("Sync Recovery Code") + .font(.system(size: 22)) + } + + @ViewBuilder + func information() -> some View { + // swiftlint:disable line_length + Text("The Recovery Code allows you to sync your bookmarks across multiple devices and recover your synced data if you lose access to a device.") + .font(.system(size: 12)) + .lineSpacing(1.167) + .lineLimit(2) + .multilineTextAlignment(.center) + .padding(.horizontal, 54) + // swiftlint:enable line_length + } + + @ViewBuilder + func codeBox() -> some View { + ZStack(alignment: .top) { + VStack(spacing: 0) { + codeBoxHeader() + + ZStack { + HStack { + QRCodeView(string: code, size: 144, style: .light) + Spacer() + } + } + .padding(24) + .background( + ZStack(alignment: .top) { + RoundedRectangle(cornerRadius: 8).foregroundColor(.white) + Rectangle() + .foregroundColor(.white) + .frame(height: 10) + } + ) + .padding(.top, 6) + } + .padding(1) + } + .background(ZStack { + RoundedRectangle(cornerRadius: 8).foregroundColor(.blue80) + + RoundedRectangle(cornerRadius: 8).foregroundColor(.blueBase) + .padding(1) + }) + .padding(.top, 28) + .padding(.horizontal, 54) + } + + @ViewBuilder + func instructionsTitle() -> some View { + Text("How does this code work?") + .font(.system(size: 17, weight: .bold)) + } + + @ViewBuilder + func step(id: Int, text: String) -> some View { + VStack(spacing: 16) { + Text("\(id)") + .foregroundColor(.white) + .font(.system(size: 17, weight: .bold)) + .background(Circle() + .frame(width: 36, height: 36) + .foregroundColor(.blueBase)) + + Text(text) + .multilineTextAlignment(.center) + .lineLimit(nil) + .lineSpacing(0.98) + .font(.system(size: 12)) + } + .frame(width: 156) + } + + @ViewBuilder + func instructions() -> some View { + HStack { + + step(id: 1, text: "Open DuckDuckGo on another device and go to Sync in Settings.") + + step(id: 2, text: "Go to Scan QR Code and scan the QR code or copy and paste the text code.") + + step(id: 3, text: "Sync will be activated and you'll once again have access to all of your data.") + + } + } + + @ViewBuilder + func appLogo() -> some View { + Image("LogoDarkText") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 72) + } + + public var body: some View { + VStack(spacing: 0) { + headerImage() + + title() + .padding(.top, 14.25) + + information() + .padding(.top, 10) + + codeBox() + + instructionsTitle() + .padding(.top, 28) + + instructions() + .padding(.top, 28) + + Divider() + .frame(width: 280) + .padding(.top, 28) + + appLogo() + .padding(.top, 10) + + } + } + +} diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/SaveRecoveryKeyView.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/SaveRecoveryKeyView.swift index 54371d126c..4c6dfe7f8e 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/SaveRecoveryKeyView.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/SaveRecoveryKeyView.swift @@ -23,6 +23,11 @@ import DuckUI public struct SaveRecoveryKeyView: View { @Environment(\.presentationMode) var presentation + @Environment(\.verticalSizeClass) var verticalSizeClass + + var isCompact: Bool { + verticalSizeClass == .compact + } let model: SaveRecoveryKeyViewModel @@ -40,24 +45,12 @@ public struct SaveRecoveryKeyView: View { Text(model.key) .fontWeight(.light) .lineSpacing(1.6) - .lineLimit(3) + .lineLimit(5) .applyKerning(2) .truncationMode(.tail) .monospaceSystemFont(ofSize: 16) .frame(maxWidth: .infinity) } - - GridWithHStackFallback { - Button("Save as PDF") { - model.showRecoveryPDFAction() - } - .buttonStyle(PrimaryButtonStyle(compact: true)) - - Button("Copy Key") { - model.copyKey() - } - .buttonStyle(PrimaryButtonStyle(compact: true)) - } } .padding(.top, 20) .padding(.horizontal, 20) @@ -66,7 +59,32 @@ public struct SaveRecoveryKeyView: View { .background(RoundedRectangle(cornerRadius: 10).foregroundColor(.black.opacity(0.12))) } - public var body: some View { + @ViewBuilder + func buttons() -> some View { + VStack(spacing: isCompact ? 4 : 8) { + Button("Save as PDF") { + model.showRecoveryPDFAction() + } + .buttonStyle(PrimaryButtonStyle(compact: isCompact)) + + Button("Copy Key") { + model.copyKey() + } + .buttonStyle(SecondaryButtonStyle(compact: isCompact)) + + Button { + presentation.wrappedValue.dismiss() + } label: { + Text(UserText.notNowButton) + } + .buttonStyle(SecondaryButtonStyle(compact: isCompact)) + } + .frame(maxWidth: 360) + .padding(.horizontal, 30) + } + + @ViewBuilder + func mainContent() -> some View { VStack(spacing: 0) { Image("SyncDownloadRecoveryCode") .padding(.bottom, 24) @@ -83,37 +101,16 @@ public struct SaveRecoveryKeyView: View { .padding(.bottom, 20) recoveryInfo() - - Spacer() - - Button { - presentation.wrappedValue.dismiss() - } label: { - Text(UserText.notNowButton) - } - .buttonStyle(SecondaryButtonStyle()) } - .padding(.top, 56) + .padding(.top, isCompact ? 0 : 56) .padding(.horizontal, 30) } -} - -struct GridWithHStackFallback: View { - - let content: () -> Content - - init(@ViewBuilder content: @escaping () -> Content) { - self.content = content - } - - var body: some View { - if #available(iOS 16.0, *) { - Grid { - GridRow(content: content) - } - } else { - HStack(content: content) + public var body: some View { + UnderflowContainer { + mainContent() + } foreground: { + buttons() } } diff --git a/LocalPackages/SyncUI/Sources/SyncUI/Views/TurnOnSyncView.swift b/LocalPackages/SyncUI/Sources/SyncUI/Views/TurnOnSyncView.swift index 3a991fbd65..5a4fa0b6d0 100644 --- a/LocalPackages/SyncUI/Sources/SyncUI/Views/TurnOnSyncView.swift +++ b/LocalPackages/SyncUI/Sources/SyncUI/Views/TurnOnSyncView.swift @@ -24,6 +24,10 @@ public struct TurnOnSyncView: View { @Environment(\.verticalSizeClass) var verticalSizeClass + var isCompact: Bool { + verticalSizeClass == .compact + } + @State var turnOnSyncNavigation = false @ObservedObject var model: TurnOnSyncViewModel @@ -93,6 +97,10 @@ private struct CTAView: View { @Environment(\.verticalSizeClass) var verticalSizeClass + var isCompact: Bool { + verticalSizeClass == .compact + } + let imageName: String let title: String let message: String @@ -127,56 +135,26 @@ private struct CTAView: View { secondaryLabel: String, primaryAction: @escaping () -> Void, secondaryAction: @escaping () -> Void) -> some View { - VStack(spacing: verticalSizeClass == .compact ? 4 : 8) { + VStack(spacing: isCompact ? 4 : 8) { Button(primaryLabel, action: primaryAction) - .buttonStyle(DuckUI.PrimaryButtonStyle()) + .buttonStyle(PrimaryButtonStyle(compact: isCompact)) Button(secondaryLabel, action: secondaryAction) - .buttonStyle(DuckUI.SecondaryButtonStyle()) + .buttonStyle(SecondaryButtonStyle(compact: isCompact)) } .frame(maxWidth: 360) } var body: some View { - - ZStack { - ScrollView { - VStack { - hero(imageName: imageName, - title: title, - text: message) - - Spacer() - } - } - - VStack { - Spacer() - - VStack(spacing: verticalSizeClass == .compact ? 4 : 8) { - buttons(primaryLabel: primaryButtonLabel, - secondaryLabel: secondaryButtonLabel, - primaryAction: primaryAction, - secondaryAction: secondaryAction) - } - .ignoresSafeArea(.container) - .frame(maxWidth: .infinity) - .applyBackgroundOnPhone(isCompact: verticalSizeClass == .compact) - } - } - - } - -} - -private extension View { - - @ViewBuilder - func applyBackgroundOnPhone(isCompact: Bool) -> some View { - if UIDevice.current.userInterfaceIdiom == .phone && isCompact { - self.regularMaterialBackground() - } else { - self + UnderflowContainer { + hero(imageName: imageName, + title: title, + text: message) + } foreground: { + buttons(primaryLabel: primaryButtonLabel, + secondaryLabel: secondaryButtonLabel, + primaryAction: primaryAction, + secondaryAction: secondaryAction) } }