Merge pull request #35 from joinappex/feature/feedback-paywall

CU-86dugrjdv Add a paywall at the end of the feedback form
This commit is contained in:
fmistoiu-appex
2024-10-23 12:41:19 +03:00
committed by GitHub
40 changed files with 1118 additions and 143 deletions

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "Vector.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@@ -0,0 +1,92 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 3.284912 -3.925781 cm
0.000000 0.678431 0.905882 scn
2.322571 16.495317 m
1.039556 17.778038 -1.040383 17.777800 -2.323103 16.494785 c
-3.605823 15.211771 -3.605585 13.131832 -2.322571 11.849112 c
2.322571 16.495317 l
h
6.963112 7.210699 m
4.640542 4.887596 l
5.923374 3.605057 8.002954 3.605087 9.285749 4.887663 c
6.963112 7.210699 l
h
23.211231 18.810753 m
24.494209 20.093510 24.494387 22.173449 23.211630 23.456427 c
21.928873 24.739403 19.848934 24.739582 18.565956 23.456825 c
23.211231 18.810753 l
h
-2.322571 11.849112 m
4.640542 4.887596 l
9.285683 9.533802 l
2.322571 16.495317 l
-2.322571 11.849112 l
h
9.285749 4.887663 m
23.211231 18.810753 l
18.565956 23.456825 l
4.640475 9.533735 l
9.285749 4.887663 l
h
f
n
Q
endstream
endobj
3 0 obj
794
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 27.458496 20.493164 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000000884 00000 n
0000000906 00000 n
0000001079 00000 n
0000001153 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1212
%%EOF

View File

@@ -0,0 +1,3 @@
<svg width="287" height="294" viewBox="0 0 287 294" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M101.306 145.081C99.1124 144.493 96.6129 144.803 94.5268 146.008L94.8152 146.15C92.8063 147.31 91.2881 149.32 90.623 151.558L78.1355 198.155C76.6738 200.807 76.5876 204.141 78.2036 206.94C78.6259 207.672 79.1323 208.319 79.7027 208.879C80.7854 209.996 82.1714 210.836 83.7715 211.242L132.519 224.302C137.394 225.608 142.261 222.798 143.49 217.968C144.841 213.171 142.031 208.303 137.156 206.997L137.112 206.92L109.032 199.443L113.57 196.823C164.795 167.248 182.333 101.806 152.76 50.5838C150.307 46.3346 144.9 44.8208 140.573 47.3188C136.324 49.7723 134.854 55.2562 137.307 59.5054C161.974 102.229 147.375 156.703 104.649 181.371L100.261 183.905L107.762 156.084L107.717 156.007C108.991 151.254 106.136 146.31 101.306 145.081Z" fill="#15A1EF"/>
</svg>

After

Width:  |  Height:  |  Size: 900 B

View File

@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "Arrow Ramp Right.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 KiB

View File

@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "AdobeStock_768605328 1.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

View File

@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "AdobeStock_776091887 2 (1).png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "AdobeStock_811446718 1.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -249,6 +249,11 @@
40E7A3012A0E1C7A00E0231A /* SplashscreenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40E7A3002A0E1C7A00E0231A /* SplashscreenViewController.swift */; };
40FC414329F74C7900BD7396 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40FC414229F74C7900BD7396 /* String+Extensions.swift */; };
4A86219093026DE70A097E79 /* Pods-LockdownTests-metadata.plist in Resources */ = {isa = PBXBuildFile; fileRef = 8DA68459884385F76BF86234 /* Pods-LockdownTests-metadata.plist */; };
51006F192CBD0E7400F5142C /* ImageBannerWithTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51006F182CBD0E7400F5142C /* ImageBannerWithTitleView.swift */; };
510AA2F62CB8222100E53560 /* FeedbackPaywallViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510AA2F52CB8222100E53560 /* FeedbackPaywallViewModel.swift */; };
5117DE882CBD4A6600C4A61B /* SectionTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5117DE872CBD4A6600C4A61B /* SectionTitleView.swift */; };
5145A19A2CBE37C40074C562 /* FeedbackFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5145A1992CBE37C40074C562 /* FeedbackFlow.swift */; };
51DD89E52CB7DA770028B4FE /* FeedbackPaywallViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DD89E42CB7DA770028B4FE /* FeedbackPaywallViewController.swift */; };
54F0B1A0273200B0002F3630 /* FirewallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCA4F4022F252720017740D /* FirewallController.swift */; };
5647ACFEBBAB001FAE27CAF9 /* Pods-LockdownTunnel-settings-metadata.plist in Resources */ = {isa = PBXBuildFile; fileRef = 6F089C7008AB8F59DE3EA7BD /* Pods-LockdownTunnel-settings-metadata.plist */; };
5666ABC4D0064E4669D1943F /* Pods-LockdownTunnel-metadata.plist in Resources */ = {isa = PBXBuildFile; fileRef = B2AFAE1E2F56A1CA9EC153D4 /* Pods-LockdownTunnel-metadata.plist */; };
@@ -775,6 +780,11 @@
40FC414229F74C7900BD7396 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
50F9BE503587CE4933CB7983 /* Pods-Lockdown-settings-metadata.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "Pods-Lockdown-settings-metadata.plist"; path = "Settings.bundle/Pods-Lockdown-settings-metadata.plist"; sourceTree = "<group>"; };
50FB8ADA1D444FD9486F2D44 /* Pods-Lockdown Firewall Widget-settings-metadata.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "Pods-Lockdown Firewall Widget-settings-metadata.plist"; path = "LockdowniOS/Settings.bundle/Pods-Lockdown Firewall Widget-settings-metadata.plist"; sourceTree = "<group>"; };
51006F182CBD0E7400F5142C /* ImageBannerWithTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageBannerWithTitleView.swift; sourceTree = "<group>"; };
510AA2F52CB8222100E53560 /* FeedbackPaywallViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackPaywallViewModel.swift; sourceTree = "<group>"; };
5117DE872CBD4A6600C4A61B /* SectionTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionTitleView.swift; sourceTree = "<group>"; };
5145A1992CBE37C40074C562 /* FeedbackFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackFlow.swift; sourceTree = "<group>"; };
51DD89E42CB7DA770028B4FE /* FeedbackPaywallViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackPaywallViewController.swift; sourceTree = "<group>"; };
66424506768B2196F870B04C /* Pods-LockdownTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LockdownTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-LockdownTests/Pods-LockdownTests.release.xcconfig"; sourceTree = "<group>"; };
6F089C7008AB8F59DE3EA7BD /* Pods-LockdownTunnel-settings-metadata.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "Pods-LockdownTunnel-settings-metadata.plist"; path = "Settings.bundle/Pods-LockdownTunnel-settings-metadata.plist"; sourceTree = "<group>"; };
71D50056A5E2E1F6486369F9 /* Pods-Lockdown VPN Widget.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Lockdown VPN Widget.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Lockdown VPN Widget/Pods-Lockdown VPN Widget.debug.xcconfig"; sourceTree = "<group>"; };
@@ -1251,6 +1261,7 @@
40098E1A29FDA61900886474 /* Paywalls */ = {
isa = PBXGroup;
children = (
51DD89E32CB7D9E10028B4FE /* FeedbackFormPaywall */,
40960AEE2A033A16000F82EB /* Models */,
40098E3329FF376900886474 /* FirewallPaywall */,
40098E2029FDA63100886474 /* VPNPaywall */,
@@ -1742,6 +1753,15 @@
name = ViewController;
sourceTree = "<group>";
};
51DD89E32CB7D9E10028B4FE /* FeedbackFormPaywall */ = {
isa = PBXGroup;
children = (
51DD89E42CB7DA770028B4FE /* FeedbackPaywallViewController.swift */,
510AA2F52CB8222100E53560 /* FeedbackPaywallViewModel.swift */,
);
name = FeedbackFormPaywall;
sourceTree = "<group>";
};
7C0D11102473EDFD00A26E04 /* Services */ = {
isa = PBXGroup;
children = (
@@ -2084,6 +2104,7 @@
B1A01CA32A432826004D43EE /* Questionnaire */ = {
isa = PBXGroup;
children = (
5145A1992CBE37C40074C562 /* FeedbackFlow.swift */,
B1A01CA62A432902004D43EE /* Controllers */,
B1A01CA72A432919004D43EE /* Models */,
B1062A342A45BD5800FA9E8B /* ViewModel */,
@@ -2124,6 +2145,8 @@
B1F11C7F2A49E35800A137A3 /* QuestionTitleView.swift */,
B1F11C812A49E63400A137A3 /* NavigationLinkView.swift */,
B1F11C892A4B050500A137A3 /* CountryView.swift */,
51006F182CBD0E7400F5142C /* ImageBannerWithTitleView.swift */,
5117DE872CBD4A6600C4A61B /* SectionTitleView.swift */,
);
path = Views;
sourceTree = "<group>";
@@ -2935,6 +2958,7 @@
B1F11C7C2A498CBF00A137A3 /* BaseStepViewModel.swift in Sources */,
3D47CDD222F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallSpinFadeLoader.swift in Sources */,
3D9FC67723E503DF004122D3 /* EmailSignInViewController.swift in Sources */,
51DD89E52CB7DA770028B4FE /* FeedbackPaywallViewController.swift in Sources */,
3DCA4F3322F22CB40017740D /* HomeViewController.swift in Sources */,
7CE91C962521ED5E009D8269 /* VPNRegion.swift in Sources */,
F0A8E0412C64E977001303C6 /* Defaults.swift in Sources */,
@@ -2964,6 +2988,7 @@
A1EBEAD42097AE6E002B9087 /* M13CheckboxPathGenerator.swift in Sources */,
40CC817E2A14BB3600F9805E /* UIView+Corners.swift in Sources */,
F01CAB7C2C61106F009C19CF /* SUI+Extensions.swift in Sources */,
5145A19A2CBE37C40074C562 /* FeedbackFlow.swift in Sources */,
B1062A2D2A447B2F00FA9E8B /* RadioSwitcher.swift in Sources */,
A1EBEACB2097AE6E002B9087 /* M13CheckboxDisclosurePathGenerator.swift in Sources */,
40960AEB2A03396F000F82EB /* ProductPurchasable.swift in Sources */,
@@ -3014,6 +3039,7 @@
B1F11C822A49E63400A137A3 /* NavigationLinkView.swift in Sources */,
A154A07E215C78180010FFCC /* BlockListCell.swift in Sources */,
F0B12AF82C60D602008EF8AA /* OneTimePaywallView.swift in Sources */,
510AA2F62CB8222100E53560 /* FeedbackPaywallViewModel.swift in Sources */,
3D47CDCE22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallScaleRippleMultiple.swift in Sources */,
B1BA87012A4C4BC400D141A8 /* QuestionModel.swift in Sources */,
40FC414329F74C7900BD7396 /* String+Extensions.swift in Sources */,
@@ -3044,6 +3070,7 @@
3D47CDC122F3C3F3003BD7F7 /* NVActivityIndicatorAnimationSemiCircleSpin.swift in Sources */,
3DBD57B422FCFF2500DE189F /* SetRegionViewController.swift in Sources */,
B1062A382A45BEBE00FA9E8B /* WhatProblemStepViewModel.swift in Sources */,
51006F192CBD0E7400F5142C /* ImageBannerWithTitleView.swift in Sources */,
402D253129E635CB00A5AB60 /* DomainsBlockedTableViewCell.swift in Sources */,
7C4D9BBB252C8748004175EA /* AccountUI.swift in Sources */,
3D47CDBE22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallScale.swift in Sources */,
@@ -3059,6 +3086,7 @@
402BAD362A0CD37C009B8820 /* ConnectivityService.swift in Sources */,
3D0711BB22FE7B5100391C6E /* TitleViewController.swift in Sources */,
3D47CDB222F3C3F3003BD7F7 /* NVActivityIndicatorShape.swift in Sources */,
5117DE882CBD4A6600C4A61B /* SectionTitleView.swift in Sources */,
7C422E97252796EE007F9C22 /* StaticTableView.swift in Sources */,
7C3EFA0224867DEE00719D96 /* TrackerInfo.swift in Sources */,
40960B152A034400000F82EB /* Keychainable.swift in Sources */,
@@ -3605,7 +3633,7 @@
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 6;
CURRENT_PROJECT_VERSION = 9;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = V8J3Z26F6Z;
ENABLE_BITCODE = NO;
@@ -3645,7 +3673,7 @@
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 6;
CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_TEAM = V8J3Z26F6Z;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (

View File

@@ -16,7 +16,8 @@ final class AccountViewController: BaseViewController, Loadable {
// MARK: - Properties
let tableView = StaticTableView(frame: .zero, style: .grouped)
var activePlans: [Subscription.PlanType] = []
var feedbackFlow: FeedbackFlow?
override func viewDidLoad() {
super.viewDidLoad()
@@ -390,16 +391,7 @@ final class AccountViewController: BaseViewController, Loadable {
}
private func openQuestionnaire() {
let stepsViewController = StepsViewController()
var viewModel = StepsViewModel { [weak self] message in
self?.sendMessage(
message,
subject: "Lockdown Error Reporting Form (iOS \(Bundle.main.versionString))"
)
}
stepsViewController.viewModel = viewModel
stepsViewController.modalPresentationStyle = .fullScreen
present(stepsViewController, animated: true)
feedbackFlow?.startFlow()
}
}

View File

@@ -67,7 +67,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// Prepare IAP
VPNSubscription.cacheLocalizedPrices()
Task {
let pids = Set(VPNSubscription.oneTimeProducts.toList())
let pids = Set(VPNSubscription.oneTimeProducts.toList() + VPNSubscription.feedbackProducts.toList())
await VPNSubscription.shared.loadSubscriptions(productIds: pids)
print("")
}

View File

@@ -198,7 +198,8 @@ open class BaseViewController: UIViewController, MFMailComposeViewControllerDele
let acceptButton = DefaultButton(title: NSLocalizedString("OK", comment: ""), dismissOnTap: true) { completionHandler() }
popup.addButtons([acceptButton])
self.present(popup, animated: true, completion: nil)
let topVC = presentedViewController ?? self
topVC.present(popup, animated: true, completion: nil)
}
enum PopupButton {
@@ -321,7 +322,9 @@ open class BaseViewController: UIViewController, MFMailComposeViewControllerDele
attachmentData.append(logFileData as Data)
}
composeVC.addAttachmentData(attachmentData as Data, mimeType: "text/plain", fileName: "diagnostics.txt")
self.present(composeVC, animated: true, completion: nil)
let topVC = presentedViewController ?? self
topVC.present(composeVC, animated: true, completion: nil)
} else {
guard let mailtoURL = Mailto.generateURL(recipient: recipient, subject: subject, body: message) else {

View File

@@ -43,7 +43,23 @@ final class ConfiguredNavigationView: UIView {
self.accentColor = accentColor
configureUI()
}
var titleView: UIView? {
didSet {
oldValue?.removeFromSuperview()
guard let titleView else {
titleLabel.isHidden = false
return
}
titleLabel.isHidden = true
addSubview(titleView)
titleView.anchors.leading.marginsPin(inset: 67)
titleView.anchors.trailing.marginsPin(inset: 67)
titleView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")

View File

@@ -19,6 +19,8 @@ final class DescriptionLabel: UIView {
lazy var lockImage: UIImageView = {
let image = UIImageView()
image.image = UIImage(named: "icn_lock")
image.setContentHuggingPriority(.required, for: .horizontal)
image.setContentCompressionResistancePriority(.required, for: .horizontal)
image.contentMode = .left
image.layer.masksToBounds = true
return image
@@ -27,6 +29,8 @@ final class DescriptionLabel: UIView {
lazy var checkmarkImage: UIImageView = {
let image = UIImageView()
image.image = UIImage(named: "icn_checkmark")
image.setContentHuggingPriority(.required, for: .horizontal)
image.setContentCompressionResistancePriority(.required, for: .horizontal)
image.contentMode = .left
image.layer.masksToBounds = true
image.isHidden = true
@@ -35,9 +39,10 @@ final class DescriptionLabel: UIView {
private lazy var descriptionLabel: UILabel = {
let label = UILabel()
label.textColor = .black
label.textColor = .label
label.font = fontMedium15
label.textAlignment = .left
label.numberOfLines = 0
return label
}()
@@ -47,8 +52,8 @@ final class DescriptionLabel: UIView {
stackView.addArrangedSubview(checkmarkImage)
stackView.addArrangedSubview(descriptionLabel)
stackView.axis = .horizontal
stackView.distribution = .fillProportionally
stackView.alignment = .leading
stackView.distribution = .fill
stackView.alignment = .center
stackView.spacing = 12
return stackView
}()

View File

@@ -0,0 +1,410 @@
//
// FeedbackPaywallViewController.swift
// Lockdown
//
// Created by Fabian Mistoiu on 10.10.2024.
// Copyright © 2024 Confirmed Inc. All rights reserved.
//
import UIKit
import Combine
class FeedbackPaywallViewController: UIViewController {
private let viewModel: FeedbackPaywallViewModel
private var subscriptions = Set<AnyCancellable>()
init(viewModel: FeedbackPaywallViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - UI
private lazy var closeButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.tintColor = .feedbackText
button.setTitle(Copy.close, for: .normal)
button.titleLabel?.font = .close
button.addAction(
UIAction { [weak self] _ in
guard let self else { return }
self.viewModel.onCloseHandler?(self)
},
for: .touchUpInside)
return button
}()
private lazy var bannerView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "feedback-paywall-banner"))
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.addSubview(bannerArrowImageView)
NSLayoutConstraint.activate([
bannerArrowImageView.centerYAnchor.constraint(equalTo: imageView.bottomAnchor),
bannerArrowImageView.centerXAnchor.constraint(equalTo: imageView.rightAnchor, constant: -10),
bannerArrowImageView.widthAnchor.constraint(equalToConstant: 92),
bannerArrowImageView.heightAnchor.constraint(equalToConstant: 92),
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: 1072 / 687),
imageView.widthAnchor.constraint(lessThanOrEqualToConstant: 420)
])
return imageView
}()
private lazy var bannerArrowImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "feedback-paywall-arrow"))
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textColor = .feedbackText
let continueRange = (Copy.title as NSString).range(of: Copy.titleHighlight)
let attributedString = NSMutableAttributedString(
string: Copy.title,
attributes: [.font: UIFont.title as Any])
attributedString.addAttribute(.foregroundColor, value: UIColor.feedbackBlue as Any, range: continueRange)
label.attributedText = attributedString
return label
}()
private lazy var descriptionLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textColor = .feedbackText
label.font = .description
label.text = Copy.description
return label
}()
private lazy var bulletPointContainer: UIView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = 5
[Copy.bulletPoint1, Copy.bulletPoint2, Copy.bulletPoint3]
.map { createBulletPointView(copy: $0) }
.forEach { stackView.addArrangedSubview($0) }
return stackView
}()
private lazy var bottomContainer: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [continueButton, linksContainer])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.alignment = .fill
stackView.distribution = .fill
stackView.spacing = 8
stackView.setCustomSpacing(17, after: continueButton)
return stackView
}()
private lazy var continueButton: UIButton = {
let button = UIButton(type: .system)
button.tintColor = .white
button.setTitle(Copy.continue, for: .normal)
button.titleLabel?.font = .ctaButton
button.backgroundColor = .feedbackBlue
button.layer.cornerRadius = 29
button.anchors.height.equal(58)
button.addAction(
UIAction { [weak self] _ in
guard let self else { return }
let pID = viewModel.paywallPlans[viewModel.selectedPlanIndex].id
viewModel.onPurchaseHandler?(self, pID)
},
for: .touchUpInside)
return button
}()
private lazy var linksContainer: UIView = {
let linkButtons = [
createLinkButton(title: Copy.terms, url: URL(string: "https://lockdownprivacy.com/terms")!),
createLinkButton(title: Copy.privacy, url: URL(string: "https://lockdownprivacy.com/privacy")!)
]
let stackView = UIStackView(arrangedSubviews: linkButtons)
stackView.distribution = .fillEqually
return stackView
}()
private var planButtons: [PlanContainer] = []
// MARK: -
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .background
view.addSubview(closeButton)
NSLayoutConstraint.activate([
closeButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 5),
closeButton.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 17)
])
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 28 + 16),
scrollView.leftAnchor.constraint(equalTo: view.leftAnchor),
scrollView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
let bannerImageContainer = UIView()
bannerImageContainer.translatesAutoresizingMaskIntoConstraints = false
bannerImageContainer.addSubview(bannerView)
NSLayoutConstraint.activate([
bannerView.topAnchor.constraint(equalTo: bannerImageContainer.topAnchor),
bannerView.bottomAnchor.constraint(equalTo: bannerImageContainer.bottomAnchor),
bannerView.centerXAnchor.constraint(equalTo: bannerImageContainer.centerXAnchor)
])
let bannerWidthConstraint = bannerView.widthAnchor.constraint(equalTo: bannerImageContainer.widthAnchor, multiplier: 0.8)
bannerWidthConstraint.priority = .init(rawValue: 999)
bannerWidthConstraint.isActive = true
let copyStackView = UIStackView(arrangedSubviews: [bannerImageContainer, titleLabel, descriptionLabel, bulletPointContainer])
copyStackView.translatesAutoresizingMaskIntoConstraints = false
copyStackView.axis = .vertical
copyStackView.alignment = .fill
copyStackView.distribution = .fill
copyStackView.spacing = 0
copyStackView.setCustomSpacing(22, after: bannerImageContainer)
copyStackView.setCustomSpacing(17, after: descriptionLabel)
scrollView.addSubview(copyStackView)
NSLayoutConstraint.activate([
copyStackView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 0),
copyStackView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
copyStackView.leftAnchor.constraint(equalTo: scrollView.leftAnchor, constant: 39),
copyStackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor)
])
view.addSubview(bottomContainer)
NSLayoutConstraint.activate([
bottomContainer.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0),
bottomContainer.centerXAnchor.constraint(equalTo: view.centerXAnchor),
bottomContainer.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 45),
bottomContainer.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 10)
])
viewModel.$paywallPlans.sink(receiveValue: { [weak self] plans in
guard let self else { return }
planButtons.forEach { $0.removeFromSuperview() }
planButtons = plans.map { self.createPlanButton(title: $0.name, price: $0.price, period: $0.pricePeriod, promo: $0.promo) }
planButtons.reversed().forEach { self.bottomContainer.insertArrangedSubview($0, at: 0) }
if let lastButton = planButtons.last {
bottomContainer.setCustomSpacing(30, after: lastButton)
}
selectButton(at: viewModel.selectedPlanIndex)
}).store(in: &subscriptions)
viewModel.$selectedPlanIndex.sink(receiveValue: { [weak self] selectedPlanIndex in
guard let self else { return }
selectButton(at: selectedPlanIndex)
}).store(in: &subscriptions)
}
func selectButton(at selectedIndex: Int) {
for (index, button) in planButtons.map(\.button).enumerated() {
let selected = index == selectedIndex
button.backgroundColor = selected ? .selectedPlanBackground : .clear
button.titleLabel?.font = selected ? .selectedPlanTitle: .unselectedPlanTitle
button.layer.borderColor = selected ? UIColor.feedbackBlue.cgColor : UIColor.smallGrey.cgColor
}
}
// MARK: - UI helper
private func createBulletPointView(copy: String) -> UIView {
let bulletPointImageView = UIImageView(image: UIImage(named: "feedback-checkmark"))
bulletPointImageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
bulletPointImageView.widthAnchor.constraint(equalToConstant: 9),
bulletPointImageView.heightAnchor.constraint(equalToConstant: 6),
])
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.text = copy
label.textColor = .feedbackText
label.font = .bulletPoint
let spacer = UIView()
spacer.setContentHuggingPriority(.required, for: .horizontal)
NSLayoutConstraint.activate([
spacer.widthAnchor.constraint(equalToConstant: 5),
spacer.heightAnchor.constraint(equalToConstant: 5),
])
let stackView = UIStackView(arrangedSubviews: [spacer, bulletPointImageView, label])
stackView.spacing = 8
stackView.alignment = .center
return stackView
}
private func createPlanButton(title: String, price: String, period: String?, promo: String?) -> PlanContainer {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.heightAnchor.constraint(equalToConstant: 54).isActive = true
button.layer.cornerRadius = 27
button.layer.borderWidth = 1
button.tintColor = .feedbackText
button.setTitle(title, for: .normal)
button.contentHorizontalAlignment = .left
button.contentVerticalAlignment = .center
button.titleEdgeInsets = UIEdgeInsets(top: 0, left: 21, bottom: 0, right: 0)
button.addTarget(self, action: #selector(planSelected), for: .touchUpInside)
let priceLabel = UILabel()
priceLabel.translatesAutoresizingMaskIntoConstraints = false
priceLabel.textColor = .feedbackText
priceLabel.font = .planPrice
priceLabel.text = price
let pricePeriodLabel: UILabel? = if period != nil { UILabel() } else { nil }
pricePeriodLabel?.translatesAutoresizingMaskIntoConstraints = false
pricePeriodLabel?.textColor = .feedbackText
pricePeriodLabel?.font = .planPeriod
pricePeriodLabel?.text = period
let stackView = UIStackView(arrangedSubviews: [priceLabel, pricePeriodLabel].compactMap { $0 })
stackView.isUserInteractionEnabled = false
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.alignment = .trailing
button.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.centerYAnchor.constraint(equalTo: button.centerYAnchor),
stackView.rightAnchor.constraint(equalTo: button.rightAnchor, constant: -18)
])
var promoView: UIView?
if let promo {
let gradientView = GradientView()
gradientView.translatesAutoresizingMaskIntoConstraints = false
gradientView.gradient = .custom([UIColor.promoGradientStart.cgColor, UIColor.promoGradientStart.cgColor], .horizontal)
gradientView.layer.cornerRadius = 11.5
gradientView.clipsToBounds = true
let promoLabel = UILabel()
promoLabel.translatesAutoresizingMaskIntoConstraints = false
promoLabel.textColor = .feedbackText
promoLabel.font = .selectedPlanTitle
promoLabel.text = promo
gradientView.addSubview(promoLabel)
NSLayoutConstraint.activate([
promoLabel.centerYAnchor.constraint(equalTo: gradientView.centerYAnchor),
promoLabel.centerXAnchor.constraint(equalTo: gradientView.centerXAnchor),
promoLabel.leftAnchor.constraint(equalTo: gradientView.leftAnchor, constant: 7),
gradientView.heightAnchor.constraint(equalToConstant: 23)
])
promoView = gradientView
}
return PlanContainer(button: button, promoLabel: promoView)
}
private func createLinkButton(title: String, url: URL) -> UIButton {
let button = UIButton(type: .system)
button.titleLabel?.font = fontMedium13
button.setTitle(title, for: .normal)
button.tintColor = .feedbackText
button.addAction(
UIAction { _ in
UIApplication.shared.open(url, options: [:], completionHandler: nil)
},
for: .touchUpInside)
return button
}
@objc func planSelected(_ sender: UIButton) {
guard let index = planButtons.map(\.button).firstIndex(of: sender) else { return }
viewModel.selectPlan(at: index)
}
}
private class PlanContainer: UIView {
let button: UIButton
let promoLabel: UIView?
init(button: UIButton, promoLabel: UIView?) {
self.button = button
self.promoLabel = promoLabel
super.init(frame: .zero)
addSubview(button)
NSLayoutConstraint.activate([
button.topAnchor.constraint(equalTo: topAnchor),
button.bottomAnchor.constraint(equalTo: bottomAnchor),
button.leftAnchor.constraint(equalTo: leftAnchor),
button.rightAnchor.constraint(equalTo: rightAnchor)
])
if let promoLabel {
addSubview(promoLabel)
NSLayoutConstraint.activate([
promoLabel.centerYAnchor.constraint(equalTo: button.topAnchor),
promoLabel.rightAnchor.constraint(equalTo: button.rightAnchor, constant: -18)
])
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private enum Copy {
static var close: String = NSLocalizedString("CLOSE", comment: "")
static var title: String = NSLocalizedString("Tap Continue to Activate this ONE TIME Offer", comment: "")
static var titleHighlight: String = NSLocalizedString("Continue", comment: "")
static var `continue`: String = NSLocalizedString("Continue", comment: "")
static let description: String = NSLocalizedString("Private Browsing with Hidden IP and Global Region Switching", comment: "")
static var bulletPoint1: String = NSLocalizedString("Anonymised browsing", comment: "")
static var bulletPoint2: String = NSLocalizedString("Location and IP address hidden", comment: "")
static var bulletPoint3: String = NSLocalizedString("Unlimited bandwidth and data usage & more", comment: "")
static var terms: String = NSLocalizedString("Terms", comment: "")
static var privacy: String = NSLocalizedString("Privacy", comment: "")
}
private extension UIFont {
static let close = UIFont(name: "Montserrat-Bold", size: 13)
static let title = UIFont(name: "SFProRounded-Semibold", size: 28)
static let description = UIFont(name: "Montserrat-Regular", size: 14)
static let bulletPoint = UIFont(name: "Montserrat-SemiBold", size: 12)
static let ctaButton = UIFont(name: "Montserrat-SemiBold", size: 20)
static let selectedPlanTitle = UIFont(name: "Montserrat-Bold", size: 12)
static let unselectedPlanTitle = UIFont(name: "Montserrat-Medium", size: 12)
static let planPrice = UIFont(name: "Montserrat-SemiBold", size: 14)
static let planPeriod = UIFont(name: "Montserrat-Medium", size: 14)
}
private extension UIColor {
static let background = UIColor.panelSecondaryBackground
static let feedbackText = UIColor.label
static let feedbackBlue = UIColor.fromHex("#00ADE7")
static let selectedPlanBackground = feedbackBlue.withAlphaComponent(0.1)
static let unselectedPlanBorder = UIColor.fromHex("#999999")
static let promoGradientStart = UIColor.fromHex("#FB923C")
static let promoGradientEnd = UIColor.fromHex("#EA580C")
}

View File

@@ -0,0 +1,64 @@
//
// FeedbackPaywallViewModel.swift
// Lockdown
//
// Created by Fabian Mistoiu on 10.10.2024.
// Copyright © 2024 Confirmed Inc. All rights reserved.
//
import Foundation
import StoreKit
class FeedbackPaywallViewModel {
struct PaywallPlan {
let id: String
let name: String
let price: String
let pricePeriod: String?
let promo: String?
}
@Published var paywallPlans: [PaywallPlan] = []
@Published var selectedPlanIndex: Int = 0
private let products: FeedbackProducts
private let subscriptionInfo: [InternalSubscription]
var onCloseHandler: ((UIViewController) -> Void)? = nil
var onPurchaseHandler: ((UIViewController, String) -> Void)? = nil
init(products: FeedbackProducts, subscriptionInfo: [InternalSubscription]) {
self.products = products
self.subscriptionInfo = subscriptionInfo
createPaywallPlans()
}
public func selectPlan(at index: Int) {
selectedPlanIndex = index
}
private func createPaywallPlans() {
let currencyFormatter = NumberFormatter()
currencyFormatter.usesGroupingSeparator = true
currencyFormatter.numberStyle = .currency
currencyFormatter.locale = subscriptionInfo.first?.priceLocale
guard let yearlyPlan = subscriptionInfo.first(where: { $0.productId == products.yearly}),
let weeklyPlan = subscriptionInfo.first(where: { $0.productId == products.weekly}),
let yearlyPrice = currencyFormatter.string(from: yearlyPlan.price),
let weeklyPrice = currencyFormatter.string(from: weeklyPlan.price) else {
return
}
let yearlyPricePerWeek = yearlyPlan.price.dividing(by: 52)
let saving = 100 - Int(Double(truncating: yearlyPricePerWeek) / Double(truncating: weeklyPlan.price)*100)
paywallPlans = [
PaywallPlan(id: products.yearly, name: "Yearly Plan", price: yearlyPrice, pricePeriod: nil, promo: "SAVE \(saving)%"),
PaywallPlan(id: products.weekly, name: "Weekly Plan", price: weeklyPrice, pricePeriod: "per week", promo: nil)
]
selectedPlanIndex = 0
}
}

View File

@@ -49,6 +49,8 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load
let ratingCountKey = "ratingCount" + lastVersionToAskForRating
let ratingTriggeredKey = "ratingTriggered" + lastVersionToAskForRating
var feedbackFlow: FeedbackFlow?
private lazy var scrollView: UIScrollView = {
let view = UIScrollView()
view.isScrollEnabled = true
@@ -90,18 +92,12 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load
return label
}()
// lazy var ctaView: CTAView = {
// let view = CTAView()
//
// return view
// }()
private lazy var mainTitle: UILabel = {
let label = UILabel()
label.text = NSLocalizedString("Get Anonymous protection", comment: "")
label.font = fontBold24
label.numberOfLines = 0
label.textColor = .black
label.textColor = .label
return label
}()
@@ -156,18 +152,20 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load
private lazy var stackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.distribution = .fillProportionally
stackView.distribution = .fill
stackView.spacing = 10
stackView.layer.cornerRadius = 8
stackView.backgroundColor = .extraLightGray
stackView.layoutMargins = UIEdgeInsets(top: 0, left: 8, bottom: 8, right: 8)
stackView.backgroundColor = UIColor.dynamicColor(light: .extraLightGray, dark: .panelSecondaryBackground!)
stackView.layoutMargins = UIEdgeInsets(top: 17, left: 20, bottom: 17, right: 20)
stackView.isLayoutMarginsRelativeArrangement = true
return stackView
}()
private lazy var closeButton: UIButton = {
let button = UIButton()
button.setImage(UIImage(named: "icn_close_filled"), for: .normal)
let config = UIImage.SymbolConfiguration(pointSize: 23)
button.setImage(UIImage(systemName: "xmark.circle.fill", withConfiguration: config), for: .normal)
button.tintColor = .secondaryLabel
button.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside)
return button
}()
@@ -276,6 +274,8 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load
stackView.addArrangedSubview(mainTitle)
stackView.addArrangedSubview(descriptionLabel6)
stackView.addArrangedSubview(upgradeButton)
stackView.setCustomSpacing(14, after: mainTitle)
stackView.setCustomSpacing(14, after: descriptionLabel6)
} else if UserDefaults.hasSeenUniversalPaywall {
protectionPlanLabel.text = "Universal protection"
stackView.anchors.height.equal(0)
@@ -288,6 +288,8 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load
stackView.addArrangedSubview(mainTitle)
stackView.addArrangedSubview(descriptionLabel4)
stackView.addArrangedSubview(descriptionLabel5)
stackView.setCustomSpacing(14, after: mainTitle)
stackView.setCustomSpacing(14, after: descriptionLabel5)
stackView.addArrangedSubview(upgradeButton)
} else {
mainTitle.text = "Get Advanced\nprotection"
@@ -297,6 +299,8 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load
stackView.addArrangedSubview(descriptionLabel2)
stackView.addArrangedSubview(descriptionLabel3)
stackView.addArrangedSubview(upgradeButton)
stackView.setCustomSpacing(14, after: mainTitle)
stackView.setCustomSpacing(14, after: descriptionLabel3)
}
view.addSubview(upgradeLabel)
@@ -322,9 +326,9 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load
contentView.addSubview(stackView)
stackView.anchors.top.marginsPin()
stackView.anchors.leading.pin(inset: 8)
stackView.anchors.trailing.marginsPin(inset: 10)
stackView.anchors.leading.pin(inset: 22)
stackView.anchors.trailing.pin(inset: 22)
contentView.addSubview(mainStack)
mainStack.anchors.top.spacing(8, to: stackView.anchors.bottom)
mainStack.anchors.leading.marginsPin()
@@ -332,12 +336,12 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load
mainStack.anchors.bottom.pin()
contentView.addSubview(closeButton)
closeButton.anchors.trailing.marginsPin(inset: 20)
closeButton.anchors.trailing.spacing(-8, to: stackView.anchors.trailing)
closeButton.anchors.top.marginsPin(inset: 8)
closeButton.anchors.height.equal(40)
closeButton.anchors.width.equal(40)
}
// closeButtonTapped
@objc func closeButtonTapped() {
stackView.anchors.height.equal(0)
upgradeButton.isHidden = true
@@ -478,7 +482,6 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load
} errored: { err in
self?.handlePurchaseFailed(error: err)
}
}
let viewCtrl = UIHostingController(rootView: OneTimePaywallView(model: model))
viewCtrl.modalPresentationStyle = .fullScreen
@@ -1329,23 +1332,36 @@ extension NEVPNStatus: CustomStringConvertible {
}
}
extension HomeViewController: PurchaseHandler {
func purchase(productId: String) {
VPNSubscription.selectedProductId = productId
VPNSubscription.purchase { [weak self] in
self?.handlePurchaseSuccessful()
} errored: { [weak self] err in
self?.handlePurchaseFailed(error: err)
}
}
}
extension HomeViewController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if viewController is UINavigationController || viewController is HomeViewController {
return true
}
let stepsViewController = StepsViewController()
var viewModel = StepsViewModel { [weak self] message in
self?.sendMessage(
message,
subject: "Lockdown Error Reporting Form (iOS \(Bundle.main.versionString))"
)
}
stepsViewController.viewModel = viewModel
stepsViewController.modalPresentationStyle = .fullScreen
present(stepsViewController, animated: true)
feedbackFlow?.startFlow()
return false
}
func showFeedbackPaywall() async {
guard let productInfos = await VPNSubscription.shared.loadSubscriptions(productIds: Set(VPNSubscription.feedbackProducts.toList())) else { return }
let viewModel = FeedbackPaywallViewModel(products: VPNSubscription.feedbackProducts, subscriptionInfo: productInfos)
viewModel.onCloseHandler = { vc in vc.dismiss(animated: true) }
viewModel.onPurchaseHandler = { [weak self] paywallVC, pid in
self?.purchase(productId: pid)
}
let paywalVC = FeedbackPaywallViewController(viewModel: viewModel)
present(paywalVC, animated: true)
}
}

View File

@@ -14,8 +14,8 @@ enum LockdownGradient {
case onboardingPurple
case ltoButtonOnHomePage
case welcomePurple
case custom([CGColor])
case custom([CGColor], NSLayoutConstraint.Axis = .vertical)
var colors: [CGColor] {
switch self {
case .lightBlue:
@@ -33,8 +33,18 @@ enum LockdownGradient {
UIColor.fromHex("#FFFFFF4D").withAlphaComponent(0.3).cgColor]
case .welcomePurple:
return [UIColor.gradientPink1.cgColor, UIColor.gradientPink2.cgColor]
case .custom(let colors):
case .custom(let colors, _):
return colors
}
}
var points: (start: CGPoint, end: CGPoint) {
switch self {
case .custom(_, .horizontal):
return (CGPoint(x: 0, y: 0), CGPoint(x: 1, y: 0))
default:
return (CGPoint(x: 0, y: 0), CGPoint(x: 0, y: 1))
}
}
}

View File

@@ -15,16 +15,34 @@ final class MainTabBarController: UITabBarController {
var vpnViewController: LDVpnViewController? { viewControllers![1] as? LDVpnViewController }
var configurationViewController: LDConfigurationViewController? { viewControllers![2] as? LDConfigurationViewController }
// var homeViewController: HomeViewController {
// return viewControllers![0] as! HomeViewController
// }
//
// var accountViewController: AccountViewController {
// let navigation = viewControllers![1] as! UINavigationController
// return navigation.viewControllers[0] as! AccountViewController
// }
var accountViewController: AccountViewController? {
for viewController in viewControllers ?? [] {
if let navigationController = viewController as? UINavigationController,
let accountViewController = navigationController.viewControllers.first as? AccountViewController {
return accountViewController
}
}
return nil
}
var homeViewController: HomeViewController? {
if let homeVC = viewControllers?.first(where: { $0 is HomeViewController }) {
return homeVC as? HomeViewController
}
return nil
}
override func viewDidLoad() {
super.viewDidLoad()
guard let homeViewController else { return }
homeViewController.feedbackFlow = FeedbackFlow(presentingViewController: homeViewController, purchaseHandler: homeViewController)
guard let accountViewController else { return }
accountViewController.feedbackFlow = FeedbackFlow(presentingViewController: accountViewController, purchaseHandler: homeViewController)
}
var accountTabBarButton: UIView? {
// this assumes that "Account" is the last tab. Change the code if this is no longer true
return tabBar.subviews.last(where: { String(describing: type(of: $0)) == "UITabBarButton" })

View File

@@ -20,6 +20,7 @@ class StepsViewController: UIViewController, StepsViewProtocol {
view.leftNavButton.setImage(UIImage(systemName: "chevron.left"), for: .normal)
view.leftNavButton.addTarget(self, action: #selector(backButtonClicked), for: .touchUpInside)
view.leftNavButton.tintColor = .label
view.titleView = stepsView
return view
}()
@@ -59,11 +60,6 @@ class StepsViewController: UIViewController, StepsViewProtocol {
navigationView.anchors.trailing.pin()
navigationView.anchors.top.safeAreaPin()
view.addSubview(stepsView)
stepsView.anchors.top.spacing(0, to: navigationView.anchors.bottom)
stepsView.anchors.leading.pin(inset: 18)
stepsView.anchors.trailing.pin(inset: 18)
view.addSubview(actionButton)
actionButton.anchors.leading.pin(inset: 24)
actionButton.anchors.trailing.pin(inset: 24)

View File

@@ -0,0 +1,60 @@
//
// FeedbackFlow.swift
// Lockdown
//
// Created by Fabian Mistoiu on 15.10.2024.
// Copyright © 2024 Confirmed Inc. All rights reserved.
//
import Foundation
protocol PurchaseHandler: AnyObject {
func purchase(productId: String)
}
class FeedbackFlow {
weak var presentingViewController: BaseViewController?
weak var purchaseHandler: PurchaseHandler?
init(presentingViewController: BaseViewController, purchaseHandler: PurchaseHandler) {
self.presentingViewController = presentingViewController
self.purchaseHandler = purchaseHandler
}
func startFlow() {
let isPremiumUser = BaseUserService.shared.user.currentSubscription != nil
let viewModel = StepsViewModel(isUserPremium: isPremiumUser) { [weak self] message in
Task { @MainActor [weak self] in
if !isPremiumUser {
await self?.showFeedbackPaywall()
}
self?.presentingViewController?.sendMessage(
message,
subject: "Lockdown Error Reporting Form (iOS \(Bundle.main.versionString))"
)
}
}
let stepsViewController = StepsViewController()
stepsViewController.viewModel = viewModel
stepsViewController.modalPresentationStyle = .fullScreen
presentingViewController?.present(stepsViewController, animated: true)
}
@MainActor
private func showFeedbackPaywall() async {
guard let presentingViewController,
let productInfos = await VPNSubscription.shared.loadSubscriptions(productIds: Set(VPNSubscription.feedbackProducts.toList())) else {
return
}
let viewModel = FeedbackPaywallViewModel(products: VPNSubscription.feedbackProducts, subscriptionInfo: productInfos)
viewModel.onCloseHandler = { vc in vc.dismiss(animated: true) }
viewModel.onPurchaseHandler = { [weak self] _, pid in
guard let purchaseHandler = self?.purchaseHandler else { return }
purchaseHandler.purchase(productId: pid)
}
let paywallVC = FeedbackPaywallViewController(viewModel: viewModel)
presentingViewController.present(paywallVC, animated: true)
}
}

View File

@@ -15,7 +15,7 @@ enum Steps {
var actionTitle: String {
switch self {
case .whatsProblem: return NSLocalizedString("Next", comment: "")
case .questions: return NSLocalizedString("Email Answers", comment: "")
case .questions: return NSLocalizedString("Submit Feedback", comment: "")
}
}

View File

@@ -52,7 +52,7 @@ class BaseStepViewModel {
view.placeholderLabel.isHidden = !(text?.isEmpty ?? true)
self.setupClear(cell)
cell.addSubview(view)
view.anchors.edges.pin(insets: .init(top: 3, left: 2, bottom: 5, right: 2))
view.anchors.edges.pin(insets: .init(top: 0, left: 0, bottom: 0, right: 0))
view.textDidChanged = { [weak self] text in
didChangeText(text)
self?.staticTableView?.beginUpdates()

View File

@@ -36,7 +36,7 @@ class QuestionsStepViewModel: BaseStepViewModel, StepViewModelProtocol {
staticTableView?.clear()
addTitleRow(
NSLocalizedString("Questions", comment: ""),
subtitle: NSLocalizedString("Please fill questions answer", comment: ""),
subtitle: NSLocalizedString("Please answer the questions and share as much information as possible", comment: ""),
bottomSpacing: 2
)
@@ -141,7 +141,7 @@ class QuestionsStepViewModel: BaseStepViewModel, StepViewModelProtocol {
switcher.didSelect = didSelect
self.setupClear(cell)
cell.addSubview(switcher)
switcher.anchors.edges.pin(insets: .init(top: 37, left: 2, bottom: 15, right: 2))
switcher.anchors.edges.pin(insets: .init(top: 18, left: 0, bottom: 3, right: 0))
}
}
@@ -151,7 +151,7 @@ class QuestionsStepViewModel: BaseStepViewModel, StepViewModelProtocol {
view.titleLabel.text = title
self.setupClear(cell)
cell.addSubview(view)
view.anchors.edges.pin(insets: .init(top: 20, left: 2, bottom: 10, right: 2))
view.anchors.edges.pin(insets: .init(top: 18, left: 0, bottom: 3, right: 0))
}
}
@@ -172,7 +172,7 @@ class QuestionsStepViewModel: BaseStepViewModel, StepViewModelProtocol {
view.didSelect = perform
self.setupClear(cell)
cell.addSubview(view)
view.anchors.edges.pin(insets: .init(top: 20, left: 2, bottom: 10, right: 2))
view.anchors.edges.pin(insets: .init(top: 10, left: 23, bottom: 0, right: 23))
}
}
}

View File

@@ -24,13 +24,15 @@ protocol StepViewModelProtocol {
}
class StepsViewModel {
private let isUserPremium: Bool
private lazy var steps: [StepViewModelProtocol] = [
whatProblemStep,
questionsStep
]
private lazy var whatProblemStep: WhatProblemStepViewModel = {
let viewModel = WhatProblemStepViewModel() { [weak self] _ in
let viewModel = WhatProblemStepViewModel(isUserPremium: isUserPremium) { [weak self] _ in
self?.view?.updateNextButton()
}
return viewModel
@@ -67,7 +69,8 @@ class StepsViewModel {
steps.reduce(true) { $0 && $1.isFilled }
}
init(sendMessage: ((String) -> Void)?) {
init(isUserPremium: Bool, sendMessage: ((String) -> Void)?) {
self.isUserPremium = isUserPremium
self.sendMessage = sendMessage
}

View File

@@ -9,6 +9,9 @@
import UIKit
class WhatProblemStepViewModel: BaseStepViewModel, StepViewModelProtocol {
private let isUserPremium: Bool
let step: Steps = .whatsProblem
private let problemList = [
NSLocalizedString("Internet connection is blocked", comment: ""),
NSLocalizedString("VPN not connecting", comment: ""),
@@ -37,8 +40,7 @@ class WhatProblemStepViewModel: BaseStepViewModel, StepViewModelProtocol {
}
return nil
}
var step: Steps = .whatsProblem
var message: String? {
guard selectedProblemIndex >= 0 else { return nil }
var result = ""
@@ -64,17 +66,35 @@ class WhatProblemStepViewModel: BaseStepViewModel, StepViewModelProtocol {
var didChangeReady: ((Bool) -> Void)?
init(didChangeReady: ((Bool) -> Void)?) {
init(isUserPremium: Bool, didChangeReady: ((Bool) -> Void)?) {
self.isUserPremium = isUserPremium
self.didChangeReady = didChangeReady
}
override func updateRows() {
staticTableView?.clear()
addTitleRow(
NSLocalizedString("What problem are you experiencing?", comment: ""),
subtitle: NSLocalizedString("Select your problem", comment: "")
)
staticTableView?.addRowCell { cell in
let titleView = ImageBannerWithTitleView()
titleView.imageView.image = isUserPremium ? UIImage(named: "feedback") : UIImage(named: "feedback-promo")
titleView.titleLabel.text = isUserPremium ? NSLocalizedString("How can we assist you?", comment: "") : NSLocalizedString("Get a promo Discount", comment: "")
titleView.subtitleLabel.text = isUserPremium ?
NSLocalizedString("Your feedback is valuable to us. By selecting the issue you're facing, we can guide you through troubleshooting or escalate the problem to our support team.", comment: "") :
NSLocalizedString("Let us know your opinion, and as a thank you for your feedback, well have a special offer waiting for you at the end!", comment: "")
titleView.subtitleLabel.textAlignment = isUserPremium ? .left : .center
self.setupClear(cell)
cell.addSubview(titleView)
titleView.anchors.edges.pin(insets: .init(top: 0, left: 0, bottom: 30, right: 0))
}
staticTableView?.addRowCell { cell in
let titleView = SectionTitleView()
titleView.titleLabel.text = NSLocalizedString("Select your problem", comment: "")
self.setupClear(cell)
cell.addSubview(titleView)
titleView.anchors.edges.pin(insets: .init(top: 0, left: 0, bottom: 5, right: 0))
}
for index in 0..<problemList.count {
staticTableView?.addRowCell { [unowned self] cell in
let view = SelectableRadioSwitcherWithTitle()
@@ -85,7 +105,7 @@ class WhatProblemStepViewModel: BaseStepViewModel, StepViewModelProtocol {
}
self.setupClear(cell)
cell.addSubview(view)
view.anchors.edges.pin(insets: .init(top: 5, left: 2, bottom: 5, right: 2))
view.anchors.edges.pin(insets: .init(top: 0, left: 2, bottom: 0, right: 2))
}
}

View File

@@ -0,0 +1,71 @@
//
// ImageBannerWithTitleView.swift
// Lockdown
//
// Created by Fabian Mistoiu on 14.10.2024.
// Copyright © 2024 Confirmed Inc. All rights reserved.
//
import UIKit
class ImageBannerWithTitleView: UIView {
var imageView: UIImageView = {
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
var titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .boldLockdownFont(size: 32)
label.textAlignment = .center
label.numberOfLines = 0
label.textColor = .label
return label
}()
var subtitleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .mediumLockdownFont(size: 14)
label.textAlignment = .center
label.numberOfLines = 0
label.textColor = .secondaryLabel
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func configure() {
backgroundColor = .clear
addSubview(imageView)
addSubview(titleLabel)
addSubview(subtitleLabel)
NSLayoutConstraint.activate([
imageView.widthAnchor.constraint(equalToConstant: 207),
imageView.heightAnchor.constraint(equalToConstant: 178),
imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
imageView.topAnchor.constraint(equalTo: topAnchor, constant: -25),
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
titleLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: -4),
subtitleLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
subtitleLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8),
subtitleLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
}

View File

@@ -20,7 +20,7 @@ class NavigationLinkView: UIView {
var titleLabel: UILabel = {
let label = UILabel()
label.font = .semiboldLockdownFont(size: 16)
label.font = .semiboldLockdownFont(size: 12)
label.numberOfLines = 0
label.textColor = .label
return label
@@ -28,9 +28,9 @@ class NavigationLinkView: UIView {
var placeholderLabel: UILabel = {
let label = UILabel()
label.font = .regularLockdownFont(size: 16)
label.font = .regularLockdownFont(size: 12)
label.numberOfLines = 0
label.textColor = .label
label.textColor = .secondaryLabel
return label
}()
@@ -38,7 +38,7 @@ class NavigationLinkView: UIView {
let configuration = UIImage.SymbolConfiguration(pointSize: 15, weight: .medium)
let view = UIImageView(image: .init(systemName: "chevron.right", withConfiguration: configuration))
view.contentMode = .center
view.tintColor = .label
view.tintColor = .secondaryLabel
return view
}()
@@ -56,8 +56,9 @@ class NavigationLinkView: UIView {
private func configure() {
backgroundColor = .tableCellBackground
layer.cornerRadius = 8
layer.borderWidth = 0
layer.borderWidth = 1
layer.borderColor = UIColor.secondaryLabel.cgColor
addSubview(emojiLabel)
emojiLabel.anchors.leading.pin(inset: 20)
emojiLabel.anchors.centerY.equal(anchors.centerY)
@@ -69,15 +70,15 @@ class NavigationLinkView: UIView {
chevron.anchors.size.equal(.init(width: 16, height: 16))
addSubview(titleLabel)
titleLabel.anchors.top.pin(inset: 18)
titleLabel.anchors.bottom.pin(inset: 18)
titleLabel.anchors.top.pin(inset: 12)
titleLabel.anchors.bottom.pin(inset: 12)
titleLabel.anchors.leading.spacing(28, to: emojiLabel.anchors.trailing)
chevron.anchors.leading.greaterThanOrEqual(titleLabel.anchors.trailing, constant: 8)
addSubview(placeholderLabel)
placeholderLabel.anchors.top.greaterThanOrEqual(anchors.top, constant: 18)
placeholderLabel.anchors.top.greaterThanOrEqual(anchors.top, constant: 12)
placeholderLabel.anchors.leading.pin(inset: 18)
anchors.bottom.greaterThanOrEqual(placeholderLabel.anchors.bottom, constant: 18)
anchors.bottom.greaterThanOrEqual(placeholderLabel.anchors.bottom, constant: 12)
chevron.anchors.leading.greaterThanOrEqual(placeholderLabel.anchors.trailing, constant: 18)
addGestureRecognizer(

View File

@@ -12,7 +12,7 @@ class QuestionTitleView: UIView {
var titleLabel: UILabel = {
let label = UILabel()
label.font = .semiboldLockdownFont(size: 16)
label.font = .semiboldLockdownFont(size: 14)
label.numberOfLines = 0
label.textColor = .label
return label

View File

@@ -22,10 +22,10 @@ class RadioSwitcher: UIView {
private lazy var imageView: UIImageView = {
let view = UIImageView(image: unselectedImage)
view.contentMode = .center
view.contentMode = .scaleAspectFit
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
configure()
@@ -34,7 +34,11 @@ class RadioSwitcher: UIView {
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func toggle() {
tapped()
}
private func configure() {
backgroundColor = .clear

View File

@@ -0,0 +1,43 @@
//
// SectionTitleView.swift
// Lockdown
//
// Created by Fabian Mistoiu on 14.10.2024.
// Copyright © 2024 Confirmed Inc. All rights reserved.
//
import UIKit
class SectionTitleView: UIView {
var titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .semiboldLockdownFont(size: 14)
label.numberOfLines = 0
label.textColor = .label
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func configure() {
backgroundColor = .clear
addSubview(titleLabel)
NSLayoutConstraint.activate([
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 8),
titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
}

View File

@@ -29,7 +29,7 @@ class SelectableRadioSwitcherWithTitle: UIView {
var titleLabel: UILabel = {
let label = UILabel()
label.font = .semiboldLockdownFont(size: 16)
label.font = .regularLockdownFont(size: 14)
label.numberOfLines = 0
label.textColor = .label
return label
@@ -45,19 +45,15 @@ class SelectableRadioSwitcherWithTitle: UIView {
}
private func configure() {
backgroundColor = unselectedBackgroundColor
layer.cornerRadius = 8
layer.borderWidth = 0
addSubview(switcher)
switcher.anchors.leading.pin(inset: 18)
switcher.anchors.size.equal(.init(width: 20, height: 20))
switcher.anchors.leading.pin(inset: 26)
switcher.anchors.size.equal(.init(width: 13, height: 13))
addSubview(titleLabel)
switcher.anchors.centerY.equal(titleLabel.anchors.centerY)
titleLabel.anchors.leading.spacing(22, to: switcher.anchors.trailing)
titleLabel.anchors.top.pin(inset: 18)
titleLabel.anchors.bottom.pin(inset: 18)
titleLabel.anchors.leading.spacing(9, to: switcher.anchors.trailing)
titleLabel.anchors.top.pin(inset: 8)
titleLabel.anchors.bottom.pin(inset: 8)
titleLabel.anchors.trailing.pin(inset: 18)
addGestureRecognizer(
@@ -67,9 +63,6 @@ class SelectableRadioSwitcherWithTitle: UIView {
private func updateView() {
switcher.isSelected = isSelected
backgroundColor = isSelected ? selectedBackgroundColor : unselectedBackgroundColor
layer.borderColor = (isSelected ? selectedBorderColor : .clear).cgColor
layer.borderWidth = isSelected ? 1 : 0
}
@objc private func tapped() {

View File

@@ -15,7 +15,7 @@ class TextViewWithPlaceholder: UIView {
private(set) lazy var textView: UITextView = {
let textView = UITextView()
textView.delegate = self
textView.font = .regularLockdownFont(size: 16)
textView.font = .regularLockdownFont(size: 12)
textView.backgroundColor = .clear
textView.textColor = .label
textView.isScrollEnabled = false
@@ -24,9 +24,8 @@ class TextViewWithPlaceholder: UIView {
private(set) lazy var placeholderLabel: UILabel = {
let label = UILabel()
label.font = .regularLockdownFont(size: 16)
label.textColor = .label
label.alpha = 0.3
label.font = .regularLockdownFont(size: 12)
label.textColor = .secondaryLabel
label.numberOfLines = 0
return label
}()
@@ -41,17 +40,22 @@ class TextViewWithPlaceholder: UIView {
}
private func configure() {
backgroundColor = .tableCellBackground
layer.cornerRadius = 10
let backgroundView = UIView()
backgroundView.backgroundColor = .tableCellBackground
backgroundView.layer.cornerRadius = 8
backgroundView.layer.borderWidth = 1
backgroundView.layer.borderColor = UIColor.secondaryLabel.cgColor
addSubview(backgroundView)
backgroundView.anchors.edges.pin(insets: .init(top: 10, left: 23, bottom: 0, right: 23))
addSubview(textView)
textView.anchors.edges.pin(insets: .init(top: 10, left: 14, bottom: 10, right: 14))
textView.anchors.height.greaterThanOrEqual(108)
textView.anchors.edges.pin(insets: .init(top: 15, left: 32, bottom: 0, right: 23))
textView.anchors.height.greaterThanOrEqual(95)
addSubview(placeholderLabel)
placeholderLabel.anchors.leading.pin(inset: 18)
placeholderLabel.anchors.top.pin(inset: 18)
placeholderLabel.anchors.trailing.pin(inset: 18)
placeholderLabel.anchors.leading.pin(inset: 36)
placeholderLabel.anchors.top.pin(inset: 18 + 5)
placeholderLabel.anchors.trailing.pin(inset: 36)
}
}

View File

@@ -12,7 +12,8 @@ class TitleAndSubtitleView: UIView {
var titleLabel: UILabel = {
let label = UILabel()
label.font = .boldLockdownFont(size: 34)
label.textAlignment = .center
label.font = .boldLockdownFont(size: 20)
label.numberOfLines = 0
label.textColor = .label
return label
@@ -20,7 +21,8 @@ class TitleAndSubtitleView: UIView {
var subtitleLabel: UILabel = {
let label = UILabel()
label.font = .mediumLockdownFont(size: 16)
label.textAlignment = .center
label.font = .mediumLockdownFont(size: 14)
label.numberOfLines = 0
label.textColor = .secondaryLabel
return label
@@ -45,7 +47,7 @@ class TitleAndSubtitleView: UIView {
addSubview(subtitleLabel)
subtitleLabel.anchors.leading.pin()
subtitleLabel.anchors.top.spacing(10, to: titleLabel.anchors.bottom)
subtitleLabel.anchors.top.spacing(8, to: titleLabel.anchors.bottom)
subtitleLabel.anchors.trailing.pin()
subtitleLabel.anchors.bottom.pin()
}

View File

@@ -20,7 +20,7 @@ class YesNoRadioSwitcherView: UIView {
var titleLabel: UILabel = {
let label = UILabel()
label.font = .semiboldLockdownFont(size: 16)
label.font = .semiboldLockdownFont(size: 14)
label.numberOfLines = 0
label.textColor = .label
return label
@@ -34,9 +34,10 @@ class YesNoRadioSwitcherView: UIView {
private lazy var yesLabel: UILabel = {
let label = UILabel()
label.font = .regularLockdownFont(size: 16)
label.font = .regularLockdownFont(size: 14)
label.textColor = .label
label.text = NSLocalizedString("Yes", comment: "")
label.isUserInteractionEnabled = true
return label
}()
@@ -48,9 +49,10 @@ class YesNoRadioSwitcherView: UIView {
private lazy var noLabel: UILabel = {
let label = UILabel()
label.font = .regularLockdownFont(size: 16)
label.font = .regularLockdownFont(size: 14)
label.textColor = .label
label.text = NSLocalizedString("No", comment: "")
label.isUserInteractionEnabled = true
return label
}()
@@ -74,16 +76,21 @@ class YesNoRadioSwitcherView: UIView {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.distribution = .fillEqually
stackView.alignment = .leading
stackView.alignment = .fill
addSubview(stackView)
stackView.anchors.top.spacing(15, to: titleLabel.anchors.bottom)
stackView.anchors.leading.pin()
stackView.anchors.top.spacing(12, to: titleLabel.anchors.bottom)
stackView.anchors.leading.pin(inset: 23)
stackView.anchors.trailing.pin()
stackView.anchors.bottom.pin()
stackView.anchors.height.equal(20)
stackView.addArrangedSubview(view(for: yesSwitcher, andLabel: yesLabel))
stackView.addArrangedSubview(view(for: noSwitcher, andLabel: noLabel))
stackView.anchors.height.equal(23)
let yesView = view(for: yesSwitcher, andLabel: yesLabel)
yesView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tappedYes)))
stackView.addArrangedSubview(yesView)
let noView = view(for: noSwitcher, andLabel: noLabel)
noView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tappedNo)))
stackView.addArrangedSubview(noView)
}
private func view(for switcher: RadioSwitcher, andLabel label: UILabel) -> UIView {
@@ -93,8 +100,8 @@ class YesNoRadioSwitcherView: UIView {
view.addSubview(switcher)
switcher.anchors.leading.pin()
switcher.anchors.centerY.equal(view.anchors.centerY)
switcher.anchors.size.equal(.init(width: 25, height: 25))
switcher.anchors.size.equal(.init(width: 13, height: 13))
view.addSubview(label)
label.anchors.top.pin()
label.anchors.leading.spacing(10, to: switcher.anchors.trailing)
@@ -108,4 +115,12 @@ class YesNoRadioSwitcherView: UIView {
yesSwitcher.isSelected = isSelected ?? false
noSwitcher.isSelected = !(isSelected ?? true)
}
@objc private func tappedYes() {
yesSwitcher.toggle()
}
@objc private func tappedNo() {
noSwitcher.toggle()
}
}

View File

@@ -17,8 +17,8 @@ extension UIView {
func applyGradient(_ gradient: LockdownGradient, corners: Corners = .continuous(0)) -> CAGradientLayer {
let gradientLayer = CAGradientLayer()
gradientLayer.colors = gradient.colors
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 0, y: 1)
gradientLayer.startPoint = gradient.points.start
gradientLayer.endPoint = gradient.points.end
gradientLayer.frame = bounds
gradientLayer.corners = corners
@@ -146,4 +146,24 @@ extension UIColor {
alpha: CGFloat(1.0)
)
}
static func dynamicColor(light: UIColor, dark: UIColor) -> UIColor {
guard #available(iOS 13.0, *) else { return light }
return UIColor { $0.userInterfaceStyle == .dark ? dark : light }
}
}
class GradientView: UIView {
var gradient: LockdownGradient? {
didSet {
guard let gradient else { return }
gradientLayer = applyGradient(gradient)
}
}
var gradientLayer: CALayer?
override func layoutSubviews() {
super.layoutSubviews()
gradientLayer?.frame = bounds
}
}

View File

@@ -25,6 +25,18 @@ struct OnetTimeProducts {
}
}
struct FeedbackProducts {
let weekly: String
let yearly: String
func toList() -> [String] {
let otherSelf = Mirror(reflecting: self)
return otherSelf.children.compactMap {
$0.value as? String
}
}
}
struct InternalSubscription: Hashable {
let productId: String
let period: SKProduct.PeriodUnit
@@ -72,8 +84,10 @@ actor VPNSubscription: NSObject {
weeklyTrial: "lockdown.weekly.1.202408.3_days_free_trial.4hrs_offer",
yearly: "lockdown.yearly.40.202408.no_trial.4hrs_offer_",
yearlyTrial: "lockdown.yearly.40.202408.3_days_free_trial.4hrs_offer")
static let feedbackProducts = FeedbackProducts(weekly: "lockdown.weekly.1.202409.no_trial.feedback",
yearly: "lockdown.yearly.40.202409.no_trial.feedback")
static var selectedProductId = productIdAdvancedMonthly
// Advanced Level
static var defaultPriceStringAdvancedMonthly = "$4.99"
static var defaultPriceStringAdvancedYearly = "$29.99"