Skip to content
This repository was archived by the owner on Feb 24, 2025. It is now read-only.

Commit 50e686b

Browse files
authored
Sync UI PDF Generation (#1527)
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
1 parent 36902d7 commit 50e686b

File tree

10 files changed

+467
-129
lines changed

10 files changed

+467
-129
lines changed

DuckDuckGo/SyncSettingsViewController.swift

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ class SyncSettingsViewController: UIHostingController<SyncSettingsScreenView> {
2929

3030
lazy var authenticator = Authenticator()
3131

32+
static let fakeCode = "eyAicmVjb3ZlcnkiOiB7ICJ1c2VyX2lkIjogIjY4RTc4OTlBLTQ5OTQtNEUzMi04MERDLT" +
33+
"gyNzNFMDc1MUExMSIsICJwcmltYXJ5X2tleSI6ICJNVEl6TkRVMk56ZzVN" +
34+
"REV5TXpRMU5qYzRPVEF4TWpNME5UWTNPRGt3TVRJPSIgfSB9"
35+
3236
convenience init() {
3337
self.init(rootView: SyncSettingsScreenView(model: SyncSettingsScreenViewModel()))
3438

@@ -99,34 +103,64 @@ extension SyncSettingsViewController: SyncManagementViewModelDelegate {
99103
}
100104

101105
func shareRecoveryPDF() {
102-
guard let view = navigationController?.visibleViewController?.view,
103-
let url = Bundle.main.url(forResource: "DuckDuckGo Recovery Document", withExtension: "pdf") else {
104-
return
106+
let pdfController = UIHostingController(rootView: RecoveryKeyPDFView(code: Self.fakeCode))
107+
pdfController.loadView()
108+
109+
let pdfRect = CGRect(x: 0, y: 0, width: 612, height: 792)
110+
pdfController.view.frame = CGRect(x: 0, y: 0, width: pdfRect.width, height: pdfRect.height + 100)
111+
pdfController.view.insetsLayoutMarginsFromSafeArea = false
112+
113+
let rootVC = UIApplication.shared.windows.first?.rootViewController
114+
rootVC?.addChild(pdfController)
115+
rootVC?.view.insertSubview(pdfController.view, at: 0)
116+
defer {
117+
pdfController.view.removeFromSuperview()
105118
}
106119

107-
navigationController?.visibleViewController?.presentShareSheet(withItems: [url],
108-
fromView: view) { [weak self] _, success, _, _ in
109-
if success {
110-
self?.navigationController?.visibleViewController?.dismiss(animated: true)
111-
}
120+
let format = UIGraphicsPDFRendererFormat()
121+
format.documentInfo = [
122+
kCGPDFContextTitle as String: "DuckDuckGo Sync Recovery Code"
123+
]
124+
125+
let renderer = UIGraphicsPDFRenderer(bounds: pdfRect, format: format)
126+
let data = renderer.pdfData { context in
127+
context.beginPage()
128+
context.cgContext.translateBy(x: 0, y: -100)
129+
pdfController.view.layer.render(in: context.cgContext)
130+
131+
let paragraphStyle = NSMutableParagraphStyle()
132+
paragraphStyle.lineHeightMultiple = 1.55
133+
134+
let code = Self.fakeCode
135+
code.draw(in: CGRect(x: 240, y: 380, width: 294, height: 1000), withAttributes: [
136+
.font: UIFont.monospacedSystemFont(ofSize: 13, weight: .regular),
137+
.foregroundColor: UIColor.black,
138+
.paragraphStyle: paragraphStyle,
139+
.kern: 2
140+
])
112141
}
113142

143+
let pdf = RecoveryCodeItem(data: data)
144+
navigationController?.visibleViewController?.presentShareSheet(withItems: [pdf],
145+
fromView: view) { [weak self] _, success, _, _ in
146+
guard success else { return }
147+
self?.navigationController?.visibleViewController?.dismiss(animated: true)
148+
}
114149
}
115150

116151
func showDeviceConnected() {
117-
let model = SaveRecoveryKeyViewModel { [weak self] in
152+
let model = SaveRecoveryKeyViewModel(key: Self.fakeCode) { [weak self] in
118153
self?.shareRecoveryPDF()
119154
}
120155
let controller = UIHostingController(rootView: DeviceConnectedView(saveRecoveryKeyViewModel: model))
121156
navigationController?.present(controller, animated: true) {
122157
self.rootView.model.showDevices()
123158
self.rootView.model.appendDevice(.init(id: UUID().uuidString, name: "Another Device", isThisDevice: false))
124159
}
125-
126160
}
127161

128162
func showRecoveryPDF() {
129-
let model = SaveRecoveryKeyViewModel { [weak self] in
163+
let model = SaveRecoveryKeyViewModel(key: Self.fakeCode) { [weak self] in
130164
self?.shareRecoveryPDF()
131165
}
132166
let controller = UIHostingController(rootView: SaveRecoveryKeyView(model: model))
@@ -234,3 +268,22 @@ private class PortraitNavigationController: UINavigationController {
234268
}
235269

236270
}
271+
272+
private class RecoveryCodeItem: NSObject, UIActivityItemSource {
273+
274+
let data: Data
275+
276+
init(data: Data) {
277+
self.data = data
278+
super.init()
279+
}
280+
281+
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
282+
return URL(fileURLWithPath: "DuckDuckGo Sync Recovery Code.pdf")
283+
}
284+
285+
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
286+
data
287+
}
288+
289+
}

LocalPackages/DuckUI/Sources/DuckUI/Button.swift

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,22 +46,36 @@ public struct PrimaryButtonStyle: ButtonStyle {
4646

4747
public struct SecondaryButtonStyle: ButtonStyle {
4848
@Environment(\.colorScheme) private var colorScheme
49-
50-
public init() {}
49+
50+
let compact: Bool
51+
52+
public init(compact: Bool = false) {
53+
self.compact = compact
54+
}
5155

5256
private var backgoundColor: Color {
5357
colorScheme == .light ? Color.white : .gray70
5458
}
59+
5560
private var foregroundColor: Color {
5661
colorScheme == .light ? .blueBase : .white
5762
}
58-
63+
64+
@ViewBuilder
65+
func compactPadding(view: some View) -> some View {
66+
if compact {
67+
view
68+
} else {
69+
view.padding()
70+
}
71+
}
72+
5973
public func makeBody(configuration: Configuration) -> some View {
60-
configuration.label
61-
.font(Font(UIFont.boldAppFont(ofSize: Consts.fontSize)))
74+
compactPadding(view: configuration.label)
75+
.font(Font(UIFont.boldAppFont(ofSize: compact ? Consts.fontSize - 1 : Consts.fontSize)))
6276
.foregroundColor(configuration.isPressed ? foregroundColor.opacity(Consts.pressedOpacity) : foregroundColor.opacity(1))
6377
.padding()
64-
.frame(minWidth: 0, maxWidth: .infinity, maxHeight: Consts.height)
78+
.frame(minWidth: 0, maxWidth: .infinity, maxHeight: compact ? Consts.height - 10 : Consts.height)
6579
.cornerRadius(Consts.cornerRadius)
6680
}
6781
}

LocalPackages/SyncUI/Sources/SyncUI/ViewModels/SaveRecoveryKeyViewModel.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,7 @@ public class SaveRecoveryKeyViewModel: ObservableObject {
2525
let key: String
2626
let showRecoveryPDFAction: () -> Void
2727

28-
public init(key: String = "eyJyZWNvdmVyeSI6eyJ1c2ViNjgwRDQ1QjUtNUU2RS00MzQ3jZGQkU4MEZDNEE3IiwicHJpbWFyeV9rZXkiOiJBUBUUVCQVFFQkFRRUJBUUVCBUUVCQVFFPSJ9fQ==",
29-
30-
showRecoveryPDFAction: @escaping () -> Void) {
28+
public init(key: String, showRecoveryPDFAction: @escaping () -> Void) {
3129
self.key = key
3230
self.showRecoveryPDFAction = showRecoveryPDFAction
3331
}

LocalPackages/SyncUI/Sources/SyncUI/Views/DeviceConnectedView.swift

Lines changed: 35 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ import DuckUI
2222

2323
public struct DeviceConnectedView: View {
2424

25+
@Environment(\.verticalSizeClass) var verticalSizeClass
26+
27+
var isCompact: Bool {
28+
verticalSizeClass == .compact
29+
}
30+
2531
@State var showRecoveryPDF = false
2632

2733
let saveRecoveryKeyViewModel: SaveRecoveryKeyViewModel
@@ -32,35 +38,37 @@ public struct DeviceConnectedView: View {
3238

3339
@ViewBuilder
3440
func deviceSyncedView() -> some View {
35-
VStack(spacing: 0) {
36-
Image("SyncSuccess")
37-
.padding(.bottom, 20)
41+
UnderflowContainer {
42+
VStack(spacing: 0) {
43+
Image("SyncSuccess")
44+
.padding(.bottom, 20)
3845

39-
Text(UserText.deviceSyncedTitle)
40-
.font(.system(size: 28, weight: .bold))
41-
.padding(.bottom, 24)
46+
Text(UserText.deviceSyncedTitle)
47+
.font(.system(size: 28, weight: .bold))
48+
.padding(.bottom, 24)
4249

43-
ZStack {
44-
RoundedRectangle(cornerRadius: 8)
45-
.stroke(.black.opacity(0.14))
50+
ZStack {
51+
RoundedRectangle(cornerRadius: 8)
52+
.stroke(.black.opacity(0.14))
4653

47-
HStack(spacing: 0) {
48-
Image(systemName: "checkmark.circle")
49-
.padding(.horizontal, 18)
50-
Text("WIP: Another Device")
51-
Spacer()
54+
HStack(spacing: 0) {
55+
Image(systemName: "checkmark.circle")
56+
.padding(.horizontal, 18)
57+
Text("WIP: Another Device")
58+
Spacer()
59+
}
5260
}
53-
}
54-
.frame(height: 44)
55-
.padding(.horizontal, 20)
56-
.padding(.bottom, 20)
57-
58-
Text(UserText.deviceSyncedMessage)
59-
.lineLimit(nil)
60-
.multilineTextAlignment(.center)
61+
.frame(height: 44)
62+
.padding(.horizontal, 20)
63+
.padding(.bottom, 20)
6164

62-
Spacer()
65+
Text(UserText.deviceSyncedMessage)
66+
.lineLimit(nil)
67+
.multilineTextAlignment(.center)
6368

69+
Spacer()
70+
}
71+
} foreground: {
6472
Button {
6573
withAnimation {
6674
self.showRecoveryPDF = true
@@ -69,9 +77,10 @@ public struct DeviceConnectedView: View {
6977
Text(UserText.nextButtonTitle)
7078
}
7179
.buttonStyle(PrimaryButtonStyle())
80+
.frame(maxWidth: 360)
81+
.padding(.horizontal, 30)
7282
}
73-
.padding(.top, 56)
74-
.padding(.horizontal)
83+
.padding(.top, isCompact ? 0 : 56)
7584
.padding(.bottom)
7685
}
7786

@@ -80,6 +89,7 @@ public struct DeviceConnectedView: View {
8089
SaveRecoveryKeyView(model: saveRecoveryKeyViewModel)
8190
.transition(.move(edge: .trailing))
8291
} else {
92+
// TODO apply underflow
8393
deviceSyncedView()
8494
.transition(.move(edge: .leading))
8595
}

LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/QRCodeView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ struct QRCodeView: View {
4545
Image(uiImage: generateQRCode(from: string, size: size))
4646
.resizable()
4747
.aspectRatio(contentMode: .fit)
48-
.frame(maxHeight: size)
48+
.frame(height: size)
4949
}
5050

5151
func generateQRCode(from text: String, size: CGFloat) -> UIImage {
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
//
2+
// UnderflowContainer.swift
3+
// DuckDuckGo
4+
//
5+
// Copyright © 2023 DuckDuckGo. All rights reserved.
6+
//
7+
// Licensed under the Apache License, Version 2.0 (the "License");
8+
// you may not use this file except in compliance with the License.
9+
// You may obtain a copy of the License at
10+
//
11+
// http://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing, software
14+
// distributed under the License is distributed on an "AS IS" BASIS,
15+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
// See the License for the specific language governing permissions and
17+
// limitations under the License.
18+
//
19+
20+
import SwiftUI
21+
22+
struct UnderflowContainer<BackgroundContent: View, ForegroundContent: View>: View {
23+
24+
let space = CoordinateSpace.named("overContent")
25+
26+
@Environment(\.verticalSizeClass) var verticalSizeClass
27+
28+
var isCompact: Bool {
29+
verticalSizeClass == .compact
30+
}
31+
32+
@State var minHeight = 0.0 {
33+
didSet {
34+
print("***", minHeight)
35+
}
36+
}
37+
38+
let background: () -> BackgroundContent
39+
let foreground: () -> ForegroundContent
40+
41+
var body: some View {
42+
ZStack {
43+
ScrollView {
44+
VStack {
45+
background()
46+
Spacer()
47+
ZStack {
48+
EmptyView()
49+
}
50+
.frame(minHeight: minHeight)
51+
}
52+
}
53+
54+
VStack {
55+
Spacer()
56+
foreground()
57+
.modifier(SizeModifier())
58+
.padding(.top, isCompact ? 8 : 0)
59+
.frame(maxWidth: .infinity)
60+
.ignoresSafeArea(.container)
61+
.applyUnderflowBackgroundOnPhone(isCompact: isCompact)
62+
}
63+
}
64+
.onPreferenceChange(SizePreferenceKey.self) { self.minHeight = $0.height + 8 }
65+
}
66+
67+
}
68+
69+
struct SizePreferenceKey: PreferenceKey {
70+
static var defaultValue: CGSize = .zero
71+
72+
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
73+
print(#function, value)
74+
if value.height == 0 || value.width == 0 {
75+
value = nextValue()
76+
}
77+
}
78+
}
79+
80+
struct SizeModifier: ViewModifier {
81+
private var sizeView: some View {
82+
GeometryReader { geometry in
83+
Color.clear.preference(key: SizePreferenceKey.self, value: geometry.size)
84+
}
85+
}
86+
87+
func body(content: Content) -> some View {
88+
content.background(sizeView)
89+
}
90+
}

LocalPackages/SyncUI/Sources/SyncUI/Views/Internal/ViewExtensions.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ extension View {
4949
}
5050
}
5151

52+
@ViewBuilder
53+
func thinMaterialBackground() -> some View {
54+
if #available(iOS 15.0, *) {
55+
self.background(.ultraThinMaterial)
56+
} else {
57+
self.background(Rectangle().foregroundColor(.black.opacity(0.9)))
58+
}
59+
}
60+
5261
@ViewBuilder
5362
func monospaceSystemFont(ofSize size: Double) -> some View {
5463
if #available(iOS 15.0, *) {
@@ -67,4 +76,12 @@ extension View {
6776
}
6877
}
6978

79+
@ViewBuilder
80+
func applyUnderflowBackgroundOnPhone(isCompact: Bool) -> some View {
81+
if UIDevice.current.userInterfaceIdiom == .phone && isCompact {
82+
self.thinMaterialBackground()
83+
} else {
84+
self
85+
}
86+
}
7087
}

0 commit comments

Comments
 (0)