mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Update SeedVC for multi device QR code scanning
This commit is contained in:
parent
ade1fc3239
commit
b607df0c4e
4 changed files with 69 additions and 123 deletions
|
@ -1,21 +1,19 @@
|
|||
|
||||
// TODO: Split this into multiple VCs
|
||||
|
||||
final class SeedVC : OnboardingBaseViewController, DeviceLinkingModalDelegate {
|
||||
final class SeedVC : OnboardingBaseViewController, DeviceLinkingModalDelegate, OWSQRScannerDelegate {
|
||||
private var mode: Mode = .register { didSet { if mode != oldValue { handleModeChanged() } } }
|
||||
private var seed: Data! { didSet { updateMnemonic() } }
|
||||
private var mnemonic: String! { didSet { handleMnemonicChanged() } }
|
||||
|
||||
// MARK: Components
|
||||
private lazy var registerStackView: UIStackView = {
|
||||
let result = UIStackView(arrangedSubviews: [ explanationLabel1, UIView.spacer(withHeight: 32), mnemonicLabel, UIView.spacer(withHeight: 24), copyButton, restoreButton1, linkButton1 ])
|
||||
let result = UIStackView(arrangedSubviews: [ explanationLabel1, UIView.spacer(withHeight: 32), mnemonicLabel, UIView.spacer(withHeight: 24), copyButton, restoreButton, linkButton1 ])
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.registerStackView"
|
||||
result.axis = .vertical
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var explanationLabel1: UILabel = {
|
||||
let result = createExplanationLabel(text: NSLocalizedString("Please save the seed below in a safe location. It can be used to restore your account if you lose access, or to migrate to a new device.", comment: ""))
|
||||
let result = createExplanationLabel(text: NSLocalizedString("Please save the seed below in a safe location. It can be used to restore your account if you lose access, or to migrate your account to a new device.", comment: ""))
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.explanationLabel1"
|
||||
result.textColor = Theme.primaryColor
|
||||
var fontTraits = result.font.fontDescriptor.symbolicTraits
|
||||
|
@ -41,22 +39,22 @@ final class SeedVC : OnboardingBaseViewController, DeviceLinkingModalDelegate {
|
|||
return result
|
||||
}()
|
||||
|
||||
private lazy var restoreButton1: OWSFlatButton = {
|
||||
private lazy var restoreButton: OWSFlatButton = {
|
||||
let result = createLinkButton(title: NSLocalizedString("Restore Using Seed", comment: ""), selector: #selector(handleSwitchModeButton1Tapped))
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.restoreButton1"
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.restoreButton"
|
||||
result.setBackgroundColors(upColor: .clear, downColor: .clear)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var linkButton1: OWSFlatButton = {
|
||||
let result = createLinkButton(title: NSLocalizedString("Link Device", comment: ""), selector: #selector(handleSwitchModeButton2Tapped))
|
||||
let result = createLinkButton(title: NSLocalizedString("Link Device", comment: ""), selector: #selector(handleLinkButtonTapped))
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.linkButton1"
|
||||
result.setBackgroundColors(upColor: .clear, downColor: .clear)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var restoreStackView: UIStackView = {
|
||||
let result = UIStackView(arrangedSubviews: [ explanationLabel2, UIView.spacer(withHeight: 32), errorLabel1, errorLabel1Spacer, mnemonicTextField, UIView.spacer(withHeight: 24), registerButton1, linkButton2 ])
|
||||
let result = UIStackView(arrangedSubviews: [ explanationLabel2, UIView.spacer(withHeight: 32), errorLabel, errorLabelSpacer, mnemonicTextField, UIView.spacer(withHeight: 24), registerButton, linkButton2 ])
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.restoreStackView"
|
||||
result.axis = .vertical
|
||||
return result
|
||||
|
@ -72,9 +70,9 @@ final class SeedVC : OnboardingBaseViewController, DeviceLinkingModalDelegate {
|
|||
return result
|
||||
}()
|
||||
|
||||
private lazy var errorLabel1: UILabel = {
|
||||
private lazy var errorLabel: UILabel = {
|
||||
let result = createExplanationLabel(text: "")
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.errorLabel1"
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.errorLabel"
|
||||
result.textColor = UIColor.red
|
||||
var fontTraits = result.font.fontDescriptor.symbolicTraits
|
||||
fontTraits.insert(.traitBold)
|
||||
|
@ -82,7 +80,7 @@ final class SeedVC : OnboardingBaseViewController, DeviceLinkingModalDelegate {
|
|||
return result
|
||||
}()
|
||||
|
||||
private lazy var errorLabel1Spacer: UIView = {
|
||||
private lazy var errorLabelSpacer: UIView = {
|
||||
let result = UIView.spacer(withHeight: 32)
|
||||
result.isHidden = true
|
||||
return result
|
||||
|
@ -102,90 +100,28 @@ final class SeedVC : OnboardingBaseViewController, DeviceLinkingModalDelegate {
|
|||
return result
|
||||
}()
|
||||
|
||||
private lazy var registerButton1: OWSFlatButton = {
|
||||
private lazy var registerButton: OWSFlatButton = {
|
||||
let result = createLinkButton(title: NSLocalizedString("Register a New Account", comment: ""), selector: #selector(handleSwitchModeButton1Tapped))
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.registerButton1"
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.registerButton"
|
||||
result.setBackgroundColors(upColor: .clear, downColor: .clear)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var linkButton2: OWSFlatButton = {
|
||||
let result = createLinkButton(title: NSLocalizedString("Link Device", comment: ""), selector: #selector(handleSwitchModeButton2Tapped))
|
||||
let result = createLinkButton(title: NSLocalizedString("Link Device", comment: ""), selector: #selector(handleLinkButtonTapped))
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.linkButton2"
|
||||
result.setBackgroundColors(upColor: .clear, downColor: .clear)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var linkStackView: UIStackView = {
|
||||
let result = UIStackView(arrangedSubviews: [ explanationLabel3, UIView.spacer(withHeight: 32), errorLabel2, errorLabel2Spacer, masterHexEncodedPublicKeyTextField, UIView.spacer(withHeight: 24), registerButton2, restoreButton2 ])
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.linkStackView"
|
||||
result.axis = .vertical
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var explanationLabel3: UILabel = {
|
||||
let result = createExplanationLabel(text: NSLocalizedString("Link to an existing device by going into its in-app settings and clicking \"Link Device\".", comment: ""))
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.explanationLabel3"
|
||||
result.textColor = Theme.primaryColor
|
||||
var fontTraits = result.font.fontDescriptor.symbolicTraits
|
||||
fontTraits.insert(.traitBold)
|
||||
result.font = UIFont(descriptor: result.font.fontDescriptor.withSymbolicTraits(fontTraits)!, size: result.font.pointSize)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var errorLabel2: UILabel = {
|
||||
let result = createExplanationLabel(text: "")
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.errorLabel2"
|
||||
result.textColor = UIColor.red
|
||||
var fontTraits = result.font.fontDescriptor.symbolicTraits
|
||||
fontTraits.insert(.traitBold)
|
||||
result.font = UIFont(descriptor: result.font.fontDescriptor.withSymbolicTraits(fontTraits)!, size: 12)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var errorLabel2Spacer: UIView = {
|
||||
let result = UIView.spacer(withHeight: 32)
|
||||
result.isHidden = true
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var masterHexEncodedPublicKeyTextField: UITextField = {
|
||||
let result = UITextField(frame: CGRect.zero)
|
||||
result.textColor = Theme.primaryColor
|
||||
result.font = UIFont.ows_dynamicTypeBodyClamped
|
||||
result.textAlignment = .center
|
||||
let placeholder = NSMutableAttributedString(string: NSLocalizedString("Enter the Other Device's Public Key", comment: ""))
|
||||
placeholder.addAttribute(.foregroundColor, value: Theme.placeholderColor, range: NSRange(location: 0, length: placeholder.length))
|
||||
result.attributedPlaceholder = placeholder
|
||||
result.tintColor = UIColor.lokiGreen()
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.masterHexEncodedPublicKeyTextField"
|
||||
result.keyboardAppearance = .dark
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var registerButton2: OWSFlatButton = {
|
||||
let result = createLinkButton(title: NSLocalizedString("Register a New Account", comment: ""), selector: #selector(handleSwitchModeButton1Tapped))
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.registerButton2"
|
||||
result.setBackgroundColors(upColor: .clear, downColor: .clear)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var restoreButton2: OWSFlatButton = {
|
||||
let result = createLinkButton(title: NSLocalizedString("Restore Using Seed", comment: ""), selector: #selector(handleSwitchModeButton2Tapped))
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.restoreButton2"
|
||||
result.setBackgroundColors(upColor: .clear, downColor: .clear)
|
||||
return result
|
||||
}()
|
||||
|
||||
// Shared
|
||||
private lazy var mainButton: OWSFlatButton = {
|
||||
let result = createButton(title: "", selector: #selector(handleMainButtonTapped))
|
||||
let result = createButton(title: "", selector: #selector(proceed(with:)))
|
||||
result.accessibilityIdentifier = "onboarding.keyPairStep.mainButton"
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Types
|
||||
enum Mode { case register, restore, link }
|
||||
enum Mode { case register, restore }
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func viewDidLoad() {
|
||||
|
@ -207,7 +143,6 @@ final class SeedVC : OnboardingBaseViewController, DeviceLinkingModalDelegate {
|
|||
let mainView = UIView(frame: CGRect.zero)
|
||||
mainView.addSubview(restoreStackView)
|
||||
mainView.addSubview(registerStackView)
|
||||
mainView.addSubview(linkStackView)
|
||||
let mainStackView = UIStackView(arrangedSubviews: [ titleLabel, mainView, mainButton ])
|
||||
mainStackView.axis = .vertical
|
||||
mainStackView.layoutMargins = UIEdgeInsets(top: 32, left: 32, bottom: 32, right: 32)
|
||||
|
@ -221,8 +156,6 @@ final class SeedVC : OnboardingBaseViewController, DeviceLinkingModalDelegate {
|
|||
registerStackView.autoVCenterInSuperview()
|
||||
restoreStackView.autoPinWidthToSuperview()
|
||||
restoreStackView.autoVCenterInSuperview()
|
||||
linkStackView.autoPinWidthToSuperview()
|
||||
linkStackView.autoVCenterInSuperview()
|
||||
}
|
||||
|
||||
// MARK: General
|
||||
|
@ -237,9 +170,8 @@ final class SeedVC : OnboardingBaseViewController, DeviceLinkingModalDelegate {
|
|||
private func handleModeChanged() {
|
||||
let (activeStackView, otherStackViews) = { () -> (UIStackView, [UIStackView]) in
|
||||
switch mode {
|
||||
case .register: return (registerStackView, [ restoreStackView, linkStackView ])
|
||||
case .restore: return (restoreStackView, [ registerStackView, linkStackView ])
|
||||
case .link: return (linkStackView, [ registerStackView, restoreStackView ])
|
||||
case .register: return (registerStackView, [ restoreStackView ])
|
||||
case .restore: return (restoreStackView, [ registerStackView ])
|
||||
}
|
||||
}()
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
|
@ -250,14 +182,12 @@ final class SeedVC : OnboardingBaseViewController, DeviceLinkingModalDelegate {
|
|||
switch mode {
|
||||
case .register: return NSLocalizedString("Register", comment: "")
|
||||
case .restore: return NSLocalizedString("Restore", comment: "")
|
||||
case .link: return NSLocalizedString("Link", comment: "")
|
||||
}
|
||||
}()
|
||||
UIView.transition(with: mainButton, duration: 0.25, options: .transitionCrossDissolve, animations: {
|
||||
self.mainButton.setTitle(mainButtonTitle)
|
||||
}, completion: nil)
|
||||
if mode != .restore { mnemonicTextField.resignFirstResponder() }
|
||||
if mode != .link { masterHexEncodedPublicKeyTextField.resignFirstResponder() }
|
||||
}
|
||||
|
||||
private func updateSeed() {
|
||||
|
@ -287,20 +217,41 @@ final class SeedVC : OnboardingBaseViewController, DeviceLinkingModalDelegate {
|
|||
switch mode {
|
||||
case .register: mode = .restore
|
||||
case .restore: mode = .register
|
||||
case .link: mode = .register
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleSwitchModeButton2Tapped() {
|
||||
switch mode {
|
||||
case .register: mode = .link
|
||||
case .restore: mode = .link
|
||||
case .link: mode = .restore
|
||||
@objc private func handleLinkButtonTapped() {
|
||||
ows_ask(forCameraPermissions: { [weak self] hasCameraAccess in
|
||||
guard let self = self else { return }
|
||||
if hasCameraAccess {
|
||||
let message = NSLocalizedString("something something something", comment: "")
|
||||
let scanQRCodeWrapperVC = ScanQRCodeWrapperVC(message: message)
|
||||
scanQRCodeWrapperVC.delegate = self
|
||||
self.present(scanQRCodeWrapperVC, animated: true, completion: nil)
|
||||
} else {
|
||||
// Do nothing
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func controller(_ controller: OWSQRCodeScanningViewController, didDetectQRCodeWith string: String) {
|
||||
dismiss(animated: true, completion: nil)
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.proceed(with: string)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleMainButtonTapped() {
|
||||
@objc private func proceed(with masterHexEncodedPublicKey: String? = nil) {
|
||||
var seed: Data
|
||||
if let masterHexEncodedPublicKey = masterHexEncodedPublicKey {
|
||||
seed = self.seed
|
||||
if !ECKeyPair.isValidHexEncodedPublicKey(candidate: masterHexEncodedPublicKey) {
|
||||
let alert = UIAlertController(title: NSLocalizedString("Invalid QR Code", comment: ""), message: NSLocalizedString("Please make sure the QR code you scanned is correct and try again.", comment: ""), preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), accessibilityIdentifier: nil, style: .default, handler: nil))
|
||||
return present(alert, animated: true, completion: nil)
|
||||
}
|
||||
Analytics.shared.track("Device Linking Attempted")
|
||||
} else {
|
||||
let mode = self.mode
|
||||
switch mode {
|
||||
case .register: seed = self.seed
|
||||
|
@ -311,15 +262,9 @@ final class SeedVC : OnboardingBaseViewController, DeviceLinkingModalDelegate {
|
|||
seed = Data(hex: hexEncodedSeed)
|
||||
} catch let error {
|
||||
let error = error as? Mnemonic.DecodingError ?? Mnemonic.DecodingError.generic
|
||||
errorLabel1Spacer.isHidden = false
|
||||
return errorLabel1.text = error.errorDescription
|
||||
errorLabelSpacer.isHidden = false
|
||||
return errorLabel.text = error.errorDescription
|
||||
}
|
||||
case .link:
|
||||
seed = self.seed
|
||||
let masterHexEncodedPublicKey = masterHexEncodedPublicKeyTextField.text!.trimmingCharacters(in: CharacterSet.whitespaces)
|
||||
if !ECKeyPair.isValidHexEncodedPublicKey(candidate: masterHexEncodedPublicKey) {
|
||||
errorLabel2Spacer.isHidden = false
|
||||
return errorLabel2.text = NSLocalizedString("Invalid public key", comment: "")
|
||||
}
|
||||
}
|
||||
// Use KVC to access dbConnection even though it's private
|
||||
|
@ -335,10 +280,8 @@ final class SeedVC : OnboardingBaseViewController, DeviceLinkingModalDelegate {
|
|||
switch mode {
|
||||
case .register: Analytics.shared.track("Seed Created")
|
||||
case .restore: Analytics.shared.track("Seed Restored")
|
||||
case .link: Analytics.shared.track("Device Linking Attempted")
|
||||
}
|
||||
if mode == .link {
|
||||
let masterHexEncodedPublicKey = masterHexEncodedPublicKeyTextField.text!.trimmingCharacters(in: CharacterSet.whitespaces)
|
||||
if let masterHexEncodedPublicKey = masterHexEncodedPublicKey {
|
||||
TSAccountManager.sharedInstance().didRegister()
|
||||
setUserInteractionEnabled(false)
|
||||
let _ = LokiStorageAPI.getDeviceLinks(associatedWith: masterHexEncodedPublicKey).done(on: DispatchQueue.main) { [weak self] deviceLinks in
|
||||
|
@ -384,8 +327,8 @@ final class SeedVC : OnboardingBaseViewController, DeviceLinkingModalDelegate {
|
|||
|
||||
// MARK: Convenience
|
||||
private func setUserInteractionEnabled(_ isEnabled: Bool) {
|
||||
registerButton2.isUserInteractionEnabled = isEnabled
|
||||
restoreButton2.isUserInteractionEnabled = isEnabled
|
||||
mainButton.isUserInteractionEnabled = isEnabled
|
||||
[ copyButton, restoreButton, linkButton1, registerButton, linkButton2, mainButton ].forEach {
|
||||
$0.isUserInteractionEnabled = isEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ final class SeedModal : Modal {
|
|||
let subtitleLabel = UILabel()
|
||||
subtitleLabel.textColor = Theme.primaryColor
|
||||
subtitleLabel.font = UIFont.ows_dynamicTypeCaption1Clamped
|
||||
subtitleLabel.text = NSLocalizedString("This is your personal secret. It can be used to restore your account if you lose access, or to migrate to a new device.", comment: "")
|
||||
subtitleLabel.text = NSLocalizedString("This is your personal secret. It can be used to restore your account if you lose access, or to migrate your account to a new device.", comment: "")
|
||||
subtitleLabel.numberOfLines = 0
|
||||
subtitleLabel.lineBreakMode = .byWordWrapping
|
||||
subtitleLabel.textAlignment = .center
|
||||
|
|
|
@ -31,9 +31,10 @@ public class OnboardingSplashViewController: OnboardingBaseViewController {
|
|||
lokiLogoContainer.addSubview(lokiLogoImageView)
|
||||
|
||||
let betaTermsLabel = UILabel()
|
||||
betaTermsLabel.text = NSLocalizedString("Loki Messenger is currently in beta. For development purposes the beta version collects basic usage statistics and crash logs. In addition, the beta version doesn't provide full privacy and shouldn't be used to transmit sensitive information.", comment: "")
|
||||
betaTermsLabel.text = NSLocalizedString("Loki Messenger is currently in beta. For development purposes the beta version collects basic usage statistics and crash logs. In addition, the beta version doesn't yet provide full privacy and shouldn't be used to transmit sensitive information.", comment: "")
|
||||
betaTermsLabel.textColor = .white
|
||||
betaTermsLabel.font = .ows_dynamicTypeSubheadlineClamped
|
||||
let font = UIFont.ows_dynamicTypeCaption1Clamped
|
||||
betaTermsLabel.font = UIFont(descriptor: font.fontDescriptor.withSymbolicTraits(.traitBold)!, size: font.pointSize)
|
||||
betaTermsLabel.numberOfLines = 0
|
||||
betaTermsLabel.textAlignment = .center
|
||||
betaTermsLabel.lineBreakMode = .byWordWrapping
|
||||
|
|
|
@ -2552,7 +2552,7 @@
|
|||
"Password (Optional)" = "Password (Optional)";
|
||||
"Next" = "Next";
|
||||
"Add" = "Add";
|
||||
"Please save the seed below in a safe location. It can be used to restore your account if you lose access, or to migrate to a new device." = "Please save the seed below in a safe location. It can be used to restore your account if you lose access, or to migrate to a new device.";
|
||||
"Please save the seed below in a safe location. It can be used to restore your account if you lose access, or to migrate your account to a new device." = "Please save the seed below in a safe location. It can be used to restore your account if you lose access, or to migrate your account to a new device.";
|
||||
"Restore your account by entering your seed below." = "Restore your account by entering your seed below.";
|
||||
"Copy" = "Copy";
|
||||
"Copied ✓" = "Copied ✓";
|
||||
|
@ -2621,7 +2621,7 @@
|
|||
"Loki" = "Loki";
|
||||
"Can't Start Conversation" = "Can't Start Conversation";
|
||||
"Please enter the public key of the person you'd like to message." = "Please enter the public key of the person you'd like to message.";
|
||||
"Loki Messenger is currently in beta. For development purposes the beta version collects basic usage statistics and crash logs. In addition, the beta version doesn't provide full privacy and shouldn't be used to transmit sensitive information." = "Loki Messenger is currently in beta. For development purposes the beta version collects basic usage statistics and crash logs. In addition, the beta version doesn't provide full privacy and shouldn't be used to transmit sensitive information.";
|
||||
"Loki Messenger is currently in beta. For development purposes the beta version collects basic usage statistics and crash logs. In addition, the beta version doesn't yet provide full privacy and shouldn't be used to transmit sensitive information." = "Loki Messenger is currently in beta. For development purposes the beta version collects basic usage statistics and crash logs. In addition, the beta version doesn't yet provide full privacy and shouldn't be used to transmit sensitive information.";
|
||||
"Copy Public Key" = "Copy Public Key";
|
||||
"Link Device" = "Link Device";
|
||||
"Waiting for Device" = "Waiting for Device";
|
||||
|
@ -2633,7 +2633,7 @@
|
|||
"Link to an existing device by going into its in-app settings and clicking \"Link Device\"." = "Link to an existing device by going into its in-app settings and clicking \"Link Device\".";
|
||||
"Authorize" = "Authorize";
|
||||
"Enter the Other Device's Public Key" = "Enter the Other Device's Public Key";
|
||||
"This is your personal secret. It can be used to restore your account if you lose access, or to migrate to a new device." = "This is your personal secret. It can be used to restore your account if you lose access, or to migrate to a new device.";
|
||||
"This is your personal secret. It can be used to restore your account if you lose access, or to migrate your account to a new device." = "This is your personal secret. It can be used to restore your account if you lose access, or to migrate your account to a new device.";
|
||||
"Device Link Authorized" = "Device Link Authorized";
|
||||
"Your device has been linked successfully" = "Your device has been linked successfully";
|
||||
"Link" = "Link";
|
||||
|
@ -2650,3 +2650,5 @@
|
|||
"Please pick a display name that consists of only a-z, A-Z, 0-9 and _ characters" = "Please pick a display name that consists of only a-z, A-Z, 0-9 and _ characters";
|
||||
"Multi Device Limit Reached" = "Multi Device Limit Reached";
|
||||
"It's currently not allowed to link more than one device." = "It's currently not allowed to link more than one device.";
|
||||
"Invalid QR Code" = "Invalid QR Code";
|
||||
"Please make sure the QR code you scanned is correct and try again." = "Please make sure the QR code you scanned is correct and try again.";
|
||||
|
|
Loading…
Reference in a new issue