Skip to content

Commit

Permalink
Sync UI PDF Generation (#1527)
Browse files Browse the repository at this point in the history
Adds PDF Generation and updates save recovery key screen.

Steps to test this PR:

Smoke test all of Sync UI
After turning on sync or connecting a device, save the PDF and check the contents
Make sure copy button works
  • Loading branch information
brindy authored Mar 9, 2023
1 parent 36902d7 commit 50e686b
Show file tree
Hide file tree
Showing 10 changed files with 467 additions and 129 deletions.
75 changes: 64 additions & 11 deletions DuckDuckGo/SyncSettingsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ class SyncSettingsViewController: UIHostingController<SyncSettingsScreenView> {

lazy var authenticator = Authenticator()

static let fakeCode = "eyAicmVjb3ZlcnkiOiB7ICJ1c2VyX2lkIjogIjY4RTc4OTlBLTQ5OTQtNEUzMi04MERDLT" +
"gyNzNFMDc1MUExMSIsICJwcmltYXJ5X2tleSI6ICJNVEl6TkRVMk56ZzVN" +
"REV5TXpRMU5qYzRPVEF4TWpNME5UWTNPRGt3TVRJPSIgfSB9"

convenience init() {
self.init(rootView: SyncSettingsScreenView(model: SyncSettingsScreenViewModel()))

Expand Down Expand Up @@ -99,34 +103,64 @@ 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))
navigationController?.present(controller, animated: true) {
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))
Expand Down Expand Up @@ -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
}

}
26 changes: 20 additions & 6 deletions LocalPackages/DuckUI/Sources/DuckUI/Button.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
60 changes: 35 additions & 25 deletions LocalPackages/SyncUI/Sources/SyncUI/Views/DeviceConnectedView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
}

Expand All @@ -80,6 +89,7 @@ public struct DeviceConnectedView: View {
SaveRecoveryKeyView(model: saveRecoveryKeyViewModel)
.transition(.move(edge: .trailing))
} else {
// TODO apply underflow
deviceSyncedView()
.transition(.move(edge: .leading))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<BackgroundContent: View, ForegroundContent: View>: 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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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, *) {
Expand All @@ -67,4 +76,12 @@ extension View {
}
}

@ViewBuilder
func applyUnderflowBackgroundOnPhone(isCompact: Bool) -> some View {
if UIDevice.current.userInterfaceIdiom == .phone && isCompact {
self.thinMaterialBackground()
} else {
self
}
}
}
Loading

0 comments on commit 50e686b

Please sign in to comment.