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
210 lines
8.6 KiB
Swift
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
|
|
}
|
|
}
|