mirror of https://github.com/oxen-io/session-ios
Sketch out the 'onboarding phone number' view.
parent
f25e54f58b
commit
2a4b9426c3
@ -0,0 +1,514 @@
|
||||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import PromiseKit
|
||||
|
||||
@objc
|
||||
public class OnboardingPhoneNumberViewController: OnboardingBaseViewController {
|
||||
|
||||
// MARK: - Dependencies
|
||||
|
||||
private var tsAccountManager: TSAccountManager {
|
||||
return TSAccountManager.sharedInstance()
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private let countryNameLabel = UILabel()
|
||||
private let callingCodeLabel = UILabel()
|
||||
private let phoneNumberTextField = UITextField()
|
||||
private var nextButton: OWSFlatButton?
|
||||
|
||||
// - (void)didTapLegalTerms:(UIButton *)sender
|
||||
//{
|
||||
// [[UIApplication sharedApplication] openURL:[NSURL URLWithString:kLegalTermsUrlString]];
|
||||
// }
|
||||
//#pragma mark - Keyboard notifications
|
||||
//
|
||||
//- (void)initializeKeyboardHandlers
|
||||
//{
|
||||
// UITapGestureRecognizer *outsideTabRecognizer =
|
||||
// [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dismissKeyboardFromAppropriateSubView)];
|
||||
// [self.view addGestureRecognizer:outsideTabRecognizer];
|
||||
// }
|
||||
//
|
||||
// - (void)dismissKeyboardFromAppropriateSubView
|
||||
// {
|
||||
// [self.view endEditing:NO];
|
||||
//}
|
||||
//
|
||||
|
||||
override public func loadView() {
|
||||
super.loadView()
|
||||
|
||||
// TODO: Is this still necessary?
|
||||
if let navigationController = self.navigationController as? OWSNavigationController {
|
||||
SignalApp.shared().signUpFlowNavigationController = navigationController
|
||||
} else {
|
||||
owsFailDebug("Missing or invalid navigationController")
|
||||
}
|
||||
|
||||
populateDefaults()
|
||||
|
||||
view.backgroundColor = Theme.backgroundColor
|
||||
view.layoutMargins = .zero
|
||||
|
||||
// TODO:
|
||||
// navigationItem.title = NSLocalizedString("SETTINGS_BACKUP", comment: "Label for the backup view in app settings.")
|
||||
|
||||
let titleLabel = self.titleLabel(text: NSLocalizedString("ONBOARDING_PHONE_NUMBER_TITLE", comment: "Title of the 'onboarding phone number' view."))
|
||||
|
||||
// Country
|
||||
|
||||
// TODO: dynamic
|
||||
let fontSizePoints: CGFloat = ScaleFromIPhone5To7Plus(16, 20)
|
||||
let rowHeight: CGFloat = 40
|
||||
|
||||
countryNameLabel.textColor = Theme.primaryColor
|
||||
countryNameLabel.font = UIFont.ows_dynamicTypeBody
|
||||
countryNameLabel.setContentHuggingHorizontalLow()
|
||||
countryNameLabel.setCompressionResistanceHorizontalLow()
|
||||
|
||||
let countryIcon = UIImage(named: (CurrentAppContext().isRTL
|
||||
? "small_chevron_left"
|
||||
: "small_chevron_right"))
|
||||
// NavBarBackRTL
|
||||
// small_chevron_right
|
||||
// system_disclosure_indicator_rtl
|
||||
let countryImageView = UIImageView(image: countryIcon?.withRenderingMode(.alwaysTemplate))
|
||||
countryImageView.tintColor = Theme.placeholderColor
|
||||
countryImageView.setContentHuggingHigh()
|
||||
countryImageView.setCompressionResistanceHigh()
|
||||
|
||||
let countryRow = UIStackView(arrangedSubviews: [
|
||||
countryNameLabel,
|
||||
countryImageView
|
||||
])
|
||||
countryRow.axis = .horizontal
|
||||
countryRow.alignment = .center
|
||||
countryRow.spacing = 10
|
||||
countryRow.isUserInteractionEnabled = true
|
||||
countryRow.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(countryRowTapped)))
|
||||
countryRow.autoSetDimension(.height, toSize: rowHeight)
|
||||
addBottomStroke(countryRow)
|
||||
|
||||
callingCodeLabel.textColor = Theme.primaryColor
|
||||
callingCodeLabel.font = UIFont.ows_dynamicTypeBody
|
||||
callingCodeLabel.setContentHuggingHorizontalHigh()
|
||||
callingCodeLabel.setCompressionResistanceHorizontalHigh()
|
||||
callingCodeLabel.isUserInteractionEnabled = true
|
||||
callingCodeLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(countryCodeTapped)))
|
||||
addBottomStroke(callingCodeLabel)
|
||||
callingCodeLabel.autoSetDimension(.width, toSize: rowHeight, relation: .greaterThanOrEqual)
|
||||
|
||||
phoneNumberTextField.textAlignment = .left
|
||||
phoneNumberTextField.delegate = self
|
||||
phoneNumberTextField.keyboardType = .numberPad
|
||||
phoneNumberTextField.textColor = Theme.primaryColor
|
||||
phoneNumberTextField.font = UIFont.ows_dynamicTypeBody
|
||||
phoneNumberTextField.setContentHuggingHorizontalLow()
|
||||
phoneNumberTextField.setCompressionResistanceHorizontalLow()
|
||||
|
||||
addBottomStroke(phoneNumberTextField)
|
||||
|
||||
let phoneNumberRow = UIStackView(arrangedSubviews: [
|
||||
callingCodeLabel,
|
||||
phoneNumberTextField
|
||||
])
|
||||
phoneNumberRow.axis = .horizontal
|
||||
phoneNumberRow.alignment = .fill
|
||||
phoneNumberRow.spacing = 10
|
||||
phoneNumberRow.autoSetDimension(.height, toSize: rowHeight)
|
||||
callingCodeLabel.autoMatch(.height, to: .height, of: phoneNumberTextField)
|
||||
|
||||
// TODO: Finalize copy.
|
||||
|
||||
let nextButton = self.button(title: NSLocalizedString("BUTTON_NEXT",
|
||||
comment: "Label for the 'next' button."),
|
||||
selector: #selector(nextPressed))
|
||||
self.nextButton = nextButton
|
||||
let topSpacer = UIView.vStretchingSpacer()
|
||||
let bottomSpacer = UIView.vStretchingSpacer()
|
||||
|
||||
let stackView = UIStackView(arrangedSubviews: [
|
||||
titleLabel,
|
||||
topSpacer,
|
||||
countryRow,
|
||||
UIView.spacer(withHeight: 8),
|
||||
phoneNumberRow,
|
||||
bottomSpacer,
|
||||
nextButton
|
||||
])
|
||||
stackView.axis = .vertical
|
||||
stackView.alignment = .fill
|
||||
stackView.layoutMargins = UIEdgeInsets(top: 32, left: 32, bottom: 32, right: 32)
|
||||
stackView.isLayoutMarginsRelativeArrangement = true
|
||||
view.addSubview(stackView)
|
||||
stackView.autoPinWidthToSuperviewMargins()
|
||||
stackView.autoPinWidthToSuperviewMargins()
|
||||
stackView.autoPin(toTopLayoutGuideOf: self, withInset: 0)
|
||||
autoPinView(toBottomOfViewControllerOrKeyboard: stackView, avoidNotch: true)
|
||||
|
||||
// Ensure whitespace is balanced, so inputs are vertically centered.
|
||||
topSpacer.autoMatch(.height, to: .height, of: bottomSpacer)
|
||||
}
|
||||
|
||||
private func addBottomStroke(_ view: UIView) {
|
||||
let strokeView = UIView()
|
||||
strokeView.backgroundColor = Theme.middleGrayColor
|
||||
view.addSubview(strokeView)
|
||||
strokeView.autoSetDimension(.height, toSize: CGHairlineWidth())
|
||||
strokeView.autoPinWidthToSuperview()
|
||||
strokeView.autoPinEdge(toSuperviewEdge: .bottom)
|
||||
}
|
||||
|
||||
public override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
self.navigationController?.isNavigationBarHidden = false
|
||||
|
||||
phoneNumberTextField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
public override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
self.navigationController?.isNavigationBarHidden = false
|
||||
|
||||
phoneNumberTextField.becomeFirstResponder()
|
||||
|
||||
if tsAccountManager.isReregistering() {
|
||||
// If re-registering, pre-populate the country (country code, calling code, country name)
|
||||
// and phone number state.
|
||||
guard let phoneNumberE164 = tsAccountManager.reregisterationPhoneNumber() else {
|
||||
owsFailDebug("Could not resume re-registration; missing phone number.")
|
||||
return
|
||||
}
|
||||
tryToReregister(phoneNumberE164: phoneNumberE164)
|
||||
}
|
||||
}
|
||||
|
||||
private func tryToReregister(phoneNumberE164: String) {
|
||||
guard phoneNumberE164.count > 0 else {
|
||||
owsFailDebug("Could not resume re-registration; invalid phoneNumberE164.")
|
||||
return
|
||||
}
|
||||
guard let parsedPhoneNumber = PhoneNumber(fromE164: phoneNumberE164) else {
|
||||
owsFailDebug("Could not resume re-registration; couldn't parse phoneNumberE164.")
|
||||
return
|
||||
}
|
||||
guard let callingCode = parsedPhoneNumber.getCountryCode() else {
|
||||
owsFailDebug("Could not resume re-registration; missing callingCode.")
|
||||
return
|
||||
}
|
||||
let callingCodeText = "\(COUNTRY_CODE_PREFIX)\(callingCode)"
|
||||
let countryCodes: [String] =
|
||||
PhoneNumberUtil.sharedThreadLocal().countryCodes(fromCallingCode: callingCodeText)
|
||||
guard let countryCode = countryCodes.first else {
|
||||
owsFailDebug("Could not resume re-registration; unknown countryCode.")
|
||||
return
|
||||
}
|
||||
guard let countryName = PhoneNumberUtil.countryName(fromCountryCode: countryCode) else {
|
||||
owsFailDebug("Could not resume re-registration; unknown countryName.")
|
||||
return
|
||||
}
|
||||
if !phoneNumberE164.hasPrefix(callingCodeText) {
|
||||
owsFailDebug("Could not resume re-registration; non-matching calling code.")
|
||||
return
|
||||
}
|
||||
let phoneNumberWithoutCallingCode = phoneNumberE164.substring(from: callingCodeText.count)
|
||||
|
||||
update(withCountryName: countryName, callingCode: callingCodeText, countryCode: countryCode)
|
||||
|
||||
phoneNumberTextField.text = phoneNumberWithoutCallingCode
|
||||
// Don't let user edit their phone number while re-registering.
|
||||
phoneNumberTextField.isEnabled = false
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private var countryName = ""
|
||||
private var callingCode = ""
|
||||
private var countryCode = ""
|
||||
|
||||
private func populateDefaults() {
|
||||
|
||||
var countryCode: String = PhoneNumber.defaultCountryCode()
|
||||
if let lastRegisteredCountryCode = self.lastRegisteredCountryCode(),
|
||||
lastRegisteredCountryCode.count > 0 {
|
||||
countryCode = lastRegisteredCountryCode
|
||||
}
|
||||
|
||||
let callingCodeNumber: NSNumber = PhoneNumberUtil.sharedThreadLocal().nbPhoneNumberUtil.getCountryCode(forRegion: countryCode)
|
||||
let callingCode = "\(COUNTRY_CODE_PREFIX)\(callingCodeNumber)"
|
||||
|
||||
if let lastRegisteredPhoneNumber = self.lastRegisteredPhoneNumber(),
|
||||
lastRegisteredPhoneNumber.count > 0,
|
||||
lastRegisteredPhoneNumber.hasPrefix(callingCode) {
|
||||
phoneNumberTextField.text = lastRegisteredPhoneNumber.substring(from: callingCode.count)
|
||||
}
|
||||
|
||||
var countryName = NSLocalizedString("UNKNOWN_COUNTRY_NAME", comment: "Label for unknown countries.")
|
||||
if let countryNameDerived = PhoneNumberUtil.countryName(fromCountryCode: countryCode) {
|
||||
countryName = countryNameDerived
|
||||
}
|
||||
|
||||
update(withCountryName: countryName, callingCode: callingCode, countryCode: countryCode)
|
||||
}
|
||||
|
||||
private func update(withCountryName countryName: String, callingCode: String, countryCode: String) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard countryCode.count > 0 else {
|
||||
owsFailDebug("Invalid country code.")
|
||||
return
|
||||
}
|
||||
guard countryName.count > 0 else {
|
||||
owsFailDebug("Invalid country name.")
|
||||
return
|
||||
}
|
||||
guard callingCode.count > 0 else {
|
||||
owsFailDebug("Invalid calling code.")
|
||||
return
|
||||
}
|
||||
|
||||
self.countryName = countryName
|
||||
self.callingCode = callingCode
|
||||
self.countryCode = countryCode
|
||||
|
||||
countryNameLabel.text = countryName
|
||||
callingCodeLabel.text = callingCode
|
||||
|
||||
self.phoneNumberTextField.placeholder = ViewControllerUtils.examplePhoneNumber(forCountryCode: countryCode, callingCode: callingCode)
|
||||
}
|
||||
|
||||
// MARK: - Debug
|
||||
|
||||
private let kKeychainService_LastRegistered = "kKeychainService_LastRegistered"
|
||||
private let kKeychainKey_LastRegisteredCountryCode = "kKeychainKey_LastRegisteredCountryCode"
|
||||
private let kKeychainKey_LastRegisteredPhoneNumber = "kKeychainKey_LastRegisteredPhoneNumber"
|
||||
|
||||
private func debugValue(forKey key: String) -> String? {
|
||||
guard CurrentAppContext().isDebugBuild() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
do {
|
||||
let value = try CurrentAppContext().keychainStorage().string(forService: kKeychainService_LastRegistered, key: key)
|
||||
return value
|
||||
} catch {
|
||||
owsFailDebug("Error: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func setDebugValue(_ value: String, forKey key: String) {
|
||||
guard CurrentAppContext().isDebugBuild() else {
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
try CurrentAppContext().keychainStorage().set(string: value, service: kKeychainService_LastRegistered, key: key)
|
||||
} catch {
|
||||
owsFailDebug("Error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func lastRegisteredCountryCode() -> String? {
|
||||
return debugValue(forKey: kKeychainKey_LastRegisteredCountryCode)
|
||||
}
|
||||
|
||||
private func setLastRegisteredCountryCode(value: String) {
|
||||
setDebugValue(value, forKey: kKeychainKey_LastRegisteredCountryCode)
|
||||
}
|
||||
|
||||
private func lastRegisteredPhoneNumber() -> String? {
|
||||
return debugValue(forKey: kKeychainKey_LastRegisteredPhoneNumber)
|
||||
}
|
||||
|
||||
private func setLastRegisteredPhoneNumber(value: String) {
|
||||
setDebugValue(value, forKey: kKeychainKey_LastRegisteredPhoneNumber)
|
||||
}
|
||||
|
||||
// MARK: - Events
|
||||
|
||||
@objc func explanationLabelTapped(sender: UIGestureRecognizer) {
|
||||
guard sender.state == .recognized else {
|
||||
return
|
||||
}
|
||||
// TODO:
|
||||
}
|
||||
|
||||
@objc func countryRowTapped(sender: UIGestureRecognizer) {
|
||||
guard sender.state == .recognized else {
|
||||
return
|
||||
}
|
||||
showCountryPicker()
|
||||
}
|
||||
|
||||
@objc func countryCodeTapped(sender: UIGestureRecognizer) {
|
||||
guard sender.state == .recognized else {
|
||||
return
|
||||
}
|
||||
showCountryPicker()
|
||||
}
|
||||
|
||||
@objc func nextPressed() {
|
||||
Logger.info("")
|
||||
|
||||
onboardingController.onboardingPhoneNumberDidComplete(viewController: self)
|
||||
}
|
||||
|
||||
// MARK: - Country Picker
|
||||
|
||||
private func showCountryPicker() {
|
||||
guard !tsAccountManager.isReregistering() else {
|
||||
return
|
||||
}
|
||||
|
||||
let countryCodeController = CountryCodeViewController()
|
||||
countryCodeController.countryCodeDelegate = self
|
||||
countryCodeController.interfaceOrientationMask = .portrait
|
||||
let navigationController = OWSNavigationController(rootViewController: countryCodeController)
|
||||
self.present(navigationController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
// MARK: - Register
|
||||
|
||||
private func didTapRegisterButton() {
|
||||
guard let phoneNumberText = phoneNumberTextField.text?.ows_stripped(),
|
||||
phoneNumberText.count > 0 else {
|
||||
OWSAlerts.showAlert(title:
|
||||
NSLocalizedString("REGISTRATION_VIEW_NO_PHONE_NUMBER_ALERT_TITLE",
|
||||
comment: "Title of alert indicating that users needs to enter a phone number to register."),
|
||||
message:
|
||||
NSLocalizedString("REGISTRATION_VIEW_NO_PHONE_NUMBER_ALERT_MESSAGE",
|
||||
comment: "Message of alert indicating that users needs to enter a phone number to register."))
|
||||
return
|
||||
}
|
||||
|
||||
let phoneNumber = "\(callingCode)\(phoneNumberText)"
|
||||
guard let localNumber = PhoneNumber.tryParsePhoneNumber(fromUserSpecifiedText: phoneNumber),
|
||||
localNumber.toE164().count > 0,
|
||||
PhoneNumberValidator().isValidForRegistration(phoneNumber: localNumber) else {
|
||||
OWSAlerts.showAlert(title:
|
||||
NSLocalizedString("REGISTRATION_VIEW_INVALID_PHONE_NUMBER_ALERT_TITLE",
|
||||
comment: "Title of alert indicating that users needs to enter a valid phone number to register."),
|
||||
message:
|
||||
NSLocalizedString("REGISTRATION_VIEW_INVALID_PHONE_NUMBER_ALERT_MESSAGE",
|
||||
comment: "Message of alert indicating that users needs to enter a valid phone number to register."))
|
||||
return
|
||||
}
|
||||
let parsedPhoneNumber = localNumber.toE164()
|
||||
|
||||
if UIDevice.current.isIPad {
|
||||
let countryCode = self.countryCode
|
||||
OWSAlerts.showConfirmationAlert(title: NSLocalizedString("REGISTRATION_IPAD_CONFIRM_TITLE",
|
||||
comment: "alert title when registering an iPad"),
|
||||
message: NSLocalizedString("REGISTRATION_IPAD_CONFIRM_BODY",
|
||||
comment: "alert body when registering an iPad"),
|
||||
proceedTitle: NSLocalizedString("REGISTRATION_IPAD_CONFIRM_BUTTON",
|
||||
comment: "button text to proceed with registration when on an iPad"),
|
||||
proceedAction: { (_) in
|
||||
self.sendCode(parsedPhoneNumber: parsedPhoneNumber,
|
||||
phoneNumberText: phoneNumberText,
|
||||
countryCode: countryCode)
|
||||
})
|
||||
} else {
|
||||
sendCode(parsedPhoneNumber: parsedPhoneNumber,
|
||||
phoneNumberText: phoneNumberText,
|
||||
countryCode: countryCode)
|
||||
}
|
||||
}
|
||||
|
||||
private func sendCode(parsedPhoneNumber: String,
|
||||
phoneNumberText: String,
|
||||
countryCode: String) {
|
||||
ModalActivityIndicatorViewController.present(fromViewController: self,
|
||||
canCancel: true) { (modal) in
|
||||
self.setLastRegisteredCountryCode(value: countryCode)
|
||||
self.setLastRegisteredPhoneNumber(value: phoneNumberText)
|
||||
|
||||
self.tsAccountManager.register(withPhoneNumber: parsedPhoneNumber,
|
||||
success: {
|
||||
DispatchQueue.main.async {
|
||||
modal.dismiss(completion: {
|
||||
self.registrationSucceeded()
|
||||
})
|
||||
}
|
||||
}, failure: { (error) in
|
||||
Logger.error("Error: \(error)")
|
||||
|
||||
DispatchQueue.main.async {
|
||||
modal.dismiss(completion: {
|
||||
self.registrationFailed(error: error as NSError)
|
||||
})
|
||||
}
|
||||
}, smsVerification: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func registrationSucceeded() {
|
||||
self.onboardingController.onboardingPhoneNumberDidComplete(viewController: self)
|
||||
}
|
||||
|
||||
private func registrationFailed(error: NSError) {
|
||||
if error.code == 400 {
|
||||
OWSAlerts.showAlert(title: NSLocalizedString("REGISTRATION_ERROR", comment: ""),
|
||||
message: NSLocalizedString("REGISTRATION_NON_VALID_NUMBER", comment: ""))
|
||||
|
||||
} else {
|
||||
OWSAlerts.showAlert(title: error.localizedDescription,
|
||||
message: error.localizedRecoverySuggestion)
|
||||
}
|
||||
|
||||
phoneNumberTextField.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
extension OnboardingPhoneNumberViewController: UITextFieldDelegate {
|
||||
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
var prefix = self.callingCode
|
||||
if prefix.hasPrefix(COUNTRY_CODE_PREFIX) {
|
||||
prefix = prefix.substring(from: COUNTRY_CODE_PREFIX.count)
|
||||
}
|
||||
ViewControllerUtils.phoneNumber(textField, shouldChangeCharactersIn: range, replacementString: string, countryCode: countryCode, prefix: prefix)
|
||||
|
||||
// Inform our caller that we took care of performing the change.
|
||||
return false
|
||||
}
|
||||
|
||||
public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
didTapRegisterButton()
|
||||
textField.resignFirstResponder()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
extension OnboardingPhoneNumberViewController: CountryCodeViewControllerDelegate {
|
||||
public func countryCodeViewController(_ vc: CountryCodeViewController, didSelectCountryCode countryCode: String, countryName: String, callingCode: String) {
|
||||
guard countryCode.count > 0 else {
|
||||
owsFailDebug("Invalid country code.")
|
||||
return
|
||||
}
|
||||
guard countryName.count > 0 else {
|
||||
owsFailDebug("Invalid country name.")
|
||||
return
|
||||
}
|
||||
guard callingCode.count > 0 else {
|
||||
owsFailDebug("Invalid calling code.")
|
||||
return
|
||||
}
|
||||
|
||||
update(withCountryName: countryName, callingCode: callingCode, countryCode: countryCode)
|
||||
|
||||
// Trigger the formatting logic with a no-op edit.
|
||||
_ = textField(phoneNumberTextField, shouldChangeCharactersIn: NSRange(location: 0, length: 0), replacementString: "")
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue