diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2020-03-02 13:29:40 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2020-03-17 10:51:06 +0100 |
| commit | 00ccba310b4466e777dfa941e3a91d83996092db (patch) | |
| tree | 6fbeb3b04b10ed89621ca719d0d30de65463985a | |
| parent | f31b0d92c3be5385548d51319a261f08e82ef05f (diff) | |
| download | mullvadvpn-00ccba310b4466e777dfa941e3a91d83996092db.tar.xz mullvadvpn-00ccba310b4466e777dfa941e3a91d83996092db.zip | |
Copy public key to clipboard on tap
| -rw-r--r-- | ios/MullvadVPN/Base.lproj/Main.storyboard | 55 | ||||
| -rw-r--r-- | ios/MullvadVPN/WireguardKeysViewController.swift | 59 | ||||
| -rw-r--r-- | ios/MullvadVPN/WireguardPrivateKey.swift | 12 |
3 files changed, 93 insertions, 33 deletions
diff --git a/ios/MullvadVPN/Base.lproj/Main.storyboard b/ios/MullvadVPN/Base.lproj/Main.storyboard index 0cdf172a74..ce028e4a78 100644 --- a/ios/MullvadVPN/Base.lproj/Main.storyboard +++ b/ios/MullvadVPN/Base.lproj/Main.storyboard @@ -40,7 +40,7 @@ <rect key="frame" x="0.0" y="0.0" width="375" height="93"/> <subviews> <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="LogoIcon" translatesAutoresizingMaskIntoConstraints="NO" id="cKg-hE-JsS"> - <rect key="frame" x="12" y="31.5" width="49" height="50"/> + <rect key="frame" x="12" y="31.5" width="49.000000000000014" height="50"/> </imageView> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="uXv-Tf-PET"> <rect key="frame" x="335" y="44.5" width="24" height="24"/> @@ -183,7 +183,7 @@ </constraints> </view> <view hidden="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Ire-2z-eJu" userLabel="Footer"> - <rect key="frame" x="0.0" y="576.5" width="375" height="90.5"/> + <rect key="frame" x="0.0" y="577.5" width="375" height="89.5"/> <subviews> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Don't have an account number?" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="QcG-Tf-YdQ"> <rect key="frame" x="24" y="16" width="327" height="20.5"/> @@ -192,7 +192,7 @@ <nil key="highlightedColor"/> </label> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="osm-vd-aTb" customClass="AppButton" customModule="MullvadVPN" customModuleProvider="target"> - <rect key="frame" x="24" y="44.5" width="327" height="22"/> + <rect key="frame" x="24" y="44.5" width="327" height="21"/> <state key="normal" title="Create account" backgroundImage="DefaultButton"> <color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> </state> @@ -367,10 +367,10 @@ <color key="separatorColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <prototypes> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="Account" id="ghE-jC-RWf" customClass="SettingsAccountCell" customModule="MullvadVPN" customModuleProvider="target"> - <rect key="frame" x="0.0" y="55.5" width="375" height="43.5"/> + <rect key="frame" x="0.0" y="55.5" width="375" height="43"/> <autoresizingMask key="autoresizingMask"/> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ghE-jC-RWf" id="sTl-gI-g2a"> - <rect key="frame" x="0.0" y="0.0" width="348" height="43.5"/> + <rect key="frame" x="0.0" y="0.0" width="348" height="43"/> <autoresizingMask key="autoresizingMask"/> <subviews> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Account" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Lve-Kd-qTr"> @@ -405,10 +405,10 @@ </connections> </tableViewCell> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="AppVersion" id="pbd-iC-Emm" customClass="SettingsAppVersionCell" customModule="MullvadVPN" customModuleProvider="target"> - <rect key="frame" x="0.0" y="99" width="375" height="43.5"/> + <rect key="frame" x="0.0" y="98.5" width="375" height="43"/> <autoresizingMask key="autoresizingMask"/> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="pbd-iC-Emm" id="lYp-Z8-1sN"> - <rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/> + <rect key="frame" x="0.0" y="0.0" width="375" height="43"/> <autoresizingMask key="autoresizingMask"/> <subviews> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="App version" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pYC-Zb-8N9"> @@ -440,10 +440,10 @@ </connections> </tableViewCell> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="BasicDisclosure" id="Ahs-gu-nTM" customClass="SettingsBasicCell" customModule="MullvadVPN" customModuleProvider="target"> - <rect key="frame" x="0.0" y="142.5" width="375" height="43.5"/> + <rect key="frame" x="0.0" y="141.5" width="375" height="43"/> <autoresizingMask key="autoresizingMask"/> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Ahs-gu-nTM" id="Drq-vk-8F2"> - <rect key="frame" x="0.0" y="0.0" width="348" height="43.5"/> + <rect key="frame" x="0.0" y="0.0" width="348" height="43"/> <autoresizingMask key="autoresizingMask"/> <subviews> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Amw-A3-ePS"> @@ -505,16 +505,16 @@ <rect key="frame" x="0.0" y="0.0" width="375" height="647"/> <subviews> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="5VO-oQ-4jM" userLabel="Container"> - <rect key="frame" x="0.0" y="0.0" width="375" height="295"/> + <rect key="frame" x="0.0" y="0.0" width="375" height="295.5"/> <subviews> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Lx5-tV-hNL" userLabel="Content"> - <rect key="frame" x="24" y="24" width="327" height="247"/> + <rect key="frame" x="24" y="24" width="327" height="247.5"/> <subviews> <view contentMode="scaleToFill" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="xch-VD-kOQ" userLabel="Account number"> - <rect key="frame" x="0.0" y="0.0" width="327" height="45.5"/> + <rect key="frame" x="0.0" y="0.0" width="327" height="46"/> <subviews> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="CtE-XF-2uJ"> - <rect key="frame" x="0.0" y="0.0" width="327" height="45.5"/> + <rect key="frame" x="0.0" y="0.0" width="327" height="46"/> <subviews> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="29J-lm-A4D"> <rect key="frame" x="0.0" y="0.0" width="327" height="17"/> @@ -547,12 +547,17 @@ <constraint firstItem="Z76-2G-dkn" firstAttribute="top" secondItem="29J-lm-A4D" secondAttribute="top" id="w1K-nl-2QW"/> </constraints> </view> - <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="123456789" textAlignment="natural" lineBreakMode="characterWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="UF1-4s-m1S"> - <rect key="frame" x="0.0" y="25" width="327" height="20.5"/> + <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="leading" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="tailTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="F0p-ra-1bv"> + <rect key="frame" x="0.0" y="25" width="327" height="21"/> <fontDescription key="fontDescription" type="system" weight="medium" pointSize="17"/> - <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> - <nil key="highlightedColor"/> - </label> + <inset key="contentEdgeInsets" minX="0.01" minY="0.0" maxX="1" maxY="0.0"/> + <state key="normal" title="123456789"> + <color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + </state> + <connections> + <action selector="copyPublicKey:" destination="vAK-MJ-h3c" eventType="touchUpInside" id="xqz-Xp-I45"/> + </connections> + </button> </subviews> </stackView> </subviews> @@ -564,7 +569,7 @@ </constraints> </view> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Bjs-D6-NVj" userLabel="Expiry"> - <rect key="frame" x="0.0" y="69.5" width="327" height="45.5"/> + <rect key="frame" x="0.0" y="70" width="327" height="45.5"/> <subviews> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="8f3-SD-t3K"> <rect key="frame" x="0.0" y="0.0" width="327" height="45.5"/> @@ -592,7 +597,7 @@ </constraints> </view> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ydQ-IP-KZb" userLabel="Buttons"> - <rect key="frame" x="0.0" y="139" width="327" height="108"/> + <rect key="frame" x="0.0" y="139.5" width="327" height="108"/> <subviews> <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="24" translatesAutoresizingMaskIntoConstraints="NO" id="zF0-5W-t7M"> <rect key="frame" x="0.0" y="0.0" width="327" height="108"/> @@ -675,7 +680,7 @@ <navigationItem key="navigationItem" title="WireGuard key" id="6ve-v7-tYQ"/> <connections> <outlet property="creationDateLabel" destination="CvU-pV-ixr" id="qSg-Be-sO0"/> - <outlet property="publicKeyLabel" destination="UF1-4s-m1S" id="PaX-IU-VEH"/> + <outlet property="publicKeyButton" destination="F0p-ra-1bv" id="pZV-nS-lIs"/> <outlet property="regenerateKeyButton" destination="OCa-Jz-b7W" id="BZD-Qc-8lO"/> <outlet property="verifyKeyButton" destination="qEF-8w-MdR" id="zuU-Ts-QNG"/> <outlet property="wireguardKeyStatusView" destination="1Ue-pb-GCu" id="VV8-bG-SgD"/> @@ -895,7 +900,7 @@ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <subviews> <scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" translatesAutoresizingMaskIntoConstraints="NO" id="JYh-33-d0O"> - <rect key="frame" x="0.0" y="0.0" width="375" height="597"/> + <rect key="frame" x="0.0" y="0.0" width="375" height="598"/> <subviews> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="N9k-cQ-tlw" userLabel="Content view"> <rect key="frame" x="0.0" y="0.0" width="375" height="558"/> @@ -934,7 +939,7 @@ <nil key="highlightedColor"/> </label> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="leading" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Cas-Tk-gcz" customClass="LinkButton" customModule="MullvadVPN" customModuleProvider="target"> - <rect key="frame" x="20" y="516" width="128" height="22"/> + <rect key="frame" x="20" y="516" width="20" height="22"/> <fontDescription key="fontDescription" name=".AppleSystemUIFont" family=".AppleSystemUIFont" pointSize="18"/> <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <state key="normal" title="Privacy Policy" image="IconExtlink"/> @@ -970,10 +975,10 @@ </constraints> </scrollView> <view contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="16P-Q0-ZO9" userLabel="Footer"> - <rect key="frame" x="0.0" y="597" width="375" height="70"/> + <rect key="frame" x="0.0" y="598" width="375" height="69"/> <subviews> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ttw-7B-1MM" customClass="AppButton" customModule="MullvadVPN" customModuleProvider="target"> - <rect key="frame" x="16" y="24" width="343" height="22"/> + <rect key="frame" x="16" y="24" width="343" height="21"/> <accessibility key="accessibilityConfiguration" identifier="AgreeButton"/> <state key="normal" title="Agree and continue" backgroundImage="DefaultButton"> <color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> diff --git a/ios/MullvadVPN/WireguardKeysViewController.swift b/ios/MullvadVPN/WireguardKeysViewController.swift index 70d9ab13bc..881200c963 100644 --- a/ios/MullvadVPN/WireguardKeysViewController.swift +++ b/ios/MullvadVPN/WireguardKeysViewController.swift @@ -14,6 +14,9 @@ import os /// A UI refresh interval for the public key creation date (in seconds) private let kCreationDateRefreshInterval = TimeInterval(60) +/// A maximum number of characters to display out of the entire public key representation +private let kDisplayPublicKeyMaxLength = 20 + private enum WireguardKeysViewState { case `default` case verifyingKey @@ -45,7 +48,7 @@ extension VerifyWireguardPublicKeyError: LocalizedError { class WireguardKeysViewController: UIViewController { - @IBOutlet var publicKeyLabel: UILabel! + @IBOutlet var publicKeyButton: UIButton! @IBOutlet var creationDateLabel: UILabel! @IBOutlet var regenerateKeyButton: UIButton! @IBOutlet var verifyKeyButton: UIButton! @@ -54,7 +57,8 @@ class WireguardKeysViewController: UIViewController { private var fetchKeySubscriber: AnyCancellable? private var verifyKeySubscriber: AnyCancellable? private var regenerateKeySubscriber: AnyCancellable? - private var timerSubscriber: AnyCancellable? + private var creationDateTimerSubscriber: AnyCancellable? + private var copyToPasteboardSubscriber: AnyCancellable? private let apiClient = MullvadAPI() private var publicKey: WireguardPublicKey? @@ -78,10 +82,10 @@ class WireguardKeysViewController: UIViewController { super.viewDidLoad() // Reset Storyboard placeholders - publicKeyLabel.text = "-" + setPublicKeyTitle(string: "-", animated: false) creationDateLabel.text = "-" - timerSubscriber = Timer.publish(every: kCreationDateRefreshInterval, on: .main, in: .common) + creationDateTimerSubscriber = Timer.publish(every: kCreationDateRefreshInterval, on: .main, in: .common) .autoconnect() .sink { [weak self] _ in guard let self = self else { return } @@ -91,11 +95,32 @@ class WireguardKeysViewController: UIViewController { } } - loadPublicKey() + loadPublicKey(animated: false) } // MARK: - IBActions + @IBAction func copyPublicKey(_ sender: Any) { + guard let publicKey = self.publicKey else { return } + + UIPasteboard.general.string = publicKey.stringRepresentation() + + setPublicKeyTitle( + string: NSLocalizedString("COPIED TO PASTEBOARD!", comment: ""), + animated: true) + + copyToPasteboardSubscriber = + Just(()).delay(for: .seconds(3), scheduler: DispatchQueue.main) + .sink(receiveValue: { [weak self] _ in + guard let self = self, let publicKey = self.publicKey else { return } + + let displayKey = publicKey + .stringRepresentation(maxLength: kDisplayPublicKeyMaxLength) + + self.setPublicKeyTitle(string: displayKey, animated: true) + }) + } + @IBAction func handleRegenerateKey(_ sender: Any) { regeneratePrivateKey() } @@ -129,7 +154,7 @@ class WireguardKeysViewController: UIViewController { creationDateLabel.text = formatKeyGenerationElapsedTime(with: creationDate) ?? "-" } - private func loadPublicKey() { + private func loadPublicKey(animated: Bool) { fetchKeySubscriber = TunnelManager.shared.getWireguardPublicKey() .receive(on: DispatchQueue.main) .sink(receiveCompletion: { (completion) in @@ -146,7 +171,10 @@ class WireguardKeysViewController: UIViewController { }) { [weak self] (publicKey) in guard let self = self else { return } - self.publicKeyLabel.text = publicKey.rawRepresentation.base64EncodedString() + let displayKey = publicKey + .stringRepresentation(maxLength: kDisplayPublicKeyMaxLength) + + self.setPublicKeyTitle(string: displayKey, animated: animated) self.updateCreationDateLabel(with: publicKey.creationDate) self.publicKey = publicKey @@ -219,7 +247,7 @@ class WireguardKeysViewController: UIViewController { .sink { (completion) in switch completion { case .finished: - self.loadPublicKey() + self.loadPublicKey(animated: true) case .failure(let error): os_log(.error, "Failed to re-generate the private key: %{public}s", @@ -230,6 +258,21 @@ class WireguardKeysViewController: UIViewController { } } + private func setPublicKeyTitle(string: String, animated: Bool) { + let updateTitle = { + self.publicKeyButton.setTitle(string, for: .normal) + } + + if animated { + updateTitle() + } else { + UIView.performWithoutAnimation { + updateTitle() + publicKeyButton.layoutIfNeeded() + } + } + } + } class WireguardKeyStatusView: UIView { diff --git a/ios/MullvadVPN/WireguardPrivateKey.swift b/ios/MullvadVPN/WireguardPrivateKey.swift index 573e0e6554..293750f893 100644 --- a/ios/MullvadVPN/WireguardPrivateKey.swift +++ b/ios/MullvadVPN/WireguardPrivateKey.swift @@ -58,6 +58,18 @@ struct WireguardPublicKey { /// Raw public key representation let rawRepresentation: Data + + /// Returns a base64 encoded string representation that can be used for displaying the key in + /// the user interface + func stringRepresentation(maxLength: Int? = nil) -> String { + let base64EncodedKey = rawRepresentation.base64EncodedString() + + if let maxLength = maxLength, maxLength < base64EncodedKey.count { + return base64EncodedKey.prefix(maxLength) + "..." + } else { + return base64EncodedKey + } + } } extension WireguardPrivateKey: Codable { |
