diff --git a/Assets.xcassets/Paywall/Feadback/Contents.json b/Assets.xcassets/Paywall/Feadback/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/Assets.xcassets/Paywall/Feadback/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Assets.xcassets/Paywall/Feadback/feedback-checkmark.imageset/Contents.json b/Assets.xcassets/Paywall/Feadback/feedback-checkmark.imageset/Contents.json
new file mode 100644
index 0000000..0f1e1b9
--- /dev/null
+++ b/Assets.xcassets/Paywall/Feadback/feedback-checkmark.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "filename" : "Vector.pdf",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "preserves-vector-representation" : true
+ }
+}
diff --git a/Assets.xcassets/Paywall/Feadback/feedback-checkmark.imageset/Vector.pdf b/Assets.xcassets/Paywall/Feadback/feedback-checkmark.imageset/Vector.pdf
new file mode 100644
index 0000000..92cbdd8
--- /dev/null
+++ b/Assets.xcassets/Paywall/Feadback/feedback-checkmark.imageset/Vector.pdf
@@ -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
\ No newline at end of file
diff --git a/Assets.xcassets/Paywall/Feadback/feedback-paywall-arrow.imageset/Arrow Ramp Right.svg b/Assets.xcassets/Paywall/Feadback/feedback-paywall-arrow.imageset/Arrow Ramp Right.svg
new file mode 100644
index 0000000..2c3a6f0
--- /dev/null
+++ b/Assets.xcassets/Paywall/Feadback/feedback-paywall-arrow.imageset/Arrow Ramp Right.svg
@@ -0,0 +1,3 @@
+
diff --git a/Assets.xcassets/Paywall/Feadback/feedback-paywall-arrow.imageset/Contents.json b/Assets.xcassets/Paywall/Feadback/feedback-paywall-arrow.imageset/Contents.json
new file mode 100644
index 0000000..266c3f4
--- /dev/null
+++ b/Assets.xcassets/Paywall/Feadback/feedback-paywall-arrow.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "filename" : "Arrow Ramp Right.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "preserves-vector-representation" : true
+ }
+}
diff --git a/Assets.xcassets/Paywall/Feadback/feedback-paywall-banner.imageset/AdobeStock_768605328 1.png b/Assets.xcassets/Paywall/Feadback/feedback-paywall-banner.imageset/AdobeStock_768605328 1.png
new file mode 100644
index 0000000..e2a75cd
Binary files /dev/null and b/Assets.xcassets/Paywall/Feadback/feedback-paywall-banner.imageset/AdobeStock_768605328 1.png differ
diff --git a/Assets.xcassets/Paywall/Feadback/feedback-paywall-banner.imageset/Contents.json b/Assets.xcassets/Paywall/Feadback/feedback-paywall-banner.imageset/Contents.json
new file mode 100644
index 0000000..7ddb1d5
--- /dev/null
+++ b/Assets.xcassets/Paywall/Feadback/feedback-paywall-banner.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "AdobeStock_768605328 1.png",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Assets.xcassets/Paywall/Feadback/feedback-promo.imageset/AdobeStock_776091887 2 (1).png b/Assets.xcassets/Paywall/Feadback/feedback-promo.imageset/AdobeStock_776091887 2 (1).png
new file mode 100644
index 0000000..d33850c
Binary files /dev/null and b/Assets.xcassets/Paywall/Feadback/feedback-promo.imageset/AdobeStock_776091887 2 (1).png differ
diff --git a/Assets.xcassets/Paywall/Feadback/feedback-promo.imageset/Contents.json b/Assets.xcassets/Paywall/Feadback/feedback-promo.imageset/Contents.json
new file mode 100644
index 0000000..86fb01e
--- /dev/null
+++ b/Assets.xcassets/Paywall/Feadback/feedback-promo.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "AdobeStock_776091887 2 (1).png",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Assets.xcassets/Paywall/Feadback/feedback.imageset/AdobeStock_811446718 1.png b/Assets.xcassets/Paywall/Feadback/feedback.imageset/AdobeStock_811446718 1.png
new file mode 100644
index 0000000..e28a096
Binary files /dev/null and b/Assets.xcassets/Paywall/Feadback/feedback.imageset/AdobeStock_811446718 1.png differ
diff --git a/Assets.xcassets/Paywall/Feadback/feedback.imageset/Contents.json b/Assets.xcassets/Paywall/Feadback/feedback.imageset/Contents.json
new file mode 100644
index 0000000..201b4d7
--- /dev/null
+++ b/Assets.xcassets/Paywall/Feadback/feedback.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "AdobeStock_811446718 1.png",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/LockdowniOS.xcodeproj/project.pbxproj b/LockdowniOS.xcodeproj/project.pbxproj
index c5b20cb..99f4d1f 100644
--- a/LockdowniOS.xcodeproj/project.pbxproj
+++ b/LockdowniOS.xcodeproj/project.pbxproj
@@ -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 = ""; };
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 = ""; };
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 = ""; };
+ 51006F182CBD0E7400F5142C /* ImageBannerWithTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageBannerWithTitleView.swift; sourceTree = ""; };
+ 510AA2F52CB8222100E53560 /* FeedbackPaywallViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackPaywallViewModel.swift; sourceTree = ""; };
+ 5117DE872CBD4A6600C4A61B /* SectionTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionTitleView.swift; sourceTree = ""; };
+ 5145A1992CBE37C40074C562 /* FeedbackFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackFlow.swift; sourceTree = ""; };
+ 51DD89E42CB7DA770028B4FE /* FeedbackPaywallViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackPaywallViewController.swift; sourceTree = ""; };
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 = ""; };
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 = ""; };
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 = ""; };
@@ -1251,6 +1261,7 @@
40098E1A29FDA61900886474 /* Paywalls */ = {
isa = PBXGroup;
children = (
+ 51DD89E32CB7D9E10028B4FE /* FeedbackFormPaywall */,
40960AEE2A033A16000F82EB /* Models */,
40098E3329FF376900886474 /* FirewallPaywall */,
40098E2029FDA63100886474 /* VPNPaywall */,
@@ -1742,6 +1753,15 @@
name = ViewController;
sourceTree = "";
};
+ 51DD89E32CB7D9E10028B4FE /* FeedbackFormPaywall */ = {
+ isa = PBXGroup;
+ children = (
+ 51DD89E42CB7DA770028B4FE /* FeedbackPaywallViewController.swift */,
+ 510AA2F52CB8222100E53560 /* FeedbackPaywallViewModel.swift */,
+ );
+ name = FeedbackFormPaywall;
+ sourceTree = "";
+ };
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 = "";
@@ -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 = (
diff --git a/LockdowniOS/AccountVC.swift b/LockdowniOS/AccountVC.swift
index 97e09fb..37810ed 100644
--- a/LockdowniOS/AccountVC.swift
+++ b/LockdowniOS/AccountVC.swift
@@ -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()
}
}
diff --git a/LockdowniOS/AppDelegate.swift b/LockdowniOS/AppDelegate.swift
index 8751330..4e499cc 100644
--- a/LockdowniOS/AppDelegate.swift
+++ b/LockdowniOS/AppDelegate.swift
@@ -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("")
}
diff --git a/LockdowniOS/BaseViewController.swift b/LockdowniOS/BaseViewController.swift
index 1d4e85d..0cfa433 100644
--- a/LockdowniOS/BaseViewController.swift
+++ b/LockdowniOS/BaseViewController.swift
@@ -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 {
diff --git a/LockdowniOS/ConfiguredNavigationView.swift b/LockdowniOS/ConfiguredNavigationView.swift
index cc1ecc7..c99a76f 100644
--- a/LockdowniOS/ConfiguredNavigationView.swift
+++ b/LockdowniOS/ConfiguredNavigationView.swift
@@ -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")
diff --git a/LockdowniOS/DescriptionLabel.swift b/LockdowniOS/DescriptionLabel.swift
index 1a291d4..6713824 100644
--- a/LockdowniOS/DescriptionLabel.swift
+++ b/LockdowniOS/DescriptionLabel.swift
@@ -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
}()
diff --git a/LockdowniOS/FeedbackPaywallViewController.swift b/LockdowniOS/FeedbackPaywallViewController.swift
new file mode 100644
index 0000000..1189b8d
--- /dev/null
+++ b/LockdowniOS/FeedbackPaywallViewController.swift
@@ -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()
+
+ 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")
+}
diff --git a/LockdowniOS/FeedbackPaywallViewModel.swift b/LockdowniOS/FeedbackPaywallViewModel.swift
new file mode 100644
index 0000000..231bab6
--- /dev/null
+++ b/LockdowniOS/FeedbackPaywallViewModel.swift
@@ -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
+ }
+}
diff --git a/LockdowniOS/HomeViewController.swift b/LockdowniOS/HomeViewController.swift
index 96d1b60..985c979 100644
--- a/LockdowniOS/HomeViewController.swift
+++ b/LockdowniOS/HomeViewController.swift
@@ -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)
+ }
}
diff --git a/LockdowniOS/LockdownGradient.swift b/LockdowniOS/LockdownGradient.swift
index 1489d51..df3f878 100644
--- a/LockdowniOS/LockdownGradient.swift
+++ b/LockdowniOS/LockdownGradient.swift
@@ -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))
+
+ }
+ }
}
diff --git a/LockdowniOS/MainTabBarViewController.swift b/LockdowniOS/MainTabBarViewController.swift
index bff40e2..41c5f58 100644
--- a/LockdowniOS/MainTabBarViewController.swift
+++ b/LockdowniOS/MainTabBarViewController.swift
@@ -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" })
diff --git a/LockdowniOS/Scenes/Questionnaire/Controllers/StepsViewController.swift b/LockdowniOS/Scenes/Questionnaire/Controllers/StepsViewController.swift
index 48c6245..4682de9 100644
--- a/LockdowniOS/Scenes/Questionnaire/Controllers/StepsViewController.swift
+++ b/LockdowniOS/Scenes/Questionnaire/Controllers/StepsViewController.swift
@@ -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)
diff --git a/LockdowniOS/Scenes/Questionnaire/FeedbackFlow.swift b/LockdowniOS/Scenes/Questionnaire/FeedbackFlow.swift
new file mode 100644
index 0000000..89b5076
--- /dev/null
+++ b/LockdowniOS/Scenes/Questionnaire/FeedbackFlow.swift
@@ -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)
+ }
+}
diff --git a/LockdowniOS/Scenes/Questionnaire/Models/StepModel.swift b/LockdowniOS/Scenes/Questionnaire/Models/StepModel.swift
index 6346a08..4bbf88a 100644
--- a/LockdowniOS/Scenes/Questionnaire/Models/StepModel.swift
+++ b/LockdowniOS/Scenes/Questionnaire/Models/StepModel.swift
@@ -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: "")
}
}
diff --git a/LockdowniOS/Scenes/Questionnaire/ViewModel/BaseStepViewModel.swift b/LockdowniOS/Scenes/Questionnaire/ViewModel/BaseStepViewModel.swift
index a4c6ff8..7de60a7 100644
--- a/LockdowniOS/Scenes/Questionnaire/ViewModel/BaseStepViewModel.swift
+++ b/LockdowniOS/Scenes/Questionnaire/ViewModel/BaseStepViewModel.swift
@@ -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()
diff --git a/LockdowniOS/Scenes/Questionnaire/ViewModel/QuestionsStepViewModel.swift b/LockdowniOS/Scenes/Questionnaire/ViewModel/QuestionsStepViewModel.swift
index f150740..ec7aa15 100644
--- a/LockdowniOS/Scenes/Questionnaire/ViewModel/QuestionsStepViewModel.swift
+++ b/LockdowniOS/Scenes/Questionnaire/ViewModel/QuestionsStepViewModel.swift
@@ -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))
}
}
}
diff --git a/LockdowniOS/Scenes/Questionnaire/ViewModel/StepsViewModel.swift b/LockdowniOS/Scenes/Questionnaire/ViewModel/StepsViewModel.swift
index 84ba227..76c2757 100644
--- a/LockdowniOS/Scenes/Questionnaire/ViewModel/StepsViewModel.swift
+++ b/LockdowniOS/Scenes/Questionnaire/ViewModel/StepsViewModel.swift
@@ -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
}
diff --git a/LockdowniOS/Scenes/Questionnaire/ViewModel/WhatProblemStepViewModel.swift b/LockdowniOS/Scenes/Questionnaire/ViewModel/WhatProblemStepViewModel.swift
index aae3e47..7f69671 100644
--- a/LockdowniOS/Scenes/Questionnaire/ViewModel/WhatProblemStepViewModel.swift
+++ b/LockdowniOS/Scenes/Questionnaire/ViewModel/WhatProblemStepViewModel.swift
@@ -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, we’ll 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.. 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()
+ }
}
diff --git a/LockdowniOS/UIView+Ext.swift b/LockdowniOS/UIView+Ext.swift
index 641d99b..d2ef658 100644
--- a/LockdowniOS/UIView+Ext.swift
+++ b/LockdowniOS/UIView+Ext.swift
@@ -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
+ }
}
diff --git a/LockdowniOS/VPNSubscription.swift b/LockdowniOS/VPNSubscription.swift
index 11b631a..09626b6 100644
--- a/LockdowniOS/VPNSubscription.swift
+++ b/LockdowniOS/VPNSubscription.swift
@@ -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"