deep-linking-sample/Apps/Wikipedia/WMF Framework/WMFCaptchaViewController.swift
Javier Cicchelli 9bcdaa697b [Setup] Basic project structure (#1)
This PR contains all the work related to setting up this project as required to implement the [Assignment](https://repo.rock-n-code.com/rock-n-code/deep-linking-assignment/wiki/Assignment) on top, as intended.

To summarise this work:
- [x] created a new **Xcode** project;
- [x] cloned the `Wikipedia` app and inserted it into the **Xcode** project;
- [x] created the `Locations` app and also, its `Libraries` package;
- [x] created the `Shared` package to share dependencies between the apps;
- [x] added a `Makefile` file and implemented some **environment** and **help** commands.

Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Reviewed-on: rock-n-code/deep-linking-assignment#1
2023-04-08 18:37:13 +00:00

210 lines
8.6 KiB
Swift

import UIKit
// Presently it is assumed this view controller will be used only as a
// child view controller of another view controller.
extension UIStackView {
fileprivate var wmf_isCollapsed: Bool {
get {
for subview in arrangedSubviews {
if !subview.isHidden {
return false
}
}
return true
}
set {
for subview in arrangedSubviews {
subview.isHidden = newValue
}
}
}
}
@objc public protocol WMFCaptchaViewControllerDelegate {
func captchaReloadPushed(_ sender: AnyObject)
func captchaSolutionChanged(_ sender: AnyObject, solutionText: String?)
func captchaSiteURL() -> URL
func captchaKeyboardReturnKeyTapped()
func captchaHideSubtitle() -> Bool
}
class WMFCaptchaViewController: UIViewController, UITextFieldDelegate, Themeable {
@IBOutlet fileprivate var captchaImageView: UIImageView!
@IBOutlet fileprivate var captchaTextFieldTitleLabel: UILabel!
@IBOutlet fileprivate var captchaTextField: ThemeableTextField!
@IBOutlet fileprivate var stackView: UIStackView!
@IBOutlet fileprivate var titleLabel: UILabel!
@IBOutlet fileprivate var subTitleLabel: WMFAuthLinkLabel!
@IBOutlet fileprivate var infoButton: UIButton!
@IBOutlet fileprivate var refreshButton: UIButton!
@objc public weak var captchaDelegate: WMFCaptchaViewControllerDelegate?
fileprivate let captchaResetter = WMFCaptchaResetter()
fileprivate var theme = Theme.standard
@objc var captcha: WMFCaptcha? {
didSet {
guard let captcha = captcha else {
captchaTextField.text = nil
stackView.wmf_isCollapsed = true
return
}
stackView.wmf_isCollapsed = false
captchaTextField.text = ""
refreshImage(for: captcha)
if let captchaDelegate = captchaDelegate {
subTitleLabel.isHidden = captchaDelegate.captchaHideSubtitle()
}
}
}
@objc var solution:String? {
guard
let captchaSolution = captchaTextField.text,
!captchaSolution.isEmpty
else {
return nil
}
return captchaTextField.text
}
public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if textField == captchaTextField {
guard let captchaDelegate = captchaDelegate else {
assertionFailure("Expected delegate not set")
return true
}
captchaDelegate.captchaKeyboardReturnKeyTapped()
}
return true
}
fileprivate func refreshImage(for captcha: WMFCaptcha) {
guard let fullCaptchaImageURL = fullCaptchaImageURL(from: captcha.captchaURL) else {
assertionFailure("Unable to determine fullCaptchaImageURL")
return
}
captchaImageView.wmf_setImage(with: fullCaptchaImageURL, detectFaces: false, onGPU: false, failure: { (error) in
}) {
}
}
public func captchaTextFieldBecomeFirstResponder() {
// Reminder: captchaTextField is private so this vc maintains control over the captcha solution.
captchaTextField.becomeFirstResponder()
}
fileprivate func fullCaptchaImageURL(from captchaURL: URL) -> URL? {
guard let components = URLComponents(url: captchaURL, resolvingAgainstBaseURL: false) else {
assertionFailure("Could not extract url components")
return nil
}
return components.url(relativeTo: captchaBaseURL())
}
fileprivate func captchaBaseURL() -> URL? {
guard let captchaDelegate = captchaDelegate else {
assertionFailure("Expected delegate not set")
return nil
}
let captchaSiteURL = captchaDelegate.captchaSiteURL()
guard var components = URLComponents(url: captchaSiteURL, resolvingAgainstBaseURL: false) else {
assertionFailure("Could not extract url components")
return nil
}
components.queryItems = nil
guard let url = components.url else {
assertionFailure("Could not extract url")
return nil
}
return url
}
@IBAction func textFieldDidChange(_ sender: UITextField) {
captchaDelegate?.captchaSolutionChanged(self, solutionText:sender.text)
}
override var prefersStatusBarHidden: Bool {
return true
}
override func viewDidLoad() {
super.viewDidLoad()
guard let captchaDelegate = captchaDelegate else {
assertionFailure("Required delegate is unset")
return
}
assert(stackView.wmf_firstArrangedSubviewWithRequiredNonZeroHeightConstraint() == nil, stackView.wmf_anArrangedSubviewHasRequiredNonZeroHeightConstraintAssertString())
captcha = nil
captchaTextFieldTitleLabel.text = WMFLocalizedString("field-captcha-title", value:"Enter the text you see above", comment: "Title for captcha field")
captchaTextField.placeholder = WMFLocalizedString("field-captcha-placeholder", value:"CAPTCHA text", comment: "Placeholder text shown inside captcha field until user taps on it")
titleLabel.text = WMFLocalizedString("account-creation-captcha-title", value:"CAPTCHA security check", comment: "Title for account creation CAPTCHA interface")
// Reminder: used a label instead of a button for subtitle because of multi-line string issues with UIButton.
subTitleLabel.strings = WMFAuthLinkLabelStrings(dollarSignString: WMFLocalizedString("account-creation-captcha-cannot-see-image", value:"Can't see the image? %1$@", comment: "Text asking the user if they cannot see the captcha image. %1$@ is the message {{msg-wm|Wikipedia-ios-account-creation-captcha-request-account}}"), substitutionString: WMFLocalizedString("account-creation-captcha-request-account", value:"Request account.", comment: "Text for link to 'Request an account' page."))
subTitleLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(requestAnAccountTapped(_:))))
subTitleLabel.isHidden = (captcha == nil) || captchaDelegate.captchaHideSubtitle()
view.wmf_configureSubviewsForDynamicType()
apply(theme: theme)
}
@objc func requestAnAccountTapped(_ recognizer: UITapGestureRecognizer) {
navigate(to: URL(string: "https://en.wikipedia.org/wiki/Wikipedia:Request_an_account"))
}
@IBAction fileprivate func infoButtonTapped(withSender sender: UIButton) {
navigate(to: URL(string: "https://en.wikipedia.org/wiki/Special:Captcha/help"))
}
@IBAction fileprivate func refreshButtonTapped(withSender sender: UIButton) {
captchaDelegate?.captchaReloadPushed(self)
let failure: WMFErrorHandler = {error in }
WMFAlertManager.sharedInstance.showAlert(WMFLocalizedString("account-creation-captcha-obtaining", value:"Obtaining a new CAPTCHA...", comment: "Alert shown when user wants a new captcha when creating account"), sticky: false, dismissPreviousAlerts: true, tapCallBack: nil)
self.captchaResetter.resetCaptcha(siteURL: captchaBaseURL()!, success: { result in
DispatchQueue.main.async {
guard let previousCaptcha = self.captcha else {
assertionFailure("If resetting a captcha, expect to have a previous one here")
return
}
let previousCaptchaURL = previousCaptcha.captchaURL
let previousCaptchaNSURL = previousCaptchaURL as NSURL
let newCaptchaID = result.index
// Resetter only fetches captchaID, so use previous captchaURL changing its wpCaptchaId.
let newCaptchaURL = previousCaptchaNSURL.wmf_url(withValue: newCaptchaID, forQueryKey:"wpCaptchaId")
let newCaptcha = WMFCaptcha.init(captchaID: newCaptchaID, captchaURL: newCaptchaURL)
self.captcha = newCaptcha
}
}, failure:failure)
}
func apply(theme: Theme) {
self.theme = theme
guard viewIfLoaded != nil else {
return
}
titleLabel.textColor = theme.colors.primaryText
captchaTextFieldTitleLabel.textColor = theme.colors.secondaryText
subTitleLabel.apply(theme: theme)
view.backgroundColor = theme.colors.paperBackground
captchaTextField.apply(theme: theme)
view.tintColor = theme.colors.link
}
}