task: CP-8314 [iOS] Change T&C URL for Wallet in sign-up

Refs: https://gitlab.protontech.ch/apple/shared/protoncore/-/merge_requests/1856

Changelog: changed
This commit is contained in:
Alex Morral 2024-07-05 17:35:50 +00:00
commit aa90f83548
14 changed files with 128 additions and 51 deletions

View file

@ -542,10 +542,10 @@
</objects>
<point key="canvasLocation" x="2473.913043478261" y="-618.08035714285711"/>
</scene>
<!--View Controller-->
<!--External Link View Controller-->
<scene sceneID="dI1-bc-aG0">
<objects>
<viewController storyboardIdentifier="TC" id="mgS-ty-qip" customClass="TCViewController" customModule="ProtonCoreLoginUI" sceneMemberID="viewController">
<viewController storyboardIdentifier="ExternalLink" id="mgS-ty-qip" customClass="ExternalLinkViewController" customModule="ProtonCoreLoginUI" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="6g4-vM-jwm">
<rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>

View file

@ -133,7 +133,7 @@ final class Container {
}
func makePasswordViewModel() -> PasswordViewModel {
return PasswordViewModel()
return PasswordViewModel(clientApp: clientApp)
}
func makeRecoveryViewModel(initialCountryCode: Int) -> RecoveryViewModel {

View file

@ -283,11 +283,27 @@ final class SignupCoordinator {
}
private func showTermsAndConditionsViewController() {
let tcViewController = UIStoryboard.instantiateInSignup(TCViewController.self, inAppTheme: customization.inAppTheme)
tcViewController.termsAndConditionsURL = externalLinks.termsAndConditions
tcViewController.delegate = self
let elViewController = UIStoryboard.instantiateInSignup(ExternalLinkViewController.self, inAppTheme: customization.inAppTheme)
elViewController.configuration = .init(
title: LUITranslation.terms_conditions_view_title.l10n,
url: externalLinks.termsAndConditions
)
elViewController.delegate = self
let navigationVC = LoginNavigationViewController(rootViewController: tcViewController)
let navigationVC = LoginNavigationViewController(rootViewController: elViewController)
navigationVC.modalPresentationStyle = .pageSheet
navigationController?.present(navigationVC, animated: true)
}
private func showPrivacyPolicyViewController() {
let elViewController = UIStoryboard.instantiateInSignup(ExternalLinkViewController.self, inAppTheme: customization.inAppTheme)
elViewController.configuration = .init(
title: LUITranslation.privacy_policy_view_title.l10n,
url: externalLinks.privacyPolicy
)
elViewController.delegate = self
let navigationVC = LoginNavigationViewController(rootViewController: elViewController)
navigationVC.modalPresentationStyle = .pageSheet
navigationController?.present(navigationVC, animated: true)
}
@ -523,6 +539,10 @@ extension SignupCoordinator: PasswordViewControllerDelegate {
func termsAndConditionsLinkPressed() {
showTermsAndConditionsViewController()
}
func privacyPolicyLinkPressed() {
showPrivacyPolicyViewController()
}
}
// MARK: RecoveryViewControllerDelegate
@ -669,8 +689,8 @@ extension SignupCoordinator: CompleteViewControllerDelegate {
// MARK: TCViewControllerDelegate
extension SignupCoordinator: TCViewControllerDelegate {
func termsAndConditionsClose() {
extension SignupCoordinator: ExternalLinkViewControllerDelegate {
func externalLinkViewControllerClose() {
navigationController?.dismiss(animated: true)
}
}

View file

@ -51,7 +51,16 @@ final class ExternalLinks {
}
var termsAndConditions: URL {
return URL(string: "https://proton.me/legal/terms-ios")!
switch clientApp {
case .wallet:
return URL(string: "https://proton.me/leqal/wallet/terms")!
default:
return URL(string: "https://proton.me/legal/terms-ios")!
}
}
var privacyPolicy: URL {
return URL(string: "https://proton.me/wallet/privacy-policy")!
}
var support: URL {

View file

@ -103,6 +103,10 @@ public enum LUITranslation: TranslationsExposing {
case password_view_title
case password_field_minimum_length_hint
case repeat_password_field_title
case password_t_c_desc
case password_t_c_link
case password_t_c_wallet_desc
case password_p_p_link
case domains_sheet_title
case recovery_view_title
case recovery_view_title_optional
@ -111,8 +115,6 @@ public enum LUITranslation: TranslationsExposing {
case recovery_seg_phone
case recovery_email_field_title
case recovery_phone_field_title
case recovery_t_c_desc
case recovery_t_c_link
case skip_button
case recovery_skip_title
case recovery_skip_desc
@ -129,6 +131,7 @@ public enum LUITranslation: TranslationsExposing {
case email_verification_code_desc
case did_not_receive_code_button
case terms_conditions_view_title
case privacy_policy_view_title
case error_invalid_token_request
case error_invalid_token
case error_create_user_failed
@ -290,6 +293,14 @@ public enum LUITranslation: TranslationsExposing {
return localized(key: "Password must contain at least 8 characters", comment: "Password field hint about minimum length")
case .repeat_password_field_title:
return localized(key: "Repeat password", comment: "Repeat password field title")
case .password_t_c_desc:
return localized(key: "By clicking Next, you agree with Proton's Terms and Conditions", comment: "Password terms and conditions description")
case .password_t_c_link:
return localized(key: "Terms and Conditions", comment: "Password terms and conditions link")
case .password_t_c_wallet_desc:
return localized(key: "By clicking Next, you agree with Proton's Terms and Conditions and Privacy Policy", comment: "Password terms and conditions description")
case .password_p_p_link:
return localized(key: "Privacy Policy", comment: "Password privacy policy link")
case .domains_sheet_title:
return localized(key: "Domain", comment: "Title of domains bottom action sheet")
case .recovery_view_title:
@ -306,10 +317,6 @@ public enum LUITranslation: TranslationsExposing {
return localized(key: "Recovery email", comment: "Recovery email field title")
case .recovery_phone_field_title:
return localized(key: "Recovery phone number", comment: "Recovery phone field title")
case .recovery_t_c_desc:
return localized(key: "By clicking Next, you agree with Proton's Terms and Conditions", comment: "Recovery terms and conditions description")
case .recovery_t_c_link:
return localized(key: "Terms and Conditions", comment: "Recovery terms and conditions link")
case .skip_button:
return localized(key: "Skip", comment: "Skip button")
case .recovery_skip_title:
@ -342,6 +349,8 @@ public enum LUITranslation: TranslationsExposing {
return localized(key: "Did not receive a code?", comment: "Did not receive code button")
case .terms_conditions_view_title:
return localized(key: "Terms and Conditions", comment: "Terms and conditions view title")
case .privacy_policy_view_title:
return localized(key: "Privacy Policy", comment: "Privacy policy view title")
case .error_invalid_token_request:
return localized(key: "Invalid token request", comment: "Invalid token request error")
case .error_invalid_token:

View file

@ -144,8 +144,12 @@
"By clicking Next, you agree with Proton's Terms and Conditions" = "By clicking Next, you agree with Proton's Terms and Conditions";
"By clicking Next, you agree with Proton's Terms and Conditions and Privacy Policy" = "By clicking Next, you agree with Proton's Terms and Conditions and Privacy Policy";
"Terms and Conditions" = "Terms and Conditions";
"Privacy Policy" = "Privacy Policy";
"Skip" = "Skip";
"Skip recovery method?" = "Skip recovery method?";
@ -178,6 +182,8 @@
"Terms and Conditions" = "Terms and Conditions";
"Privacy Policy" = "Privacy Policy";
"Invalid token request" = "Invalid token request";
"Invalid token error" = "Invalid token error";

View file

@ -1,5 +1,5 @@
//
// TCViewController.swift
// ExternalLinkViewController.swift
// ProtonCore-Login - Created on 11/03/2021.
//
// Copyright (c) 2022 Proton Technologies AG
@ -26,14 +26,19 @@ import WebKit
import ProtonCoreFoundations
import ProtonCoreUIFoundations
protocol TCViewControllerDelegate: AnyObject {
func termsAndConditionsClose()
protocol ExternalLinkViewControllerDelegate: AnyObject {
func externalLinkViewControllerClose()
}
class TCViewController: UIViewController, AccessibleView {
struct ExternalLinkConfiguration {
let title: String
let url: URL
}
weak var delegate: TCViewControllerDelegate?
var termsAndConditionsURL: URL?
class ExternalLinkViewController: UIViewController, AccessibleView {
weak var delegate: ExternalLinkViewControllerDelegate?
var configuration: ExternalLinkConfiguration?
override var preferredStatusBarStyle: UIStatusBarStyle { darkModeAwarePreferredStatusBarStyle() }
@ -46,9 +51,9 @@ class TCViewController: UIViewController, AccessibleView {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = ColorProvider.BackgroundNorm
navigationItem.title = LUITranslation.terms_conditions_view_title.l10n
navigationItem.title = configuration?.title
navigationController?.navigationBar.tintColor = ColorProvider.IconNorm
setUpCloseButton(showCloseButton: true, action: #selector(TCViewController.onCloseButtonTap(_:)))
setUpCloseButton(showCloseButton: true, action: #selector(ExternalLinkViewController.onCloseButtonTap(_:)))
setupWebView()
generateAccessibilityIdentifiers()
updateTitleAttributes()
@ -57,20 +62,20 @@ class TCViewController: UIViewController, AccessibleView {
// MARK: Actions
@objc func onCloseButtonTap(_ sender: UIButton) {
delegate?.termsAndConditionsClose()
delegate?.externalLinkViewControllerClose()
}
// MARK: Private methods
func setupWebView() {
webView.navigationDelegate = self
guard let url = self.termsAndConditionsURL else { return }
guard let url = configuration?.url else { return }
let request = URLRequest(url: url, cachePolicy: URLRequest.CachePolicy.reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 20.0)
webView.load(request)
}
}
extension TCViewController: WKNavigationDelegate {
extension ExternalLinkViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
@ -80,7 +85,7 @@ extension TCViewController: WKNavigationDelegate {
}
// promise webview won't navigate to other link
if url == termsAndConditionsURL?.absoluteString {
if url == configuration?.url.absoluteString {
decisionHandler(.allow)
} else {
decisionHandler(.cancel)

View file

@ -26,12 +26,14 @@ import ProtonCoreFoundations
import ProtonCoreUIFoundations
import ProtonCoreObservability
import ProtonCoreTelemetry
import ProtonCoreUtilities
protocol PasswordViewControllerDelegate: AnyObject {
func passwordIsShown()
func validatedPassword(password: String, completionHandler: (() -> Void)?)
func passwordBackButtonPressed()
func termsAndConditionsLinkPressed()
func privacyPolicyLinkPressed()
}
class PasswordViewController: UIViewController, AccessibleView, Focusable, ProductMetricsMeasurable {
@ -284,8 +286,20 @@ extension PasswordViewController: SignUpErrorCapable, LoginErrorCapable {
extension PasswordViewController: UITextViewDelegate {
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
delegate?.termsAndConditionsLinkPressed()
measureOnViewClicked(item: "terms")
guard let stringRange = Range(characterRange, in: textView.text) else {
return false
}
switch textView.text[stringRange] {
case LUITranslation.password_t_c_link.l10n:
delegate?.termsAndConditionsLinkPressed()
measureOnViewClicked(item: "terms")
case LUITranslation.password_p_p_link.l10n:
delegate?.privacyPolicyLinkPressed()
measureOnViewClicked(item: "privacy_policy")
default:
break
}
return false
}
}

View file

@ -23,9 +23,15 @@
import Foundation
import ProtonCoreLogin
import ProtonCoreDataModel
import UIKit
class PasswordViewModel {
let clientApp: ClientApp
init(clientApp: ClientApp) {
self.clientApp = clientApp
}
func passwordValidationResult(for restrictions: SignupPasswordRestrictions,
password: String,
@ -57,18 +63,25 @@ class PasswordViewModel {
}
func termsAttributedString(textView: UITextView) -> NSAttributedString {
/// Fix me poissble bug: if _login_recovery_t_c_desc translated string doesnt match in _login_recovery_t_c_link translated string. the hyper link could be failed when clicking.
var text = LUITranslation.recovery_t_c_desc.l10n
let linkText = LUITranslation.recovery_t_c_link.l10n
if ProcessInfo.processInfo.arguments.contains("RunningInUITests") {
// Workaround for UI test automation to detect link in separated line
let texts = text.components(separatedBy: linkText)
if texts.count >= 2 {
text = texts[0] + "\n" + linkText + texts[1]
switch clientApp {
case .wallet:
let text = NSMutableAttributedString(string: LUITranslation.password_t_c_wallet_desc.l10n)
text.addHyperLink(subString: LUITranslation.password_t_c_link.l10n, link: "", font: textView.font)
text.addHyperLink(subString: LUITranslation.password_p_p_link.l10n, link: "", font: textView.font)
return text
default:
var text = LUITranslation.password_t_c_desc.l10n
let linkText = LUITranslation.password_t_c_link.l10n
if ProcessInfo.processInfo.arguments.contains("RunningInUITests") {
// Workaround for UI test automation to detect link in separated line
let texts = text.components(separatedBy: linkText)
if texts.count >= 2 {
text = texts[0] + "\n" + linkText + texts[1]
}
}
}
return .hyperlink(in: text, as: linkText, path: "", subfont: textView.font)
return .hyperlink(in: text, as: linkText, path: "", subfont: textView.font)
}
}
}

View file

@ -70,7 +70,7 @@ class SignupScreenLoadObservabilityTests: SnapshotTestCase {
let passwordViewController = UIStoryboard.instantiate(storyboardName: "PMSignup",
controllerType: PasswordViewController.self,
inAppTheme: { .default })
passwordViewController.viewModel = PasswordViewModel()
passwordViewController.viewModel = PasswordViewModel(clientApp: .other(named: "core-unit-tests"))
_ = passwordViewController.view
XCTAssertTrue(stub.reportStub.wasCalledExactlyOnce)
XCTAssertTrue(stub.reportStub.lastArguments!.value.isSameAs(event: .screenLoadCountTotal(screenName: .passwordCreation)))

View file

@ -34,8 +34,9 @@ import ProtonCoreUIFoundations
class TCSnapshotTests: SnapshotTestCase {
func testTCViewControllerScreen() {
let tcViewController = UIStoryboard.instantiate(storyboardName: "PMSignup", controllerType: TCViewController.self, inAppTheme: { .default })
let navigationViewController = LoginNavigationViewController(rootViewController: tcViewController)
let elViewController = UIStoryboard.instantiate(storyboardName: "PMSignup", controllerType: ExternalLinkViewController.self, inAppTheme: { .default })
elViewController.configuration = .init(title: LUITranslation.terms_conditions_view_title.l10n, url: URL(string: "https://proton.me")!)
let navigationViewController = LoginNavigationViewController(rootViewController: elViewController)
checkSnapshots(controller: navigationViewController, perceptualPrecision: 0.98)
}
}

View file

@ -30,7 +30,7 @@ import XCTest
class PasswordViewModelTests: XCTestCase {
func testPasswordOK1() throws {
let viewModel = PasswordViewModel()
let viewModel = PasswordViewModel(clientApp: .other(named: "core-unit-tests"))
let result = viewModel.passwordValidationResult(for: .notEmpty, password: "a", repeatParrword: "a")
switch result {
case .success:
@ -41,7 +41,7 @@ class PasswordViewModelTests: XCTestCase {
}
func testPasswordOK2() throws {
let viewModel = PasswordViewModel()
let viewModel = PasswordViewModel(clientApp: .other(named: "core-unit-tests"))
let result = viewModel.passwordValidationResult(for: .notEmpty, password: "fhhjdhjdhjdhjhdjhddssaww@#$", repeatParrword: "fhhjdhjdhjdhjhdjhddssaww@#$")
switch result {
case .success:
@ -52,7 +52,7 @@ class PasswordViewModelTests: XCTestCase {
}
func testPasswordEmpty() throws {
let viewModel = PasswordViewModel()
let viewModel = PasswordViewModel(clientApp: .other(named: "core-unit-tests"))
let result = viewModel.passwordValidationResult(for: .notEmpty, password: "", repeatParrword: "")
switch result {
case .success:
@ -63,7 +63,7 @@ class PasswordViewModelTests: XCTestCase {
}
func testPasswordNotEqual1() throws {
let viewModel = PasswordViewModel()
let viewModel = PasswordViewModel(clientApp: .other(named: "core-unit-tests"))
let result = viewModel.passwordValidationResult(for: .notEmpty, password: "aa", repeatParrword: "bb")
switch result {
case .success:
@ -74,7 +74,7 @@ class PasswordViewModelTests: XCTestCase {
}
func testPasswordNotEqual2() throws {
let viewModel = PasswordViewModel()
let viewModel = PasswordViewModel(clientApp: .other(named: "core-unit-tests"))
let result = viewModel.passwordValidationResult(for: .notEmpty, password: "", repeatParrword: "bb")
switch result {
case .success:
@ -85,7 +85,7 @@ class PasswordViewModelTests: XCTestCase {
}
func testPasswordNotEqual3() throws {
let viewModel = PasswordViewModel()
let viewModel = PasswordViewModel(clientApp: .other(named: "core-unit-tests"))
let result = viewModel.passwordValidationResult(for: .notEmpty, password: "c", repeatParrword: "")
switch result {
case .success:

View file

@ -38,7 +38,7 @@ private let recoveryDialogTitleName = LUITranslation.recovery_skip_title.l10n
private let recoveryDialogMessageName = LUITranslation.recovery_skip_desc.l10n
private let recoveryDialogSkipButtonAccessibilityId = "DialogSkipButton"
private let recoveryDialogRecoveryButtonAccessibilityId = "DialogRecoveryMethodButton"
private let linkString = LUITranslation.recovery_t_c_link.l10n
private let linkString = LUITranslation.password_t_c_link.l10n
private let errorBannerHVRequired = "Human verification required"
private let errorBannerInvalidNumber = "Phone number failed validation"
private let errorBannerButton = LUITranslation._core_ok_button.l10n

View file

@ -61,7 +61,7 @@ extension String {
}
extension String {
public extension String {
subscript(value: Int) -> Character {
self[index(at: value)]