mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-21 09:02:44 +02:00
Secrets recovery: Implement secrets recovery with recovery key screen.
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
Copyright 2020 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
final class SecretsRecoveryWithKeyCoordinator: SecretsRecoveryWithKeyCoordinatorType {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let secretsRecoveryWithKeyViewController: SecretsRecoveryWithKeyViewController
|
||||
private let secretsRecoveryWithKeyViewModel: SecretsRecoveryWithKeyViewModel
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var childCoordinators: [Coordinator] = []
|
||||
|
||||
weak var delegate: SecretsRecoveryWithKeyCoordinatorDelegate?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(recoveryService: MXRecoveryService) {
|
||||
|
||||
let secretsRecoveryWithKeyViewModel = SecretsRecoveryWithKeyViewModel(recoveryService: recoveryService)
|
||||
let secretsRecoveryWithKeyViewController = SecretsRecoveryWithKeyViewController.instantiate(with: secretsRecoveryWithKeyViewModel)
|
||||
self.secretsRecoveryWithKeyViewController = secretsRecoveryWithKeyViewController
|
||||
self.secretsRecoveryWithKeyViewModel = secretsRecoveryWithKeyViewModel
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
self.secretsRecoveryWithKeyViewModel.coordinatorDelegate = self
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
return self.secretsRecoveryWithKeyViewController
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - secretsRecoveryWithKeyViewModelCoordinatorDelegate
|
||||
extension SecretsRecoveryWithKeyCoordinator: SecretsRecoveryWithKeyViewModelCoordinatorDelegate {
|
||||
func secretsRecoveryWithKeyViewModelDidRecover(_ viewModel: SecretsRecoveryWithKeyViewModelType) { self.delegate?.secretsRecoveryWithKeyCoordinatorDidRecover(self)
|
||||
}
|
||||
|
||||
func secretsRecoveryWithKeyViewModelDidCancel(_ viewModel: SecretsRecoveryWithKeyViewModelType) { self.delegate?.secretsRecoveryWithKeyCoordinatorDidCancel(self)
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
Copyright 2020 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol SecretsRecoveryWithKeyCoordinatorDelegate: class {
|
||||
func secretsRecoveryWithKeyCoordinatorDidRecover(_ coordinator: SecretsRecoveryWithKeyCoordinatorType)
|
||||
func secretsRecoveryWithKeyCoordinatorDidCancel(_ coordinator: SecretsRecoveryWithKeyCoordinatorType)
|
||||
}
|
||||
|
||||
/// `SecretsRecoveryWithKeyCoordinatorType` is a protocol describing a Coordinator that handle secrets recovery from recovery key navigation flow.
|
||||
protocol SecretsRecoveryWithKeyCoordinatorType: Coordinator, Presentable {
|
||||
var delegate: SecretsRecoveryWithKeyCoordinatorDelegate? { get }
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
Copyright 2020 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
/// SecretsRecoveryWithKeyViewController view actions exposed to view model
|
||||
enum SecretsRecoveryWithKeyViewAction {
|
||||
case recover
|
||||
case unknownRecoveryKey
|
||||
case cancel
|
||||
}
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="xgV-qW-Fsz">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Secrets Recovery With Key View Controller-->
|
||||
<scene sceneID="CVz-YO-PzM">
|
||||
<objects>
|
||||
<viewController extendedLayoutIncludesOpaqueBars="YES" automaticallyAdjustsScrollViewInsets="NO" id="xgV-qW-Fsz" customClass="SecretsRecoveryWithKeyViewController" customModule="Riot" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="L4F-7e-apF">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="lGc-J1-CgX">
|
||||
<rect key="frame" x="0.0" y="20" width="375" height="647"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="byL-eq-NOk">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="409"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="lYT-Tl-Mah">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="409"/>
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="key_backup_logo" translatesAutoresizingMaskIntoConstraints="NO" id="eS4-VO-THO">
|
||||
<rect key="frame" x="166" y="35" width="43" height="52"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="43" id="1xY-5q-ziZ"/>
|
||||
<constraint firstAttribute="height" constant="52" id="k5n-aQ-HHu"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Use your recovery key to unlock your secure message history" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nE7-cw-Z30">
|
||||
<rect key="frame" x="20" y="117" width="335" height="36"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="v8b-un-B61">
|
||||
<rect key="frame" x="0.0" y="193" width="375" height="50"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="751" text="enter" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="UdR-PC-WoG">
|
||||
<rect key="frame" x="20" y="10" width="38" height="30"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Enter passphrase" textAlignment="natural" minimumFontSize="17" clearButtonMode="whileEditing" translatesAutoresizingMaskIntoConstraints="NO" id="e79-U2-RHY">
|
||||
<rect key="frame" x="78" y="0.0" width="243" height="50"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="50" id="3ip-DB-fLs"/>
|
||||
</constraints>
|
||||
<nil key="textColor"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<textInputTraits key="textInputTraits" autocorrectionType="no" returnKeyType="done"/>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="xgV-qW-Fsz" id="cQg-bp-MBE"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="5nP-Tq-Idd">
|
||||
<rect key="frame" x="321" y="3" width="44" height="44"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="44" id="0kv-dT-HTJ"/>
|
||||
<constraint firstAttribute="height" constant="44" id="5d7-nR-WUE"/>
|
||||
</constraints>
|
||||
<state key="normal" image="import_files_button"/>
|
||||
<connections>
|
||||
<action selector="importFileButtonAction:" destination="xgV-qW-Fsz" eventType="touchUpInside" id="Vkc-Ye-4X8"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="UdR-PC-WoG" firstAttribute="centerY" secondItem="v8b-un-B61" secondAttribute="centerY" id="1pb-aK-0yU"/>
|
||||
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="e79-U2-RHY" secondAttribute="bottom" id="597-MS-j5I"/>
|
||||
<constraint firstItem="UdR-PC-WoG" firstAttribute="top" secondItem="v8b-un-B61" secondAttribute="top" constant="10" id="Ioc-O6-eLQ"/>
|
||||
<constraint firstItem="e79-U2-RHY" firstAttribute="leading" secondItem="UdR-PC-WoG" secondAttribute="trailing" priority="750" constant="10" id="THA-Gk-Bep"/>
|
||||
<constraint firstItem="5nP-Tq-Idd" firstAttribute="centerY" secondItem="e79-U2-RHY" secondAttribute="centerY" id="ZxF-8C-qp9"/>
|
||||
<constraint firstAttribute="trailing" secondItem="5nP-Tq-Idd" secondAttribute="trailing" constant="10" id="a2G-J4-Ecr"/>
|
||||
<constraint firstItem="e79-U2-RHY" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="UdR-PC-WoG" secondAttribute="trailing" constant="20" id="g3B-Fs-ULK"/>
|
||||
<constraint firstItem="UdR-PC-WoG" firstAttribute="leading" secondItem="v8b-un-B61" secondAttribute="leading" constant="20" id="hnv-lq-UPY"/>
|
||||
<constraint firstItem="e79-U2-RHY" firstAttribute="top" relation="greaterThanOrEqual" secondItem="v8b-un-B61" secondAttribute="top" id="hsi-wr-jNN"/>
|
||||
<constraint firstItem="UdR-PC-WoG" firstAttribute="centerY" secondItem="e79-U2-RHY" secondAttribute="centerY" id="lLe-lE-kwL"/>
|
||||
<constraint firstItem="5nP-Tq-Idd" firstAttribute="leading" secondItem="e79-U2-RHY" secondAttribute="trailing" id="sC7-lp-4nA"/>
|
||||
<constraint firstAttribute="bottom" secondItem="UdR-PC-WoG" secondAttribute="bottom" constant="10" id="uZ7-Ar-ZOa"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="7xd-p5-QUU">
|
||||
<rect key="frame" x="0.0" y="259" width="375" height="50"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="50" id="CaT-lg-9Dz"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<inset key="contentEdgeInsets" minX="20" minY="0.0" maxX="20" maxY="0.0"/>
|
||||
<state key="normal" title="Don’t know your recovery key ? You can use your recovery key.">
|
||||
<color key="titleColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
</state>
|
||||
<state key="disabled">
|
||||
<color key="titleColor" red="0.47843137250000001" green="0.78823529410000004" blue="0.63137254899999995" alpha="0.5" colorSpace="calibratedRGB"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="unknownPassphraseButtonAction:" destination="xgV-qW-Fsz" eventType="touchUpInside" id="kRi-a9-0hI"/>
|
||||
<action selector="usePassphraseButtonAction:" destination="xgV-qW-Fsz" eventType="touchUpInside" id="l9T-Gu-hd9"/>
|
||||
</connections>
|
||||
</button>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="xo4-pw-QbA">
|
||||
<rect key="frame" x="0.0" y="339" width="375" height="50"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="HrF-Jh-mlh">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="50"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="50" id="PEw-6U-ojN"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<inset key="contentEdgeInsets" minX="10" minY="0.0" maxX="10" maxY="0.0"/>
|
||||
<state key="normal" title="Unlock History">
|
||||
<color key="titleColor" red="0.47843137250000001" green="0.78823529410000004" blue="0.63137254899999995" alpha="1" colorSpace="calibratedRGB"/>
|
||||
</state>
|
||||
<state key="disabled">
|
||||
<color key="titleColor" red="0.47843137250000001" green="0.78823529410000004" blue="0.63137254899999995" alpha="0.5" colorSpace="calibratedRGB"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="usePassphraseButtonAction:" destination="xgV-qW-Fsz" eventType="touchUpInside" id="fV5-El-BUo"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="HrF-Jh-mlh" secondAttribute="trailing" id="HUg-pF-aeX"/>
|
||||
<constraint firstItem="HrF-Jh-mlh" firstAttribute="leading" secondItem="xo4-pw-QbA" secondAttribute="leading" id="L0R-Jy-qhk"/>
|
||||
<constraint firstAttribute="bottom" secondItem="HrF-Jh-mlh" secondAttribute="bottom" id="Qhj-tm-6gQ"/>
|
||||
<constraint firstItem="HrF-Jh-mlh" firstAttribute="top" secondItem="xo4-pw-QbA" secondAttribute="top" id="dr9-B3-nAt"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="xo4-pw-QbA" secondAttribute="bottom" constant="20" id="0B3-c2-ro5"/>
|
||||
<constraint firstItem="v8b-un-B61" firstAttribute="top" secondItem="nE7-cw-Z30" secondAttribute="bottom" constant="40" id="0OM-LN-FIF"/>
|
||||
<constraint firstAttribute="trailing" secondItem="nE7-cw-Z30" secondAttribute="trailing" constant="20" id="25y-AS-2UT"/>
|
||||
<constraint firstItem="7xd-p5-QUU" firstAttribute="top" secondItem="v8b-un-B61" secondAttribute="bottom" constant="16" id="45T-0y-Gkx"/>
|
||||
<constraint firstItem="xo4-pw-QbA" firstAttribute="leading" secondItem="lYT-Tl-Mah" secondAttribute="leading" id="5h3-ut-ljD"/>
|
||||
<constraint firstItem="xo4-pw-QbA" firstAttribute="top" secondItem="7xd-p5-QUU" secondAttribute="bottom" constant="30" id="5mE-RV-t2R"/>
|
||||
<constraint firstItem="nE7-cw-Z30" firstAttribute="top" secondItem="eS4-VO-THO" secondAttribute="bottom" constant="30" id="87H-5c-7L4"/>
|
||||
<constraint firstAttribute="trailing" secondItem="7xd-p5-QUU" secondAttribute="trailing" id="DC3-Ch-3nU"/>
|
||||
<constraint firstItem="eS4-VO-THO" firstAttribute="top" secondItem="lYT-Tl-Mah" secondAttribute="top" constant="35" id="JwA-G2-VIj"/>
|
||||
<constraint firstAttribute="trailing" secondItem="v8b-un-B61" secondAttribute="trailing" id="MIp-Z4-OI9"/>
|
||||
<constraint firstItem="v8b-un-B61" firstAttribute="leading" secondItem="lYT-Tl-Mah" secondAttribute="leading" id="XIH-Fa-RSj"/>
|
||||
<constraint firstItem="eS4-VO-THO" firstAttribute="centerX" secondItem="lYT-Tl-Mah" secondAttribute="centerX" id="YZk-TW-2Yb"/>
|
||||
<constraint firstAttribute="trailing" secondItem="xo4-pw-QbA" secondAttribute="trailing" id="fWz-BT-edl"/>
|
||||
<constraint firstItem="nE7-cw-Z30" firstAttribute="leading" secondItem="lYT-Tl-Mah" secondAttribute="leading" constant="20" id="ooI-aS-mXQ"/>
|
||||
<constraint firstAttribute="width" priority="750" constant="500" id="qpC-Rx-87A"/>
|
||||
<constraint firstItem="7xd-p5-QUU" firstAttribute="leading" secondItem="lYT-Tl-Mah" secondAttribute="leading" id="wLU-2M-phR"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="lYT-Tl-Mah" secondAttribute="trailing" id="DkZ-bd-vfI"/>
|
||||
<constraint firstItem="lYT-Tl-Mah" firstAttribute="top" secondItem="byL-eq-NOk" secondAttribute="top" id="PVX-TK-31L"/>
|
||||
<constraint firstItem="lYT-Tl-Mah" firstAttribute="centerX" secondItem="byL-eq-NOk" secondAttribute="centerX" id="pNE-0d-Vub"/>
|
||||
<constraint firstItem="lYT-Tl-Mah" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="byL-eq-NOk" secondAttribute="leading" id="qXS-tx-xAN"/>
|
||||
<constraint firstAttribute="bottom" secondItem="lYT-Tl-Mah" secondAttribute="bottom" id="whb-v3-UuY"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="byL-eq-NOk" firstAttribute="top" secondItem="lGc-J1-CgX" secondAttribute="top" id="5KQ-5e-Peq"/>
|
||||
<constraint firstAttribute="trailing" secondItem="byL-eq-NOk" secondAttribute="trailing" id="Ubm-Z5-qm2"/>
|
||||
<constraint firstItem="byL-eq-NOk" firstAttribute="leading" secondItem="lGc-J1-CgX" secondAttribute="leading" id="bsT-kH-wQg"/>
|
||||
<constraint firstAttribute="bottom" secondItem="byL-eq-NOk" secondAttribute="bottom" id="vgR-dG-N3N"/>
|
||||
<constraint firstItem="byL-eq-NOk" firstAttribute="width" secondItem="lGc-J1-CgX" secondAttribute="width" id="yro-du-eKi"/>
|
||||
</constraints>
|
||||
</scrollView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.94509803920000002" green="0.96078431369999995" blue="0.97254901959999995" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="cJ5-FM-XjF" firstAttribute="trailing" secondItem="lGc-J1-CgX" secondAttribute="trailing" id="D6o-OE-lJC"/>
|
||||
<constraint firstItem="lGc-J1-CgX" firstAttribute="leading" secondItem="cJ5-FM-XjF" secondAttribute="leading" id="buk-UU-nV0"/>
|
||||
<constraint firstItem="cJ5-FM-XjF" firstAttribute="top" secondItem="lGc-J1-CgX" secondAttribute="top" id="tzo-69-ROT"/>
|
||||
<constraint firstAttribute="bottom" secondItem="lGc-J1-CgX" secondAttribute="bottom" id="zkd-IJ-IOG"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="cJ5-FM-XjF"/>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="importFileButton" destination="5nP-Tq-Idd" id="4Ln-F9-CRr"/>
|
||||
<outlet property="informationLabel" destination="nE7-cw-Z30" id="hue-l8-iKj"/>
|
||||
<outlet property="recoverButton" destination="HrF-Jh-mlh" id="t9E-gY-qL1"/>
|
||||
<outlet property="recoverButtonBackgroundView" destination="xo4-pw-QbA" id="7MX-fj-8bz"/>
|
||||
<outlet property="recoveryKeyTextField" destination="e79-U2-RHY" id="xEP-LQ-rKR"/>
|
||||
<outlet property="recoveryKeyTextFieldBackgroundView" destination="v8b-un-B61" id="YIA-Sn-A22"/>
|
||||
<outlet property="recoveryKeyTitleLabel" destination="UdR-PC-WoG" id="R8h-o0-2tR"/>
|
||||
<outlet property="scrollView" destination="lGc-J1-CgX" id="9Il-vB-zoh"/>
|
||||
<outlet property="shieldImageView" destination="eS4-VO-THO" id="P6x-QL-Mqv"/>
|
||||
<outlet property="unknownRecoveryKeyButton" destination="7xd-p5-QUU" id="d2Z-b2-18B"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Nrf-mX-a2q" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-3772" y="-774"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="import_files_button" width="48" height="48"/>
|
||||
<image name="key_backup_logo" width="48" height="46"/>
|
||||
</resources>
|
||||
</document>
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
/*
|
||||
Copyright 2020 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import MobileCoreServices
|
||||
|
||||
final class SecretsRecoveryWithKeyViewController: UIViewController {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Outlets
|
||||
|
||||
@IBOutlet private weak var scrollView: UIScrollView!
|
||||
|
||||
@IBOutlet private weak var shieldImageView: UIImageView!
|
||||
|
||||
@IBOutlet private weak var informationLabel: UILabel!
|
||||
|
||||
@IBOutlet private weak var recoveryKeyTitleLabel: UILabel!
|
||||
@IBOutlet private weak var recoveryKeyTextField: UITextField!
|
||||
@IBOutlet private weak var recoveryKeyTextFieldBackgroundView: UIView!
|
||||
|
||||
@IBOutlet private weak var importFileButton: UIButton!
|
||||
|
||||
@IBOutlet private weak var unknownRecoveryKeyButton: UIButton!
|
||||
|
||||
@IBOutlet private weak var recoverButtonBackgroundView: UIView!
|
||||
@IBOutlet private weak var recoverButton: UIButton!
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private var viewModel: SecretsRecoveryWithKeyViewModelType!
|
||||
private var keyboardAvoider: KeyboardAvoider?
|
||||
private var theme: Theme!
|
||||
private var errorPresenter: MXKErrorPresentation!
|
||||
private var activityPresenter: ActivityIndicatorPresenter!
|
||||
private weak var skipAlertController: UIAlertController?
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
class func instantiate(with viewModel: SecretsRecoveryWithKeyViewModelType) -> SecretsRecoveryWithKeyViewController {
|
||||
let viewController = StoryboardScene.SecretsRecoveryWithKeyViewController.initialScene.instantiate()
|
||||
viewController.viewModel = viewModel
|
||||
viewController.theme = ThemeService.shared().theme
|
||||
return viewController
|
||||
}
|
||||
|
||||
// MARK: - Life cycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
// Do any additional setup after loading the view.
|
||||
|
||||
self.vc_removeBackTitle()
|
||||
|
||||
self.setupViews()
|
||||
self.keyboardAvoider = KeyboardAvoider(scrollViewContainerView: self.view, scrollView: self.scrollView)
|
||||
self.activityPresenter = ActivityIndicatorPresenter()
|
||||
self.errorPresenter = MXKErrorAlertPresentation()
|
||||
|
||||
self.registerThemeServiceDidChangeThemeNotification()
|
||||
self.update(theme: self.theme)
|
||||
|
||||
self.viewModel.viewDelegate = self
|
||||
}
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return self.theme.statusBarStyle
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func setupViews() {
|
||||
let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in
|
||||
self?.cancelButtonAction()
|
||||
}
|
||||
self.navigationItem.rightBarButtonItem = cancelBarButtonItem
|
||||
|
||||
self.title = VectorL10n.secretsRecoveryTitle
|
||||
|
||||
self.scrollView.keyboardDismissMode = .interactive
|
||||
|
||||
let shieldImage = Asset.Images.keyBackupLogo.image.withRenderingMode(.alwaysTemplate)
|
||||
self.shieldImageView.image = shieldImage
|
||||
|
||||
self.informationLabel.text = VectorL10n.secretsRecoveryWithKeyInformation
|
||||
|
||||
self.recoveryKeyTitleLabel.text = VectorL10n.secretsRecoveryWithKeyRecoveryKeyTitle
|
||||
self.recoveryKeyTextField.addTarget(self, action: #selector(recoveryKeyTextFieldDidChange(_:)), for: .editingChanged)
|
||||
|
||||
let importFileImage = Asset.Images.importFilesButton.image.withRenderingMode(.alwaysTemplate)
|
||||
self.importFileButton.setImage(importFileImage, for: .normal)
|
||||
|
||||
self.unknownRecoveryKeyButton.vc_enableMultiLinesTitle()
|
||||
self.unknownRecoveryKeyButton.setTitle(VectorL10n.secretsRecoveryWithKeyLostRecoveryKeyAction, for: .normal)
|
||||
// Interaction is disabled for the moment
|
||||
self.unknownRecoveryKeyButton.isUserInteractionEnabled = false
|
||||
|
||||
self.recoverButton.vc_enableMultiLinesTitle()
|
||||
self.recoverButton.setTitle(VectorL10n.secretsRecoveryWithKeyRecoverAction, for: .normal)
|
||||
|
||||
self.updateRecoverButton()
|
||||
}
|
||||
|
||||
private func update(theme: Theme) {
|
||||
self.theme = theme
|
||||
|
||||
self.view.backgroundColor = theme.headerBackgroundColor
|
||||
|
||||
if let navigationBar = self.navigationController?.navigationBar {
|
||||
theme.applyStyle(onNavigationBar: navigationBar)
|
||||
}
|
||||
|
||||
self.informationLabel.textColor = theme.textPrimaryColor
|
||||
|
||||
self.shieldImageView.tintColor = theme.textPrimaryColor
|
||||
|
||||
self.recoveryKeyTextFieldBackgroundView.backgroundColor = theme.backgroundColor
|
||||
self.recoveryKeyTitleLabel.textColor = theme.textPrimaryColor
|
||||
theme.applyStyle(onTextField: self.recoveryKeyTextField)
|
||||
self.recoveryKeyTextField.attributedPlaceholder = NSAttributedString(string: VectorL10n.secretsRecoveryWithKeyRecoveryKeyPlaceholder,
|
||||
attributes: [.foregroundColor: theme.placeholderTextColor])
|
||||
|
||||
theme.applyStyle(onButton: self.importFileButton)
|
||||
|
||||
self.recoverButtonBackgroundView.backgroundColor = theme.backgroundColor
|
||||
theme.applyStyle(onButton: self.recoverButton)
|
||||
|
||||
self.unknownRecoveryKeyButton.setTitleColor(theme.textPrimaryColor, for: .normal)
|
||||
}
|
||||
|
||||
private func registerThemeServiceDidChangeThemeNotification() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil)
|
||||
}
|
||||
|
||||
@objc private func themeDidChange() {
|
||||
self.update(theme: ThemeService.shared().theme)
|
||||
}
|
||||
|
||||
private func updateRecoverButton() {
|
||||
self.recoverButton.isEnabled = self.viewModel.isFormValid
|
||||
}
|
||||
|
||||
private func render(viewState: SecretsRecoveryWithKeyViewState) {
|
||||
switch viewState {
|
||||
case .loading:
|
||||
self.renderLoading()
|
||||
case .loaded:
|
||||
self.renderLoaded()
|
||||
case .error(let error):
|
||||
self.render(error: error)
|
||||
}
|
||||
}
|
||||
|
||||
private func renderLoading() {
|
||||
self.view.endEditing(true)
|
||||
self.activityPresenter.presentActivityIndicator(on: self.view, animated: true)
|
||||
}
|
||||
|
||||
private func renderLoaded() {
|
||||
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
|
||||
}
|
||||
|
||||
private func render(error: Error) {
|
||||
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
|
||||
|
||||
// TODO: Use appropriate error codes
|
||||
if (error as NSError).domain == MXSecretStorageErrorDomain
|
||||
&& (error as NSError).code == Int(MXSecretStorageErrorCode.badMacCode.rawValue) {
|
||||
|
||||
self.errorPresenter.presentError(from: self,
|
||||
title: VectorL10n.secretsRecoveryWithKeyInvalidRecoveryKeyTitle,
|
||||
message: VectorL10n.secretsRecoveryWithKeyInvalidRecoveryKeyMessage,
|
||||
animated: true,
|
||||
handler: nil)
|
||||
} else {
|
||||
self.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func showFileSelection() {
|
||||
// Show only text documents
|
||||
let documentPicker = UIDocumentPickerViewController(documentTypes: [kUTTypeText as String], in: .import)
|
||||
documentPicker.delegate = self
|
||||
self.present(documentPicker, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
private func importRecoveryKey(from url: URL) {
|
||||
if let recoveryKey = self.getDocumentContent(from: url) {
|
||||
self.recoveryKeyTextField.text = recoveryKey
|
||||
self.recoveryKeyTextFieldDidChange(self.recoveryKeyTextField)
|
||||
} else {
|
||||
self.errorPresenter.presentGenericError(from: self, animated: true, handler: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func getDocumentContent(from documentURL: URL) -> String? {
|
||||
let documentContent: String?
|
||||
|
||||
do {
|
||||
documentContent = try String(contentsOf: documentURL)
|
||||
} catch {
|
||||
print("Error: \(error)")
|
||||
documentContent = nil
|
||||
}
|
||||
|
||||
return documentContent
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
private func cancelButtonAction() {
|
||||
self.viewModel.process(viewAction: .cancel)
|
||||
}
|
||||
|
||||
@IBAction private func importFileButtonAction(_ sender: Any) {
|
||||
self.showFileSelection()
|
||||
}
|
||||
|
||||
@objc private func recoveryKeyTextFieldDidChange(_ textField: UITextField) {
|
||||
self.viewModel.recoveryKey = textField.text
|
||||
self.updateRecoverButton()
|
||||
}
|
||||
|
||||
@IBAction private func usePassphraseButtonAction(_ sender: Any) {
|
||||
self.viewModel.process(viewAction: .recover)
|
||||
}
|
||||
|
||||
@IBAction private func unknownPassphraseButtonAction(_ sender: Any) {
|
||||
self.viewModel.process(viewAction: .unknownRecoveryKey)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITextFieldDelegate
|
||||
extension SecretsRecoveryWithKeyViewController: UITextFieldDelegate {
|
||||
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
textField.resignFirstResponder()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SecretsRecoveryWithKeyViewModelViewDelegate
|
||||
extension SecretsRecoveryWithKeyViewController: SecretsRecoveryWithKeyViewModelViewDelegate {
|
||||
func secretsRecoveryWithKeyViewModel(_ viewModel: SecretsRecoveryWithKeyViewModelType, didUpdateViewState viewSate: SecretsRecoveryWithKeyViewState) {
|
||||
self.render(viewState: viewSate)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIDocumentPickerDelegate
|
||||
extension SecretsRecoveryWithKeyViewController: UIDocumentPickerDelegate {
|
||||
|
||||
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||
guard let documentUrl = urls.first else {
|
||||
return
|
||||
}
|
||||
self.importRecoveryKey(from: documentUrl)
|
||||
}
|
||||
|
||||
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt url: URL) {
|
||||
self.importRecoveryKey(from: url)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
Copyright 2020 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
final class SecretsRecoveryWithKeyViewModel: SecretsRecoveryWithKeyViewModelType {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let recoveryService: MXRecoveryService
|
||||
private var currentHTTPOperation: MXHTTPOperation?
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var recoveryKey: String?
|
||||
|
||||
var isFormValid: Bool {
|
||||
return self.recoveryKey?.isEmpty == false
|
||||
}
|
||||
|
||||
weak var viewDelegate: SecretsRecoveryWithKeyViewModelViewDelegate?
|
||||
weak var coordinatorDelegate: SecretsRecoveryWithKeyViewModelCoordinatorDelegate?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(recoveryService: MXRecoveryService) {
|
||||
self.recoveryService = recoveryService
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.currentHTTPOperation?.cancel()
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func process(viewAction: SecretsRecoveryWithKeyViewAction) {
|
||||
switch viewAction {
|
||||
case .recover:
|
||||
self.recover()
|
||||
case .cancel:
|
||||
self.coordinatorDelegate?.secretsRecoveryWithKeyViewModelDidCancel(self)
|
||||
case .unknownRecoveryKey:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func recover() {
|
||||
guard let recoveryKey = self.recoveryKey else {
|
||||
return
|
||||
}
|
||||
|
||||
self.update(viewState: .loading)
|
||||
|
||||
do {
|
||||
let privateKey = try self.recoveryService.privateKey(fromRecoveryKey: recoveryKey)
|
||||
|
||||
self.recoveryService.recoverSecrets(nil, withPrivateKey: privateKey, recoverServices: true, success: { [weak self] _ in
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
self.update(viewState: .loaded)
|
||||
self.coordinatorDelegate?.secretsRecoveryWithKeyViewModelDidRecover(self)
|
||||
}, failure: { [weak self] error in
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
self.update(viewState: .error(error))
|
||||
})
|
||||
} catch {
|
||||
self.update(viewState: .error(error))
|
||||
}
|
||||
}
|
||||
|
||||
private func update(viewState: SecretsRecoveryWithKeyViewState) {
|
||||
self.viewDelegate?.secretsRecoveryWithKeyViewModel(self, didUpdateViewState: viewState)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
Copyright 2020 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol SecretsRecoveryWithKeyViewModelViewDelegate: class {
|
||||
func secretsRecoveryWithKeyViewModel(_ viewModel: SecretsRecoveryWithKeyViewModelType, didUpdateViewState viewSate: SecretsRecoveryWithKeyViewState)
|
||||
}
|
||||
|
||||
protocol SecretsRecoveryWithKeyViewModelCoordinatorDelegate: class {
|
||||
func secretsRecoveryWithKeyViewModelDidRecover(_ viewModel: SecretsRecoveryWithKeyViewModelType)
|
||||
func secretsRecoveryWithKeyViewModelDidCancel(_ viewModel: SecretsRecoveryWithKeyViewModelType)
|
||||
}
|
||||
|
||||
/// Protocol describing the view model used by `SecretsRecoveryWithPassphraseViewController`
|
||||
protocol SecretsRecoveryWithKeyViewModelType {
|
||||
|
||||
var recoveryKey: String? { get set }
|
||||
var isFormValid: Bool { get }
|
||||
|
||||
var viewDelegate: SecretsRecoveryWithKeyViewModelViewDelegate? { get set }
|
||||
var coordinatorDelegate: SecretsRecoveryWithKeyViewModelCoordinatorDelegate? { get set }
|
||||
|
||||
func process(viewAction: SecretsRecoveryWithKeyViewAction)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
Copyright 2020 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
/// SecretsRecoveryWithKeyViewController view state
|
||||
enum SecretsRecoveryWithKeyViewState {
|
||||
case loading
|
||||
case loaded
|
||||
case error(Error)
|
||||
}
|
||||
Reference in New Issue
Block a user