summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2020-03-02 13:29:40 +0100
committerAndrej Mihajlov <and@mullvad.net>2020-03-17 10:51:06 +0100
commit00ccba310b4466e777dfa941e3a91d83996092db (patch)
tree6fbeb3b04b10ed89621ca719d0d30de65463985a
parentf31b0d92c3be5385548d51319a261f08e82ef05f (diff)
downloadmullvadvpn-00ccba310b4466e777dfa941e3a91d83996092db.tar.xz
mullvadvpn-00ccba310b4466e777dfa941e3a91d83996092db.zip
Copy public key to clipboard on tap
-rw-r--r--ios/MullvadVPN/Base.lproj/Main.storyboard55
-rw-r--r--ios/MullvadVPN/WireguardKeysViewController.swift59
-rw-r--r--ios/MullvadVPN/WireguardPrivateKey.swift12
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 {