Commit 85e4ab36 authored by Kateryna Kostiuk's avatar Kateryna Kostiuk Committed by Andreas Traczyk

Account: link to existing

Add option to link device to an existing Ring account.

Change-Id: I730d1d354b67f001fb7826aa08a3f028cd363afb
Reviewed-by: Andreas Traczyk's avatarAndreas Traczyk <andreas.traczyk@savoirfairelinux.com>
parent e79b903d
......@@ -4,3 +4,4 @@ github "pkluz/PKHUD"
github "AliSoftware/Reusable" ~> 4.0
github "SwiftyBeaver/SwiftyBeaver"
github "ViccAlexander/Chameleon"
github "andreamazz/AMPopTip"
......@@ -2,7 +2,8 @@ github "AliSoftware/Reusable" "4.0.1"
github "ReactiveX/RxSwift" "3.5.0"
github "RxSwiftCommunity/RxDataSources" "1.0.4"
github "RxSwiftCommunity/RxRealm" "0.6.0"
github "SwiftyBeaver/SwiftyBeaver" "1.3.0"
github "SwiftyBeaver/SwiftyBeaver" "1.4.2"
github "ViccAlexander/Chameleon" "2.2.0"
github "andreamazz/AMPopTip" "3.0.2"
github "pkluz/PKHUD" "4.2.3"
github "realm/realm-cocoa" "v2.8.3"
......@@ -89,6 +89,7 @@
0ED2B6FA1F96A075001572F0 /* LinkNewDeviceViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0ED2B6F91F96A075001572F0 /* LinkNewDeviceViewController.storyboard */; };
0ED2B6FC1F96A158001572F0 /* LinkNewDeviceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED2B6FB1F96A158001572F0 /* LinkNewDeviceViewController.swift */; };
0ED2B6FE1F96A16C001572F0 /* LinkNewDeviceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED2B6FD1F96A16C001572F0 /* LinkNewDeviceViewModel.swift */; };
0ED666101F9FED1C00743D42 /* AMPopTip.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0ED6660F1F9FED1C00743D42 /* AMPopTip.framework */; };
0EDCC8601F98150500B121D7 /* UIView+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EDCC85F1F98150500B121D7 /* UIView+Rx.swift */; };
0EDE34C71F868E1200FFA15C /* EditProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EDE34C61F868E1200FFA15C /* EditProfileViewController.swift */; };
0EDE34C91F8691BB00FFA15C /* EditProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EDE34C81F8691BB00FFA15C /* EditProfileViewModel.swift */; };
......@@ -319,6 +320,7 @@
0ED2B6F91F96A075001572F0 /* LinkNewDeviceViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LinkNewDeviceViewController.storyboard; sourceTree = "<group>"; };
0ED2B6FB1F96A158001572F0 /* LinkNewDeviceViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkNewDeviceViewController.swift; sourceTree = "<group>"; };
0ED2B6FD1F96A16C001572F0 /* LinkNewDeviceViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkNewDeviceViewModel.swift; sourceTree = "<group>"; };
0ED6660F1F9FED1C00743D42 /* AMPopTip.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AMPopTip.framework; path = Carthage/Build/iOS/AMPopTip.framework; sourceTree = "<group>"; };
0EDCC85F1F98150500B121D7 /* UIView+Rx.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Rx.swift"; sourceTree = "<group>"; };
0EDE34C61F868E1200FFA15C /* EditProfileViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditProfileViewController.swift; sourceTree = "<group>"; };
0EDE34C81F8691BB00FFA15C /* EditProfileViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditProfileViewModel.swift; sourceTree = "<group>"; };
......@@ -450,6 +452,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
0ED666101F9FED1C00743D42 /* AMPopTip.framework in Frameworks */,
0EEFBA3C1F83DA21000EDBAD /* libsecp256k1.a in Frameworks */,
1A3CA32B1F102BB700283748 /* Chameleon.framework in Frameworks */,
1A1E476F1F0E894600EA9A36 /* SwiftyBeaver.framework in Frameworks */,
......@@ -557,6 +560,7 @@
02AED8171DD4C4B000F740BA /* Frameworks */ = {
isa = PBXGroup;
children = (
0ED6660F1F9FED1C00743D42 /* AMPopTip.framework */,
0EEFBA3B1F83DA21000EDBAD /* libsecp256k1.a */,
1A3CA32A1F102BB700283748 /* Chameleon.framework */,
1A1E476E1F0E894600EA9A36 /* SwiftyBeaver.framework */,
......@@ -1177,6 +1181,7 @@
CreatedOnToolsVersion = 7.3.1;
DevelopmentTeam = KM95526DS8;
LastSwiftMigration = 0810;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.BackgroundModes = {
enabled = 0;
......@@ -1276,6 +1281,7 @@
"$(SRCROOT)/Carthage/Build/iOS/Reusable.framework",
"$(SRCROOT)/Carthage/Build/iOS/SwiftyBeaver.framework",
"$(SRCROOT)/Carthage/Build/iOS/Chameleon.framework",
"$(SRCROOT)/Carthage/build/iOS/AMPopTip.framework",
);
name = "⚙️ Copy Frameworks";
outputPaths = (
......@@ -1289,6 +1295,7 @@
"$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/Reusable.framework",
"$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/SwiftyBeaver.framework",
"$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/Chameleon.framework",
"$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/AMPopTip.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
......@@ -1582,6 +1589,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = KM95526DS8;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
......@@ -1597,6 +1605,7 @@
PRODUCT_BUNDLE_IDENTIFIER = cx.ring;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE = "";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Ring/Bridging/Ring-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 3.0;
......@@ -1612,6 +1621,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = KM95526DS8;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
......@@ -1627,6 +1637,7 @@
PRODUCT_BUNDLE_IDENTIFIER = cx.ring;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE = "";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Ring/Bridging/Ring-Bridging-Header.h";
SWIFT_VERSION = 3.0;
};
......
......@@ -26,6 +26,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
......@@ -65,6 +66,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
......
......@@ -10,6 +10,7 @@
typealias Image = UIImage
#endif
// swiftlint:disable superfluous_disable_command
// swiftlint:disable file_length
@available(*, deprecated, renamed: "ImageAsset")
......
......@@ -4,6 +4,7 @@
import Foundation
import UIKit
// swiftlint:disable superfluous_disable_command
// swiftlint:disable file_length
protocol StoryboardType {
......
......@@ -2,6 +2,7 @@
import Foundation
// swiftlint:disable superfluous_disable_command
// swiftlint:disable file_length
// swiftlint:disable explicit_type_interface identifier_name line_length nesting type_body_length type_name
......@@ -15,18 +16,20 @@ enum L10n {
enum Alerts {
/// Account Added
static let accountAddedTitle = L10n.tr("Localizable", "alerts.accountAddedTitle")
/// Account couldn't be found on the Ring network. Make sure it was exported on Ring from an existing device, and that provided credentials are correct.
static let accountCannotBeFoundMessage = L10n.tr("Localizable", "alerts.accountCannotBeFoundMessage")
/// Can't find account
static let accountCannotBeFoundTitle = L10n.tr("Localizable", "alerts.accountCannotBeFoundTitle")
/// The account couldn't be created.
static let accountDefaultErrorMessage = L10n.tr("Localizable", "alerts.accountDefaultErrorMessage")
/// Unknown error
static let accountDefaultErrorTitle = L10n.tr("Localizable", "alerts.accountDefaultErrorTitle")
/// Linking account
static let accountLinkedTitle = L10n.tr("Localizable", "alerts.accountLinkedTitle")
/// Could not add account because Ring couldn't connect to the distributed network. Check your device connectivity.
static let accountNoNetworkMessage = L10n.tr("Localizable", "alerts.accountNoNetworkMessage")
/// Can't connect to the network
static let accountNoNetworkTitle = L10n.tr("Localizable", "alerts.accountNoNetworkTitle")
/// Account couldn't be found on the Ring network. Make sure it was exported on Ring from an existing device, and that provided credentials are correct.
static let acountCannotBeFoundMessage = L10n.tr("Localizable", "alerts.acountCannotBeFoundMessage")
/// Cancel
static let profileCancelPhoto = L10n.tr("Localizable", "alerts.profileCancelPhoto")
/// Take photo
......@@ -97,6 +100,23 @@ enum L10n {
static let title = L10n.tr("Localizable", "linkDevice.title")
}
enum Linktoaccount {
/// To generate the PIN code, go to the account managment settings on device that contain account you want to use. In devices settings Select "Link another device to this account". You will get the necessary PIN to complete this form. The PIN is only valid for 10 minutes.
static let explanationPinMessage = L10n.tr("Localizable", "linkToAccount.explanationPinMessage")
/// Link device
static let linkButtonTitle = L10n.tr("Localizable", "linkToAccount.linkButtonTitle")
/// Enter Password
static let passwordLabel = L10n.tr("Localizable", "linkToAccount.passwordLabel")
/// password
static let passwordPlaceholder = L10n.tr("Localizable", "linkToAccount.passwordPlaceholder")
/// Enter PIN
static let pinLabel = L10n.tr("Localizable", "linkToAccount.pinLabel")
/// PIN
static let pinPlaceholder = L10n.tr("Localizable", "linkToAccount.pinPlaceholder")
/// Account linking
static let waitLinkToAccountTitle = L10n.tr("Localizable", "linkToAccount.waitLinkToAccountTitle")
}
enum Smartlist {
/// Conversations
static let conversations = L10n.tr("Localizable", "smartlist.conversations")
......
......@@ -103,6 +103,7 @@ enum AccountCreationError: Error {
case generic
case network
case unknown
case linkError
}
extension AccountCreationError: LocalizedError {
......@@ -113,6 +114,8 @@ extension AccountCreationError: LocalizedError {
return L10n.Alerts.accountCannotBeFoundTitle
case .network:
return L10n.Alerts.accountNoNetworkTitle
case .linkError:
return L10n.Alerts.accountCannotBeFoundTitle
default:
return L10n.Alerts.accountDefaultErrorTitle
}
......@@ -124,6 +127,8 @@ extension AccountCreationError: LocalizedError {
return L10n.Alerts.accountDefaultErrorMessage
case .network:
return L10n.Alerts.accountNoNetworkMessage
case .linkError:
return L10n.Alerts.accountCannotBeFoundMessage
default:
return L10n.Alerts.accountDefaultErrorMessage
}
......
......@@ -9,23 +9,108 @@
import UIKit
import Reusable
import RxSwift
import PKHUD
import AMPopTip
class LinkDeviceViewController: UIViewController, StoryboardBased, ViewModelBased {
// MARK: outlets
@IBOutlet weak var linkButton: DesignableButton!
@IBOutlet weak var pinTextField: UITextField!
@IBOutlet weak var passwordTextField: UITextField!
@IBOutlet weak var pinInfoButton: UIButton!
@IBOutlet weak var pinLabel: UILabel!
@IBOutlet weak var passwordLabel: UILabel!
// MARK: members
private let disposeBag = DisposeBag()
var viewModel: LinkDeviceViewModel!
let popTip = PopTip()
// MARK: functions
override func viewDidLoad() {
super.viewDidLoad()
self.applyL10n()
//bind view model to view
self.pinInfoButton.rx.tap.subscribe(onNext: { [unowned self] (_) in
self.showPinInfo()
}).disposed(by: self.disposeBag)
self.linkButton.rx.tap.subscribe(onNext: { [unowned self] (_) in
self.viewModel.linkDevice()
}).disposed(by: self.disposeBag)
// handle linking state
self.viewModel.createState
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] (state) in
switch state {
case .started:
self?.showCreationHUD()
case .success:
self?.hideHud()
self?.showLinkedSuccess()
case .error (let error):
self?.hideHud()
self?.showAccountCreationError(error: error)
default:
self?.hideHud()
}
}, onError: { [weak self] (error) in
self?.hideHud()
if let error = error as? AccountCreationError {
self?.showAccountCreationError(error: error)
}
}).disposed(by: self.disposeBag)
self.viewModel.linkButtonEnabledState.bind(to: self.linkButton.rx.isEnabled)
.disposed(by: self.disposeBag)
// bind view to view model
self.pinTextField.rx.text.orEmpty.bind(to: self.viewModel.pin).disposed(by: self.disposeBag)
self.passwordTextField.rx.text.orEmpty.bind(to: self.viewModel.password).disposed(by: self.disposeBag)
}
private func applyL10n() {
self.linkButton.setTitle(L10n.Linktoaccount.linkButtonTitle, for: .normal)
self.pinLabel.text = L10n.Linktoaccount.pinLabel
self.passwordLabel.text = L10n.Linktoaccount.passwordLabel
self.pinTextField.placeholder = L10n.Linktoaccount.pinPlaceholder
self.passwordTextField.placeholder = L10n.Linktoaccount.passwordPlaceholder
}
private func showCreationHUD() {
HUD.show(.labeledProgress(title: L10n.Linktoaccount.waitLinkToAccountTitle, subtitle: nil))
}
private func showLinkedSuccess() {
HUD.flash(.labeledSuccess(title: L10n.Alerts.accountLinkedTitle, subtitle: nil), delay: Durations.alertFlashDuration.value)
}
private func hideHud() {
HUD.hide()
}
private func showAccountCreationError(error: AccountCreationError) {
let alert = UIAlertController.init(title: error.title,
message: error.message,
preferredStyle: .alert)
alert.addAction(UIAlertAction.init(title: L10n.Global.ok, style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
private func showPinInfo() {
if popTip.isVisible {
popTip.hide()
} else {
popTip.shouldDismissOnTap = true
popTip.entranceAnimation = .scale
popTip.bubbleColor = UIColor.ringSecondary
popTip.textColor = UIColor.white
popTip.show(text: L10n.Linktoaccount.explanationPinMessage, direction: .down,
maxWidth: 250, in: self.view, from: pinInfoButton.frame)
}
}
}
......@@ -28,12 +28,51 @@ class LinkDeviceViewModel: Stateable, ViewModel {
lazy var state: Observable<State> = {
return self.stateSubject.asObservable()
}()
private let accountService: AccountsService
private let accountCreationState = Variable<AccountCreationState>(.unknown)
lazy var createState: Observable<AccountCreationState> = {
return self.accountCreationState.asObservable()
}()
lazy var linkButtonEnabledState: Observable<Bool> = {
return Observable.combineLatest(self.password.asObservable(),
self.pin.asObservable()) {(password, pin) -> Bool in
if !password.isEmpty && !pin.isEmpty {
return true
}
return false
}
}()
let pin = Variable<String>("")
let password = Variable<String>("")
let disposeBag = DisposeBag()
required init (with injectionBag: InjectionBag) {
self.accountService = injectionBag.accountService
//Account creation state observer
self.accountService
.sharedResponseStream
.subscribe(onNext: { [unowned self] event in
if event.getEventInput(ServiceEventInput.registrationState) == Registered {
self.accountCreationState.value = .success
Observable<Int>.timer(Durations.alertFlashDuration.value, period: nil, scheduler: MainScheduler.instance).subscribe(onNext: { [unowned self] (_) in
self.stateSubject.onNext(WalkthroughState.deviceLinked)
}).disposed(by: self.disposeBag)
} else if event.getEventInput(ServiceEventInput.registrationState) == ErrorGeneric {
self.accountCreationState.value = .error(error: AccountCreationError.linkError)
} else if event.getEventInput(ServiceEventInput.registrationState) == ErrorNetwork {
self.accountCreationState.value = .error(error: AccountCreationError.network)
}
}, onError: { [unowned self] _ in
self.accountCreationState.value = .error(error: AccountCreationError.unknown)
}).disposed(by: disposeBag)
}
func linkDevice () {
self.stateSubject.onNext(WalkthroughState.deviceLinked)
self.accountCreationState.value = .started
self.accountService.linkToRingAccount(withPin: self.pin.value,
password: self.password.value)
}
}
......@@ -58,9 +58,18 @@
"createAccount.loading" = "Loading";
"createAccount.waitCreateAccountTitle" = "Adding account";
//Link To Account form
"linkToAccount.waitLinkToAccountTitle" = "Account linking";
"linkToAccount.linkButtonTitle" = "Link device";
"linkToAccount.passwordPlaceholder" = "password";
"linkToAccount.pinPlaceholder" = "PIN";
"linkToAccount.passwordLabel" = "Enter Password";
"linkToAccount.pinLabel" = "Enter PIN";
"linkToAccount.explanationPinMessage" = "To generate the PIN code, go to the account managment settings on device that contain account you want to use. In devices settings Select \"Link another device to this account\". You will get the necessary PIN to complete this form. The PIN is only valid for 10 minutes.";
//Alerts
"alerts.accountCannotBeFoundTitle" = "Can't find account";
"alerts.acountCannotBeFoundMessage" = "Account couldn't be found on the Ring network. Make sure it was exported on Ring from an existing device, and that provided credentials are correct.";
"alerts.accountCannotBeFoundMessage" = "Account couldn't be found on the Ring network. Make sure it was exported on Ring from an existing device, and that provided credentials are correct.";
"alerts.accountAddedTitle" = "Account Added";
"alerts.accountNoNetworkTitle" = "Can't connect to the network";
"alerts.accountNoNetworkMessage" = "Could not add account because Ring couldn't connect to the distributed network. Check your device connectivity.";
......@@ -69,6 +78,7 @@
"alerts.profileTakePhoto" = "Take photo";
"alerts.profileUploadPhoto" = "Upload photo";
"alerts.profileCancelPhoto" = "Cancel";
"alerts.accountLinkedTitle" = "Linking account";
//Account Page
"accountPage.devicesListHeader" = "Devices";
......
......@@ -220,6 +220,42 @@ class AccountsService: AccountAdapterDelegate {
}
}
func linkToRingAccount(withPin pin: String, password: String) {
do {
var ringDetails = try self.getRingInitialAccountDetails()
ringDetails.updateValue(password, forKey: ConfigKey.archivePassword.rawValue)
ringDetails.updateValue(pin, forKey: ConfigKey.archivePIN.rawValue)
let accountId = self.accountAdapter.addAccount(ringDetails)
guard accountId != nil else {
throw AddAccountError.unknownError
}
var account = self.getAccount(fromAccountId: accountId!)
if account == nil {
let details = self.getAccountDetails(fromAccountId: accountId!)
let volatileDetails = self.getVolatileAccountDetails(fromAccountId: accountId!)
let credentials = try self.getAccountCredentials(fromAccountId: accountId!)
let devices = getKnownRingDevices(fromAccountId: accountId!)
account = try AccountModel(withAccountId: accountId!,
details: details,
volatileDetails: volatileDetails,
credentials: credentials,
devices: devices)
let accountModelHelper = AccountModelHelper(withAccount: account!)
var accountAddedEvent = ServiceEvent(withEventType: .accountAdded)
accountAddedEvent.addEventInput(.id, value: account?.id)
accountAddedEvent.addEventInput(.state, value: accountModelHelper.getRegistrationState())
self.responseStream.onNext(accountAddedEvent)
}
self.currentAccount = account
} catch {
self.responseStream.onError(error)
}
}
/**
Entry point to create a brand-new SIP account.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment