mirror of
https://github.com/confirmedcode/Lockdown-iOS.git
synced 2025-12-21 12:14:02 +01:00
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:
6
Assets.xcassets/Paywall/Feadback/Contents.json
Normal file
6
Assets.xcassets/Paywall/Feadback/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
15
Assets.xcassets/Paywall/Feadback/feedback-checkmark.imageset/Contents.json
vendored
Normal file
15
Assets.xcassets/Paywall/Feadback/feedback-checkmark.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Vector.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"preserves-vector-representation" : true
|
||||
}
|
||||
}
|
||||
92
Assets.xcassets/Paywall/Feadback/feedback-checkmark.imageset/Vector.pdf
vendored
Normal file
92
Assets.xcassets/Paywall/Feadback/feedback-checkmark.imageset/Vector.pdf
vendored
Normal 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
|
||||
3
Assets.xcassets/Paywall/Feadback/feedback-paywall-arrow.imageset/Arrow Ramp Right.svg
vendored
Normal file
3
Assets.xcassets/Paywall/Feadback/feedback-paywall-arrow.imageset/Arrow Ramp Right.svg
vendored
Normal 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 |
15
Assets.xcassets/Paywall/Feadback/feedback-paywall-arrow.imageset/Contents.json
vendored
Normal file
15
Assets.xcassets/Paywall/Feadback/feedback-paywall-arrow.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Arrow Ramp Right.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"preserves-vector-representation" : true
|
||||
}
|
||||
}
|
||||
BIN
Assets.xcassets/Paywall/Feadback/feedback-paywall-banner.imageset/AdobeStock_768605328 1.png
vendored
Normal file
BIN
Assets.xcassets/Paywall/Feadback/feedback-paywall-banner.imageset/AdobeStock_768605328 1.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 401 KiB |
12
Assets.xcassets/Paywall/Feadback/feedback-paywall-banner.imageset/Contents.json
vendored
Normal file
12
Assets.xcassets/Paywall/Feadback/feedback-paywall-banner.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AdobeStock_768605328 1.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Assets.xcassets/Paywall/Feadback/feedback-promo.imageset/AdobeStock_776091887 2 (1).png
vendored
Normal file
BIN
Assets.xcassets/Paywall/Feadback/feedback-promo.imageset/AdobeStock_776091887 2 (1).png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
12
Assets.xcassets/Paywall/Feadback/feedback-promo.imageset/Contents.json
vendored
Normal file
12
Assets.xcassets/Paywall/Feadback/feedback-promo.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AdobeStock_776091887 2 (1).png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Assets.xcassets/Paywall/Feadback/feedback.imageset/AdobeStock_811446718 1.png
vendored
Normal file
BIN
Assets.xcassets/Paywall/Feadback/feedback.imageset/AdobeStock_811446718 1.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
12
Assets.xcassets/Paywall/Feadback/feedback.imageset/Contents.json
vendored
Normal file
12
Assets.xcassets/Paywall/Feadback/feedback.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AdobeStock_811446718 1.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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("")
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}()
|
||||
|
||||
410
LockdowniOS/FeedbackPaywallViewController.swift
Normal file
410
LockdowniOS/FeedbackPaywallViewController.swift
Normal 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")
|
||||
}
|
||||
64
LockdowniOS/FeedbackPaywallViewModel.swift
Normal file
64
LockdowniOS/FeedbackPaywallViewModel.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" })
|
||||
|
||||
@@ -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)
|
||||
|
||||
60
LockdowniOS/Scenes/Questionnaire/FeedbackFlow.swift
Normal file
60
LockdowniOS/Scenes/Questionnaire/FeedbackFlow.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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: "")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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..<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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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),
|
||||
])
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user