mirror of https://github.com/oxen-io/session-ios
Merge branch 'dev' of github.com:loki-project/loki-messenger-ios
parent
8b37e12146
commit
33c6f3a88d
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,73 @@
|
||||
import UIKit
|
||||
import PromiseKit
|
||||
|
||||
final class OnboardingAccountDetailsViewController : OnboardingBaseViewController {
|
||||
|
||||
private lazy var displayNameTextField: UITextField = {
|
||||
let result = UITextField()
|
||||
result.textColor = Theme.primaryColor
|
||||
result.font = UIFont.ows_dynamicTypeBodyClamped
|
||||
result.textAlignment = .center
|
||||
result.placeholder = NSLocalizedString("Display Name (Optional)", comment: "")
|
||||
result.accessibilityIdentifier = "onboarding.accountDetailsStep.displayNameTextField"
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var passwordTextField: UITextField = {
|
||||
let result = UITextField()
|
||||
result.textColor = Theme.primaryColor
|
||||
result.font = UIFont.ows_dynamicTypeBodyClamped
|
||||
result.textAlignment = .center
|
||||
result.placeholder = NSLocalizedString("Password (Optional)", comment: "")
|
||||
result.accessibilityIdentifier = "onboarding.accountDetailsStep.passwordTextField"
|
||||
result.isSecureTextEntry = true
|
||||
return result
|
||||
}()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = Theme.backgroundColor
|
||||
view.layoutMargins = .zero
|
||||
let titleLabel = self.createTitleLabel(text: NSLocalizedString("Create Your Loki Messenger Account", comment: ""))
|
||||
titleLabel.accessibilityIdentifier = "onboarding.accountDetailsStep.titleLabel"
|
||||
let topSpacer = UIView.vStretchingSpacer()
|
||||
let displayNameLabel = createExplanationLabel(text: NSLocalizedString("Enter a name that will be shown to all your contacts", comment: ""))
|
||||
displayNameLabel.accessibilityIdentifier = "onboarding.accountDetailsStep.displayNameLabel"
|
||||
let passwordLabel = createExplanationLabel(text: NSLocalizedString("Type an optional password for added security", comment: ""))
|
||||
passwordLabel.accessibilityIdentifier = "onboarding.accountDetailsStep.passwordLabel"
|
||||
let bottomSpacer = UIView.vStretchingSpacer()
|
||||
let nextButton = button(title: NSLocalizedString("Next", comment: ""), selector: #selector(goToSeedStep))
|
||||
nextButton.accessibilityIdentifier = "onboarding.accountDetailsStep.nextButton"
|
||||
let stackView = UIStackView(arrangedSubviews: [
|
||||
titleLabel,
|
||||
topSpacer,
|
||||
displayNameLabel,
|
||||
UIView.spacer(withHeight: 8),
|
||||
displayNameTextField,
|
||||
UIView.spacer(withHeight: 16),
|
||||
passwordLabel,
|
||||
UIView.spacer(withHeight: 8),
|
||||
passwordTextField,
|
||||
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.autoPinWidthToSuperview()
|
||||
stackView.autoPin(toTopLayoutGuideOf: self, withInset: 0)
|
||||
autoPinView(toBottomOfViewControllerOrKeyboard: stackView, avoidNotch: true)
|
||||
topSpacer.autoMatch(.height, to: .height, of: bottomSpacer)
|
||||
}
|
||||
|
||||
public override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
displayNameTextField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
@objc private func goToSeedStep() {
|
||||
onboardingController.pushPublicKeyViewController(from: self)
|
||||
}
|
||||
}
|
@ -1,423 +0,0 @@
|
||||
//
|
||||
// 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?
|
||||
private var phoneStrokeNormal: UIView?
|
||||
private var phoneStrokeError: UIView?
|
||||
private let validationWarningLabel = UILabel()
|
||||
private var isPhoneNumberInvalid = false
|
||||
|
||||
override public func loadView() {
|
||||
super.loadView()
|
||||
|
||||
populateDefaults()
|
||||
|
||||
view.backgroundColor = Theme.backgroundColor
|
||||
view.layoutMargins = .zero
|
||||
|
||||
let titleLabel = self.titleLabel(text: NSLocalizedString("ONBOARDING_PHONE_NUMBER_TITLE", comment: "Title of the 'onboarding phone number' view."))
|
||||
titleLabel.accessibilityIdentifier = "onboarding.phoneNumber." + "titleLabel"
|
||||
|
||||
// Country
|
||||
|
||||
let rowHeight: CGFloat = 40
|
||||
|
||||
countryNameLabel.textColor = Theme.primaryColor
|
||||
countryNameLabel.font = UIFont.ows_dynamicTypeBodyClamped
|
||||
countryNameLabel.setContentHuggingHorizontalLow()
|
||||
countryNameLabel.setCompressionResistanceHorizontalLow()
|
||||
countryNameLabel.accessibilityIdentifier = "onboarding.phoneNumber." + "countryNameLabel"
|
||||
|
||||
let countryIcon = UIImage(named: (CurrentAppContext().isRTL
|
||||
? "small_chevron_left"
|
||||
: "small_chevron_right"))
|
||||
let countryImageView = UIImageView(image: countryIcon?.withRenderingMode(.alwaysTemplate))
|
||||
countryImageView.tintColor = Theme.placeholderColor
|
||||
countryImageView.setContentHuggingHigh()
|
||||
countryImageView.setCompressionResistanceHigh()
|
||||
countryImageView.accessibilityIdentifier = "onboarding.phoneNumber." + "countryImageView"
|
||||
|
||||
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)
|
||||
_ = countryRow.addBottomStroke()
|
||||
countryRow.accessibilityIdentifier = "onboarding.phoneNumber." + "countryRow"
|
||||
|
||||
callingCodeLabel.textColor = Theme.primaryColor
|
||||
callingCodeLabel.font = UIFont.ows_dynamicTypeBodyClamped
|
||||
callingCodeLabel.setContentHuggingHorizontalHigh()
|
||||
callingCodeLabel.setCompressionResistanceHorizontalHigh()
|
||||
callingCodeLabel.isUserInteractionEnabled = true
|
||||
callingCodeLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(countryCodeTapped)))
|
||||
_ = callingCodeLabel.addBottomStroke()
|
||||
callingCodeLabel.autoSetDimension(.width, toSize: rowHeight, relation: .greaterThanOrEqual)
|
||||
callingCodeLabel.accessibilityIdentifier = "onboarding.phoneNumber." + "callingCodeLabel"
|
||||
|
||||
phoneNumberTextField.textAlignment = .left
|
||||
phoneNumberTextField.delegate = self
|
||||
phoneNumberTextField.keyboardType = .numberPad
|
||||
phoneNumberTextField.textColor = Theme.primaryColor
|
||||
phoneNumberTextField.font = UIFont.ows_dynamicTypeBodyClamped
|
||||
phoneNumberTextField.setContentHuggingHorizontalLow()
|
||||
phoneNumberTextField.setCompressionResistanceHorizontalLow()
|
||||
phoneNumberTextField.accessibilityIdentifier = "onboarding.phoneNumber." + "phoneNumberTextField"
|
||||
|
||||
phoneStrokeNormal = phoneNumberTextField.addBottomStroke()
|
||||
phoneStrokeError = phoneNumberTextField.addBottomStroke(color: .ows_destructiveRed, strokeWidth: 2)
|
||||
|
||||
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)
|
||||
|
||||
validationWarningLabel.text = NSLocalizedString("ONBOARDING_PHONE_NUMBER_VALIDATION_WARNING",
|
||||
comment: "Label indicating that the phone number is invalid in the 'onboarding phone number' view.")
|
||||
validationWarningLabel.textColor = .ows_destructiveRed
|
||||
validationWarningLabel.font = UIFont.ows_dynamicTypeSubheadlineClamped
|
||||
validationWarningLabel.autoSetDimension(.height, toSize: validationWarningLabel.font.lineHeight)
|
||||
validationWarningLabel.accessibilityIdentifier = "onboarding.phoneNumber." + "validationWarningLabel"
|
||||
|
||||
let validationWarningRow = UIView()
|
||||
validationWarningRow.addSubview(validationWarningLabel)
|
||||
validationWarningLabel.autoPinHeightToSuperview()
|
||||
validationWarningLabel.autoPinEdge(toSuperviewEdge: .trailing)
|
||||
|
||||
let nextButton = self.button(title: NSLocalizedString("BUTTON_NEXT",
|
||||
comment: "Label for the 'next' button."),
|
||||
selector: #selector(nextPressed))
|
||||
nextButton.accessibilityIdentifier = "onboarding.phoneNumber." + "nextButton"
|
||||
self.nextButton = nextButton
|
||||
let topSpacer = UIView.vStretchingSpacer()
|
||||
let bottomSpacer = UIView.vStretchingSpacer()
|
||||
|
||||
let stackView = UIStackView(arrangedSubviews: [
|
||||
titleLabel,
|
||||
topSpacer,
|
||||
countryRow,
|
||||
UIView.spacer(withHeight: 8),
|
||||
phoneNumberRow,
|
||||
UIView.spacer(withHeight: 8),
|
||||
validationWarningRow,
|
||||
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.autoPinWidthToSuperview()
|
||||
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)
|
||||
|
||||
validationWarningLabel.autoPinEdge(.leading, to: .leading, of: phoneNumberTextField)
|
||||
}
|
||||
|
||||
// MARK: - View Lifecycle
|
||||
|
||||
public override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
isPhoneNumberInvalid = false
|
||||
|
||||
updateViewState()
|
||||
}
|
||||
|
||||
public override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
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 callingCodeNumeric = parsedPhoneNumber.getCountryCode() else {
|
||||
owsFailDebug("Could not resume re-registration; missing callingCode.")
|
||||
return
|
||||
}
|
||||
let callingCode = "\(COUNTRY_CODE_PREFIX)\(callingCodeNumeric)"
|
||||
let countryCodes: [String] =
|
||||
PhoneNumberUtil.sharedThreadLocal().countryCodes(fromCallingCode: callingCode)
|
||||
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(callingCode) {
|
||||
owsFailDebug("Could not resume re-registration; non-matching calling code.")
|
||||
return
|
||||
}
|
||||
let phoneNumberWithoutCallingCode = phoneNumberE164.substring(from: callingCode.count)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
let countryState = OnboardingCountryState(countryName: countryName, callingCode: callingCode, countryCode: countryCode)
|
||||
onboardingController.update(countryState: countryState)
|
||||
|
||||
phoneNumberTextField.text = phoneNumberWithoutCallingCode
|
||||
|
||||
// Don't let user edit their phone number while re-registering.
|
||||
phoneNumberTextField.isEnabled = false
|
||||
|
||||
updateViewState()
|
||||
|
||||
// Trigger the formatting logic with a no-op edit.
|
||||
_ = textField(phoneNumberTextField, shouldChangeCharactersIn: NSRange(location: 0, length: 0), replacementString: "")
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private var countryName: String {
|
||||
get {
|
||||
return onboardingController.countryState.countryName
|
||||
}
|
||||
}
|
||||
private var callingCode: String {
|
||||
get {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
return onboardingController.countryState.callingCode
|
||||
}
|
||||
}
|
||||
private var countryCode: String {
|
||||
get {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
return onboardingController.countryState.countryCode
|
||||
}
|
||||
}
|
||||
|
||||
private func populateDefaults() {
|
||||
if let lastRegisteredPhoneNumber = OnboardingController.lastRegisteredPhoneNumber(),
|
||||
lastRegisteredPhoneNumber.count > 0,
|
||||
lastRegisteredPhoneNumber.hasPrefix(callingCode) {
|
||||
phoneNumberTextField.text = lastRegisteredPhoneNumber.substring(from: callingCode.count)
|
||||
} else if let phoneNumber = onboardingController.phoneNumber {
|
||||
phoneNumberTextField.text = phoneNumber.userInput
|
||||
}
|
||||
|
||||
updateViewState()
|
||||
|
||||
// Trigger the formatting logic with a no-op edit.
|
||||
_ = textField(phoneNumberTextField, shouldChangeCharactersIn: NSRange(location: 0, length: 0), replacementString: "")
|
||||
}
|
||||
|
||||
private func updateViewState() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
countryNameLabel.text = countryName
|
||||
callingCodeLabel.text = callingCode
|
||||
|
||||
self.phoneNumberTextField.placeholder = ViewControllerUtils.examplePhoneNumber(forCountryCode: countryCode, callingCode: callingCode)
|
||||
|
||||
updateValidationWarnings()
|
||||
}
|
||||
|
||||
private func updateValidationWarnings() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
phoneStrokeNormal?.isHidden = isPhoneNumberInvalid
|
||||
phoneStrokeError?.isHidden = !isPhoneNumberInvalid
|
||||
validationWarningLabel.isHidden = !isPhoneNumberInvalid
|
||||
}
|
||||
|
||||
// MARK: - Events
|
||||
|
||||
@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("")
|
||||
|
||||
parseAndTryToRegister()
|
||||
}
|
||||
|
||||
// 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 parseAndTryToRegister() {
|
||||
guard let phoneNumberText = phoneNumberTextField.text?.ows_stripped(),
|
||||
phoneNumberText.count > 0 else {
|
||||
|
||||
isPhoneNumberInvalid = false
|
||||
updateValidationWarnings()
|
||||
|
||||
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 {
|
||||
|
||||
isPhoneNumberInvalid = false
|
||||
updateValidationWarnings()
|
||||
|
||||
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 e164PhoneNumber = localNumber.toE164()
|
||||
|
||||
onboardingController.update(phoneNumber: OnboardingPhoneNumber(e164: e164PhoneNumber, userInput: phoneNumberText))
|
||||
|
||||
if UIDevice.current.isIPad {
|
||||
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.onboardingController.tryToRegister(fromViewController: self, smsVerification: true)
|
||||
})
|
||||
} else {
|
||||
onboardingController.tryToRegister(fromViewController: self, smsVerification: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
extension OnboardingPhoneNumberViewController: UITextFieldDelegate {
|
||||
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
ViewControllerUtils.phoneNumber(textField, shouldChangeCharactersIn: range, replacementString: string, callingCode: callingCode)
|
||||
|
||||
isPhoneNumberInvalid = false
|
||||
updateValidationWarnings()
|
||||
|
||||
// Inform our caller that we took care of performing the change.
|
||||
return false
|
||||
}
|
||||
|
||||
public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
parseAndTryToRegister()
|
||||
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
|
||||
}
|
||||
|
||||
let countryState = OnboardingCountryState(countryName: countryName, callingCode: callingCode, countryCode: countryCode)
|
||||
|
||||
onboardingController.update(countryState: countryState)
|
||||
|
||||
updateViewState()
|
||||
|
||||
// Trigger the formatting logic with a no-op edit.
|
||||
_ = textField(phoneNumberTextField, shouldChangeCharactersIn: NSRange(location: 0, length: 0), replacementString: "")
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
import UIKit
|
||||
import PromiseKit
|
||||
|
||||
final class OnboardingPublicKeyViewController : OnboardingBaseViewController {
|
||||
private var keyPair: ECKeyPair! { didSet { updateMnemonic() } }
|
||||
private var hexEncodedPublicKey: String!
|
||||
private var mnemonic: String! { didSet { mnemonicLabel.text = mnemonic } }
|
||||
|
||||
private lazy var mnemonicLabel: UILabel = {
|
||||
let result = createExplanationLabel(text: "")
|
||||
result.accessibilityIdentifier = "onboarding.publicKeyStep.mnemonicLabel"
|
||||
result.alpha = 0.8
|
||||
var fontTraits = result.font.fontDescriptor.symbolicTraits
|
||||
fontTraits.insert(.traitItalic)
|
||||
result.font = UIFont(descriptor: result.font.fontDescriptor.withSymbolicTraits(fontTraits)!, size: result.font.pointSize)
|
||||
return result
|
||||
}()
|
||||
|
||||
override public func viewDidLoad() {
|
||||
super.loadView()
|
||||
setUpViewHierarchy()
|
||||
updateKeyPair()
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
view.backgroundColor = Theme.backgroundColor
|
||||
view.layoutMargins = .zero
|
||||
let titleLabel = createTitleLabel(text: NSLocalizedString("Create Your Loki Messenger Account", comment: ""))
|
||||
titleLabel.accessibilityIdentifier = "onboarding.publicKeyStep.titleLabel"
|
||||
let topSpacer = UIView.vStretchingSpacer()
|
||||
let explanationLabel = createExplanationLabel(text: NSLocalizedString("Please save the seed below in a safe location. They can be used to restore your account if you lose access or migrate to a new device.", comment: ""))
|
||||
explanationLabel.accessibilityIdentifier = "onboarding.publicKeyStep.explanationLabel"
|
||||
let bottomSpacer = UIView.vStretchingSpacer()
|
||||
let registerButton = button(title: NSLocalizedString("Register", comment: ""), selector: #selector(register))
|
||||
registerButton.accessibilityIdentifier = "onboarding.publicKeyStep.registerButton"
|
||||
let stackView = UIStackView(arrangedSubviews: [
|
||||
titleLabel,
|
||||
topSpacer,
|
||||
explanationLabel,
|
||||
UIView.spacer(withHeight: 32),
|
||||
mnemonicLabel,
|
||||
bottomSpacer,
|
||||
registerButton
|
||||
])
|
||||
stackView.axis = .vertical
|
||||
stackView.alignment = .fill
|
||||
stackView.layoutMargins = UIEdgeInsets(top: 32, left: 32, bottom: 32, right: 32)
|
||||
stackView.isLayoutMarginsRelativeArrangement = true
|
||||
view.addSubview(stackView)
|
||||
stackView.autoPinWidthToSuperview()
|
||||
stackView.autoPin(toTopLayoutGuideOf: self, withInset: 0)
|
||||
autoPinView(toBottomOfViewControllerOrKeyboard: stackView, avoidNotch: true)
|
||||
topSpacer.autoMatch(.height, to: .height, of: bottomSpacer)
|
||||
}
|
||||
|
||||
private func updateKeyPair() {
|
||||
let identityManager = OWSIdentityManager.shared()
|
||||
identityManager.generateNewIdentityKey() // Generates and stores a new key pair
|
||||
keyPair = identityManager.identityKeyPair()!
|
||||
}
|
||||
|
||||
private func updateMnemonic() {
|
||||
hexEncodedPublicKey = keyPair.publicKey.map { String(format: "%02hhx", $0) }.joined()
|
||||
mnemonic = Mnemonic.encode(hexEncodedString: hexEncodedPublicKey)
|
||||
}
|
||||
|
||||
@objc private func register() {
|
||||
let accountManager = TSAccountManager.sharedInstance()
|
||||
accountManager.phoneNumberAwaitingVerification = hexEncodedPublicKey
|
||||
accountManager.didRegister()
|
||||
onboardingController.verificationDidComplete(fromView: self)
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
import CryptoSwift
|
||||
|
||||
enum Mnemonic {
|
||||
|
||||
struct Language : Hashable {
|
||||
let filename: String
|
||||
let prefixLength: Int
|
||||
|
||||
static let english = Language(filename: "english", prefixLength: 3)
|
||||
static let japanese = Language(filename: "japanese", prefixLength: 3)
|
||||
static let portuguese = Language(filename: "portuguese", prefixLength: 4)
|
||||
static let spanish = Language(filename: "spanish", prefixLength: 4)
|
||||
|
||||
private static var wordSetCache: [Language:[String]] = [:]
|
||||
|
||||
private init(filename: String, prefixLength: Int) {
|
||||
self.filename = filename
|
||||
self.prefixLength = prefixLength
|
||||
}
|
||||
|
||||
func loadWordSet() -> [String] {
|
||||
if let cachedResult = Language.wordSetCache[self] {
|
||||
return cachedResult
|
||||
} else {
|
||||
let url = Bundle.main.url(forResource: filename, withExtension: "txt")!
|
||||
let contents = try! String(contentsOf: url)
|
||||
let result = contents.split(separator: ",").map { String($0) }
|
||||
Language.wordSetCache[self] = result
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Based on [mnemonic.js](https://github.com/loki-project/loki-messenger/blob/development/libloki/modules/mnemonic.js) .
|
||||
static func encode(hexEncodedString string: String, language: Language = .english) -> String {
|
||||
var string = string
|
||||
let wordSet = language.loadWordSet()
|
||||
let prefixLength = language.prefixLength
|
||||
var result: [String] = []
|
||||
let wordCount = wordSet.count
|
||||
let characterCount = string.indices.count // Safe for this particular case
|
||||
for chunkStartIndexAsInt in stride(from: 0, to: characterCount, by: 8) {
|
||||
let chunkStartIndex = string.index(string.startIndex, offsetBy: chunkStartIndexAsInt)
|
||||
let chunkEndIndex = string.index(chunkStartIndex, offsetBy: 8)
|
||||
func swap(_ chunk: String) -> String {
|
||||
func toStringIndex(_ indexAsInt: Int) -> String.Index {
|
||||
return chunk.index(chunk.startIndex, offsetBy: indexAsInt)
|
||||
}
|
||||
let p1 = chunk[toStringIndex(6)..<toStringIndex(8)]
|
||||
let p2 = chunk[toStringIndex(4)..<toStringIndex(6)]
|
||||
let p3 = chunk[toStringIndex(2)..<toStringIndex(4)]
|
||||
let p4 = chunk[toStringIndex(0)..<toStringIndex(2)]
|
||||
return String(p1 + p2 + p3 + p4)
|
||||
}
|
||||
let p1 = string[string.startIndex..<chunkStartIndex]
|
||||
let p2 = swap(String(string[chunkStartIndex..<chunkEndIndex]))
|
||||
let p3 = string[chunkEndIndex..<string.endIndex]
|
||||
string = String(p1 + p2 + p3)
|
||||
}
|
||||
for chunkStartIndexAsInt in stride(from: 0, to: characterCount, by: 8) {
|
||||
let chunkStartIndex = string.index(string.startIndex, offsetBy: chunkStartIndexAsInt)
|
||||
let chunkEndIndex = string.index(chunkStartIndex, offsetBy: 8)
|
||||
let x = Int(string[chunkStartIndex..<chunkEndIndex], radix: 16)!
|
||||
let w1 = x % wordCount
|
||||
let w2 = ((x / wordCount) + w1) % wordCount
|
||||
let w3 = (((x / wordCount) / wordCount) + w2) % wordCount
|
||||
result += [ wordSet[w1], wordSet[w2], wordSet[w3] ]
|
||||
}
|
||||
let checksum = Array(result.map { String($0[$0.startIndex..<$0.index($0.startIndex, offsetBy: prefixLength)]) }.joined().utf8).crc32()
|
||||
let checksumIndex = Int(checksum) % result.count
|
||||
result.append(result[checksumIndex])
|
||||
return result.joined(separator: " ")
|
||||
}
|
||||
}
|
@ -1,2 +1,2 @@
|
||||
github "TheLevelUp/ZXingObjC"
|
||||
|
||||
github "krzyzanowskim/CryptoSwift"
|
||||
|
@ -1 +1,2 @@
|
||||
github "TheLevelUp/ZXingObjC" "3.2.2"
|
||||
github "TheLevelUp/ZXingObjC" "3.6.4"
|
||||
github "krzyzanowskim/CryptoSwift" "1.0.0"
|
||||
|
Loading…
Reference in New Issue