From 85e4ab3662c3490ce5d88000a707dfc8c79b048d Mon Sep 17 00:00:00 2001
From: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
Date: Wed, 25 Oct 2017 11:16:32 -0400
Subject: [PATCH] Account: link to existing

Add option to link device to an existing Ring account.

Change-Id: I730d1d354b67f001fb7826aa08a3f028cd363afb
Reviewed-by: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
---
 Ring/Cartfile                                 |  1 +
 Ring/Cartfile.resolved                        |  3 +-
 Ring/Ring.xcodeproj/project.pbxproj           | 11 +++
 .../xcshareddata/xcschemes/Ring.xcscheme      |  2 +
 Ring/Ring/Constants/Generated/Images.swift    |  1 +
 .../Constants/Generated/Storyboards.swift     |  1 +
 Ring/Ring/Constants/Generated/Strings.swift   | 24 ++++-
 .../CreateAccountViewModel.swift              |  5 ++
 .../LinkDeviceViewController.storyboard       | 71 +++++++++++++--
 .../LinkDevice/LinkDeviceViewController.swift | 87 ++++++++++++++++++-
 .../LinkDevice/LinkDeviceViewModel.swift      | 43 ++++++++-
 .../Resources/en.lproj/Localizable.strings    | 12 ++-
 Ring/Ring/Services/AccountsService.swift      | 36 ++++++++
 13 files changed, 282 insertions(+), 15 deletions(-)

diff --git a/Ring/Cartfile b/Ring/Cartfile
index 71113d6da..53cd434b9 100644
--- a/Ring/Cartfile
+++ b/Ring/Cartfile
@@ -4,3 +4,4 @@ github "pkluz/PKHUD"
 github "AliSoftware/Reusable" ~> 4.0
 github "SwiftyBeaver/SwiftyBeaver"
 github "ViccAlexander/Chameleon"
+github "andreamazz/AMPopTip"
diff --git a/Ring/Cartfile.resolved b/Ring/Cartfile.resolved
index a286e0b59..6c11cfdb3 100644
--- a/Ring/Cartfile.resolved
+++ b/Ring/Cartfile.resolved
@@ -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"
diff --git a/Ring/Ring.xcodeproj/project.pbxproj b/Ring/Ring.xcodeproj/project.pbxproj
index 44a53462b..ab3813784 100644
--- a/Ring/Ring.xcodeproj/project.pbxproj
+++ b/Ring/Ring.xcodeproj/project.pbxproj
@@ -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;
 			};
diff --git a/Ring/Ring.xcodeproj/xcshareddata/xcschemes/Ring.xcscheme b/Ring/Ring.xcodeproj/xcshareddata/xcschemes/Ring.xcscheme
index 13001f554..2bfbde28a 100644
--- a/Ring/Ring.xcodeproj/xcshareddata/xcschemes/Ring.xcscheme
+++ b/Ring/Ring.xcodeproj/xcshareddata/xcschemes/Ring.xcscheme
@@ -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"
diff --git a/Ring/Ring/Constants/Generated/Images.swift b/Ring/Ring/Constants/Generated/Images.swift
index f261f3f9f..a1305fc56 100644
--- a/Ring/Ring/Constants/Generated/Images.swift
+++ b/Ring/Ring/Constants/Generated/Images.swift
@@ -10,6 +10,7 @@
   typealias Image = UIImage
 #endif
 
+// swiftlint:disable superfluous_disable_command
 // swiftlint:disable file_length
 
 @available(*, deprecated, renamed: "ImageAsset")
diff --git a/Ring/Ring/Constants/Generated/Storyboards.swift b/Ring/Ring/Constants/Generated/Storyboards.swift
index 1e43a4ed2..a9d66dbb1 100644
--- a/Ring/Ring/Constants/Generated/Storyboards.swift
+++ b/Ring/Ring/Constants/Generated/Storyboards.swift
@@ -4,6 +4,7 @@
 import Foundation
 import UIKit
 
+// swiftlint:disable superfluous_disable_command
 // swiftlint:disable file_length
 
 protocol StoryboardType {
diff --git a/Ring/Ring/Constants/Generated/Strings.swift b/Ring/Ring/Constants/Generated/Strings.swift
index 27e95dc31..8a0a35e97 100644
--- a/Ring/Ring/Constants/Generated/Strings.swift
+++ b/Ring/Ring/Constants/Generated/Strings.swift
@@ -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")
diff --git a/Ring/Ring/Features/Walkthrough/CreateAccount/CreateAccountViewModel.swift b/Ring/Ring/Features/Walkthrough/CreateAccount/CreateAccountViewModel.swift
index 7adedbcfa..196e4f3a2 100644
--- a/Ring/Ring/Features/Walkthrough/CreateAccount/CreateAccountViewModel.swift
+++ b/Ring/Ring/Features/Walkthrough/CreateAccount/CreateAccountViewModel.swift
@@ -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
         }
diff --git a/Ring/Ring/Features/Walkthrough/LinkDevice/LinkDeviceViewController.storyboard b/Ring/Ring/Features/Walkthrough/LinkDevice/LinkDeviceViewController.storyboard
index 923853804..0db221635 100644
--- a/Ring/Ring/Features/Walkthrough/LinkDevice/LinkDeviceViewController.storyboard
+++ b/Ring/Ring/Features/Walkthrough/LinkDevice/LinkDeviceViewController.storyboard
@@ -1,11 +1,12 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16F73" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="1yn-Mj-8Ek">
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13196" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="1yn-Mj-8Ek">
     <device id="retina4_7" orientation="portrait">
         <adaptation id="fullscreen"/>
     </device>
     <dependencies>
         <deployment identifier="iOS"/>
-        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13174"/>
+        <capability name="Constraints to layout margins" minToolsVersion="6.0"/>
         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
     </dependencies>
     <scenes>
@@ -36,12 +37,48 @@
                                     </userDefinedRuntimeAttribute>
                                 </userDefinedRuntimeAttributes>
                             </view>
+                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Choose password" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="9AD-b5-bun">
+                                <rect key="frame" x="36" y="165" width="137" height="25"/>
+                                <constraints>
+                                    <constraint firstAttribute="height" constant="25" id="g3B-PW-uya"/>
+                                </constraints>
+                                <fontDescription key="fontDescription" type="system" pointSize="17"/>
+                                <nil key="textColor"/>
+                                <nil key="highlightedColor"/>
+                            </label>
+                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enter pin" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="VN2-rO-YYy">
+                                <rect key="frame" x="36" y="40" width="68.5" height="25"/>
+                                <constraints>
+                                    <constraint firstAttribute="height" constant="25" id="TAS-dT-bOV"/>
+                                </constraints>
+                                <fontDescription key="fontDescription" type="system" pointSize="17"/>
+                                <nil key="textColor"/>
+                                <nil key="highlightedColor"/>
+                            </label>
+                            <textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="Password" textAlignment="natural" clearsOnBeginEditing="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="TH9-pF-YYq">
+                                <rect key="frame" x="36" y="210" width="302" height="30"/>
+                                <constraints>
+                                    <constraint firstAttribute="height" constant="30" id="AeX-R9-vlN"/>
+                                </constraints>
+                                <nil key="textColor"/>
+                                <fontDescription key="fontDescription" type="system" pointSize="14"/>
+                                <textInputTraits key="textInputTraits"/>
+                            </textField>
+                            <textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="Pin" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="Cuu-Tq-miP">
+                                <rect key="frame" x="36" y="85" width="303" height="30"/>
+                                <nil key="textColor"/>
+                                <fontDescription key="fontDescription" type="system" pointSize="14"/>
+                                <textInputTraits key="textInputTraits"/>
+                            </textField>
+                            <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="infoLight" showsTouchWhenHighlighted="YES" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="rtk-w4-zfQ">
+                                <rect key="frame" x="124.5" y="43" width="22" height="22"/>
+                            </button>
                             <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="5Po-6e-k14" customClass="DesignableButton" customModule="Ring" customModuleProvider="target">
-                                <rect key="frame" x="87.5" y="313.5" width="200" height="40"/>
+                                <rect key="frame" x="87.5" y="290" width="200" height="40"/>
                                 <color key="backgroundColor" red="0.0" green="0.29803921570000003" blue="0.37647058820000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                 <constraints>
-                                    <constraint firstAttribute="width" constant="200" id="m9R-vQ-tPe"/>
-                                    <constraint firstAttribute="height" constant="40" id="uVC-dY-9Ym"/>
+                                    <constraint firstAttribute="height" constant="40" id="aUU-qY-tFB"/>
+                                    <constraint firstAttribute="width" constant="200" id="oIL-uO-iFk"/>
                                 </constraints>
                                 <state key="normal" title="Link Device">
                                     <color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
@@ -54,18 +91,36 @@
                         <constraints>
                             <constraint firstAttribute="trailing" secondItem="Gv4-18-FVt" secondAttribute="trailing" id="1Ev-WL-COb"/>
                             <constraint firstAttribute="trailing" secondItem="kur-G7-4Nq" secondAttribute="trailing" id="1U8-xt-ygk"/>
-                            <constraint firstItem="5Po-6e-k14" firstAttribute="centerX" secondItem="N1T-Xh-FH1" secondAttribute="centerX" id="4eR-3k-6Z2"/>
+                            <constraint firstItem="rtk-w4-zfQ" firstAttribute="bottom" secondItem="VN2-rO-YYy" secondAttribute="bottom" id="48M-BJ-5M7"/>
                             <constraint firstItem="Gv4-18-FVt" firstAttribute="top" secondItem="N1T-Xh-FH1" secondAttribute="top" id="5PT-u8-86G"/>
+                            <constraint firstItem="rtk-w4-zfQ" firstAttribute="leading" secondItem="VN2-rO-YYy" secondAttribute="trailing" constant="20" id="9tE-eh-oaC"/>
+                            <constraint firstItem="TH9-pF-YYq" firstAttribute="top" secondItem="9AD-b5-bun" secondAttribute="bottom" constant="20" id="Cgg-Sj-c4x"/>
                             <constraint firstItem="GVt-PH-FqG" firstAttribute="top" secondItem="Gv4-18-FVt" secondAttribute="bottom" id="DHv-Q6-GhU"/>
+                            <constraint firstItem="5Po-6e-k14" firstAttribute="centerX" secondItem="N1T-Xh-FH1" secondAttribute="centerX" id="DdK-br-kKZ"/>
+                            <constraint firstItem="5Po-6e-k14" firstAttribute="top" secondItem="TH9-pF-YYq" secondAttribute="bottom" constant="50" id="FZx-BT-87Q"/>
                             <constraint firstItem="GVt-PH-FqG" firstAttribute="top" secondItem="kur-G7-4Nq" secondAttribute="bottom" id="Jvj-VY-Nb7"/>
                             <constraint firstItem="Gv4-18-FVt" firstAttribute="leading" secondItem="N1T-Xh-FH1" secondAttribute="leading" id="LiW-7Y-wcc"/>
+                            <constraint firstItem="Cuu-Tq-miP" firstAttribute="top" secondItem="VN2-rO-YYy" secondAttribute="bottom" constant="20" id="MvO-t6-2LS"/>
+                            <constraint firstItem="Cuu-Tq-miP" firstAttribute="leading" secondItem="N1T-Xh-FH1" secondAttribute="leadingMargin" constant="20" id="PCJ-vm-jbA"/>
+                            <constraint firstItem="TH9-pF-YYq" firstAttribute="leading" secondItem="N1T-Xh-FH1" secondAttribute="leadingMargin" constant="20" id="XhT-uY-a4w"/>
+                            <constraint firstItem="Cuu-Tq-miP" firstAttribute="centerX" secondItem="N1T-Xh-FH1" secondAttribute="centerX" id="Y9x-Wm-hls"/>
+                            <constraint firstItem="9AD-b5-bun" firstAttribute="top" secondItem="Cuu-Tq-miP" secondAttribute="bottom" constant="50" id="ZbK-P2-lTS"/>
+                            <constraint firstItem="5Po-6e-k14" firstAttribute="centerX" secondItem="N1T-Xh-FH1" secondAttribute="centerX" id="a6Y-pC-oVh"/>
                             <constraint firstItem="kur-G7-4Nq" firstAttribute="top" secondItem="N1T-Xh-FH1" secondAttribute="top" id="hSt-o1-S41"/>
-                            <constraint firstItem="5Po-6e-k14" firstAttribute="centerY" secondItem="N1T-Xh-FH1" secondAttribute="centerY" id="lQW-P9-Vmv"/>
+                            <constraint firstItem="VN2-rO-YYy" firstAttribute="leading" secondItem="N1T-Xh-FH1" secondAttribute="leadingMargin" constant="20" id="lir-vx-FR8"/>
+                            <constraint firstItem="9AD-b5-bun" firstAttribute="leading" secondItem="VN2-rO-YYy" secondAttribute="leading" id="pEu-Md-SRd"/>
+                            <constraint firstItem="VN2-rO-YYy" firstAttribute="top" secondItem="jiD-fm-HFk" secondAttribute="bottom" constant="20" id="qQg-eH-gLE"/>
                             <constraint firstItem="kur-G7-4Nq" firstAttribute="leading" secondItem="N1T-Xh-FH1" secondAttribute="leading" id="r5d-rQ-Kg3"/>
+                            <constraint firstItem="TH9-pF-YYq" firstAttribute="centerX" secondItem="N1T-Xh-FH1" secondAttribute="centerX" id="xpu-85-bbk"/>
                         </constraints>
                     </view>
                     <connections>
-                        <outlet property="linkButton" destination="5Po-6e-k14" id="OoP-zB-985"/>
+                        <outlet property="linkButton" destination="5Po-6e-k14" id="qN6-zI-dIS"/>
+                        <outlet property="passwordLabel" destination="9AD-b5-bun" id="Neh-tt-Ui8"/>
+                        <outlet property="passwordTextField" destination="TH9-pF-YYq" id="vYS-Up-eeb"/>
+                        <outlet property="pinInfoButton" destination="rtk-w4-zfQ" id="eo1-z8-7RN"/>
+                        <outlet property="pinLabel" destination="VN2-rO-YYy" id="qaM-sZ-9hX"/>
+                        <outlet property="pinTextField" destination="Cuu-Tq-miP" id="6Jk-h2-tyr"/>
                     </connections>
                 </viewController>
                 <placeholder placeholderIdentifier="IBFirstResponder" id="6ma-i4-SuK" userLabel="First Responder" sceneMemberID="firstResponder"/>
diff --git a/Ring/Ring/Features/Walkthrough/LinkDevice/LinkDeviceViewController.swift b/Ring/Ring/Features/Walkthrough/LinkDevice/LinkDeviceViewController.swift
index fcfddf3e1..714cc3789 100644
--- a/Ring/Ring/Features/Walkthrough/LinkDevice/LinkDeviceViewController.swift
+++ b/Ring/Ring/Features/Walkthrough/LinkDevice/LinkDeviceViewController.swift
@@ -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)
+        }
+    }
 }
diff --git a/Ring/Ring/Features/Walkthrough/LinkDevice/LinkDeviceViewModel.swift b/Ring/Ring/Features/Walkthrough/LinkDevice/LinkDeviceViewModel.swift
index 71bba384b..8364598fa 100644
--- a/Ring/Ring/Features/Walkthrough/LinkDevice/LinkDeviceViewModel.swift
+++ b/Ring/Ring/Features/Walkthrough/LinkDevice/LinkDeviceViewModel.swift
@@ -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)
     }
-
 }
diff --git a/Ring/Ring/Resources/en.lproj/Localizable.strings b/Ring/Ring/Resources/en.lproj/Localizable.strings
index a6550930b..b25c012be 100644
--- a/Ring/Ring/Resources/en.lproj/Localizable.strings
+++ b/Ring/Ring/Resources/en.lproj/Localizable.strings
@@ -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";
diff --git a/Ring/Ring/Services/AccountsService.swift b/Ring/Ring/Services/AccountsService.swift
index 4c7cc9a9c..528bdb79e 100644
--- a/Ring/Ring/Services/AccountsService.swift
+++ b/Ring/Ring/Services/AccountsService.swift
@@ -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.
 
-- 
GitLab