Skip to content

Commit

Permalink
Merge pull request #3419 from safe-global/GH-3418/export-data
Browse files Browse the repository at this point in the history
GH-3418 created basic export and import of data
  • Loading branch information
DmitryBespalov authored Jun 7, 2024
2 parents 24c10ed + 9c87319 commit 9346226
Show file tree
Hide file tree
Showing 22 changed files with 1,804 additions and 12 deletions.
62 changes: 61 additions & 1 deletion Multisig.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

154 changes: 154 additions & 0 deletions Multisig/Features/Data Export/CreateExportPasswordViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
//
// CreateExportPasswordViewController.swift
// Multisig
//
// Created by Dmitrii Bespalov on 03.06.24.
// Copyright © 2024 Core Contributors GmbH. All rights reserved.
//


import UIKit

class CreateExportPasswordViewController: UIViewController {

var prompt: String = ""
var placeholder: String = ""
var plainTextPassword: String?
var completion: (String) -> Void = { _ in }
var validateValue: (String) -> Error? = { _ in nil }

@IBOutlet weak var textField: GNOTextField!
@IBOutlet weak var descriptionLabel: UILabel!
@IBOutlet weak var continueButton: UIButton!
@IBOutlet weak var scrollView: UIScrollView!
@IBOutlet weak var buttonBottomConstraint: NSLayoutConstraint!

private var debounceTimer: Timer!
private let debounceDuration: TimeInterval = 0.250
private var isValid: Bool = false

private var keyboardBehavior: KeyboardAvoidingBehavior!

var trackingEvent: TrackingEvent?
var trackingParameters: [String: Any]? = nil

override func viewDidLoad() {
super.viewDidLoad()

keyboardBehavior = KeyboardAvoidingBehavior(scrollView: scrollView)

textField.setPlaceholder(placeholder)
textField.textField.isSecureTextEntry = true
textField.textField.delegate = self
textField.textField.clearButtonMode = .always

if let value = plainTextPassword {
textField.textField.text = value
}

descriptionLabel.text = prompt
descriptionLabel.setStyle(.body)

continueButton.setText("Continue", .filled)

validateText()

NotificationCenter.default.addObserver(self,
selector: #selector(willShowKeyboard(_:)),
name: UIResponder.keyboardWillShowNotification,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(willHideKeyboard(_:)),
name: UIResponder.keyboardWillHideNotification,
object: nil)

}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
keyboardBehavior.start()
}

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let trackingEvent = trackingEvent {
Tracker.trackEvent(trackingEvent, parameters: trackingParameters)
}
}

override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
keyboardBehavior.stop()
}

@objc func willShowKeyboard(_ notification: NSNotification) {
guard
let frameEnd = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue,
let animationDuration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber
else {
return
}

UIView.animate(withDuration: animationDuration.doubleValue) { [weak self] in
self?.buttonBottomConstraint?.constant = 16 + frameEnd.cgRectValue.height
self?.view?.layoutIfNeeded()
}
}

@objc func willHideKeyboard(_ notification: NSNotification) {
guard
let animationDuration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber
else {
return
}
UIView.animate(withDuration: animationDuration.doubleValue) { [weak self] in
self?.buttonBottomConstraint?.constant = 16
self?.view?.layoutIfNeeded()
}
}

private func validateText() {
isValid = false
continueButton.isEnabled = false
textField.setError(nil)
guard let text = textField.textField.text?.trimmingCharacters(in: .whitespacesAndNewlines),
!text.isEmpty else {
self.plainTextPassword = nil
return
}
if let error = validateValue(text) {
textField.setError(error)
return
}
self.plainTextPassword = text
continueButton.isEnabled = true
isValid = true
}

@IBAction func didTapContinue(_ sender: Any) {
completion(plainTextPassword!)
}
}

extension CreateExportPasswordViewController: UITextFieldDelegate {

func textFieldDidBeginEditing(_ textField: UITextField) {
keyboardBehavior.activeTextField = textField
}

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
debounceTimer?.invalidate()
debounceTimer = Timer.scheduledTimer(withTimeInterval: debounceDuration, repeats: false, block: { [weak self] _ in
self?.validateText()
})
return true
}

func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if isValid {
didTapContinue(textField)
textField.resignFirstResponder()
}
return true
}
}
117 changes: 117 additions & 0 deletions Multisig/Features/Data Export/CreateExportPasswordViewController.xib
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="CreateExportPasswordViewController" customModule="Multisig" customModuleProvider="target">
<connections>
<outlet property="buttonBottomConstraint" destination="BLR-Ri-2c1" id="4Mc-DW-AHm"/>
<outlet property="continueButton" destination="gXy-yX-Ihv" id="syz-ft-J4k"/>
<outlet property="descriptionLabel" destination="qCR-Ls-3Sv" id="jfH-3z-Fne"/>
<outlet property="scrollView" destination="HvJ-08-RJo" id="Tzi-uc-zzt"/>
<outlet property="textField" destination="YAC-g2-UKn" id="96R-Fu-Bt2"/>
<outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="i5M-Pr-FkT">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="HvJ-08-RJo">
<rect key="frame" x="0.0" y="48" width="414" height="814"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="jVT-TE-2xN" userLabel="ContentView">
<rect key="frame" x="0.0" y="0.0" width="414" height="814"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="32" translatesAutoresizingMaskIntoConstraints="NO" id="1Gx-rA-E0l">
<rect key="frame" x="16" y="16" width="382" height="105"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="LsW-zt-ujG">
<rect key="frame" x="0.0" y="0.0" width="382" height="105"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Choose a password for protecting the exported data." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="qCR-Ls-3Sv">
<rect key="frame" x="0.0" y="0.0" width="382" height="41"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="YAC-g2-UKn" customClass="GNOTextField" customModule="Multisig" customModuleProvider="target">
<rect key="frame" x="0.0" y="49" width="382" height="56"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstAttribute="height" constant="56" placeholder="YES" id="OVz-U6-IPi"/>
</constraints>
</view>
</subviews>
<constraints>
<constraint firstItem="YAC-g2-UKn" firstAttribute="width" secondItem="LsW-zt-ujG" secondAttribute="width" id="Zri-CE-aSn"/>
</constraints>
</stackView>
</subviews>
</stackView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="gXy-yX-Ihv">
<rect key="frame" x="16" y="742" width="382" height="56"/>
<constraints>
<constraint firstAttribute="height" constant="56" id="2Bc-ES-1VX"/>
</constraints>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" title="Continue">
<color key="titleColor" name="labelPrimary"/>
</state>
<connections>
<action selector="didTapContinue:" destination="-1" eventType="touchUpInside" id="6Nr-QZ-zb8"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="gXy-yX-Ihv" firstAttribute="top" relation="greaterThanOrEqual" secondItem="1Gx-rA-E0l" secondAttribute="bottom" constant="16" id="0Rn-m7-otO"/>
<constraint firstItem="1Gx-rA-E0l" firstAttribute="top" secondItem="jVT-TE-2xN" secondAttribute="top" constant="16" id="2BR-bH-HVW"/>
<constraint firstAttribute="trailing" secondItem="1Gx-rA-E0l" secondAttribute="trailing" constant="16" id="8p3-ZY-H2z"/>
<constraint firstAttribute="bottom" secondItem="gXy-yX-Ihv" secondAttribute="bottom" constant="16" id="BLR-Ri-2c1"/>
<constraint firstItem="gXy-yX-Ihv" firstAttribute="leading" secondItem="jVT-TE-2xN" secondAttribute="leading" constant="16" id="E93-hD-oTa"/>
<constraint firstItem="1Gx-rA-E0l" firstAttribute="leading" secondItem="jVT-TE-2xN" secondAttribute="leading" constant="16" id="dYE-y4-oh4"/>
<constraint firstAttribute="trailing" secondItem="gXy-yX-Ihv" secondAttribute="trailing" constant="16" id="kzJ-kx-0AE"/>
</constraints>
</view>
</subviews>
<constraints>
<constraint firstItem="jVT-TE-2xN" firstAttribute="top" secondItem="EPz-Ac-Ohw" secondAttribute="top" id="KZx-r4-vvR"/>
<constraint firstItem="jVT-TE-2xN" firstAttribute="trailing" secondItem="EPz-Ac-Ohw" secondAttribute="trailing" id="fMW-SC-z0V"/>
<constraint firstItem="jVT-TE-2xN" firstAttribute="bottom" secondItem="EPz-Ac-Ohw" secondAttribute="bottom" id="oib-vQ-b9X"/>
<constraint firstItem="jVT-TE-2xN" firstAttribute="width" secondItem="JzW-9w-9wW" secondAttribute="width" id="ryj-hi-tFb"/>
<constraint firstItem="jVT-TE-2xN" firstAttribute="leading" secondItem="EPz-Ac-Ohw" secondAttribute="leading" id="vp8-07-NqA"/>
</constraints>
<viewLayoutGuide key="contentLayoutGuide" id="EPz-Ac-Ohw"/>
<viewLayoutGuide key="frameLayoutGuide" id="JzW-9w-9wW"/>
</scrollView>
</subviews>
<viewLayoutGuide key="safeArea" id="fnl-2z-Ty3"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="fnl-2z-Ty3" firstAttribute="bottom" secondItem="jVT-TE-2xN" secondAttribute="bottom" id="7p2-jh-lGh"/>
<constraint firstItem="fnl-2z-Ty3" firstAttribute="bottom" secondItem="HvJ-08-RJo" secondAttribute="bottom" id="Zl6-HL-ncb"/>
<constraint firstItem="fnl-2z-Ty3" firstAttribute="trailing" secondItem="HvJ-08-RJo" secondAttribute="trailing" id="d9t-ZP-Ls8"/>
<constraint firstItem="HvJ-08-RJo" firstAttribute="top" secondItem="fnl-2z-Ty3" secondAttribute="top" id="hNc-2i-JxC"/>
<constraint firstItem="HvJ-08-RJo" firstAttribute="leading" secondItem="fnl-2z-Ty3" secondAttribute="leading" id="mnZ-df-Aqk"/>
</constraints>
<point key="canvasLocation" x="132" y="131"/>
</view>
</objects>
<resources>
<namedColor name="labelPrimary">
<color red="0.070588235294117646" green="0.074509803921568626" blue="0.070588235294117646" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>
44 changes: 44 additions & 0 deletions Multisig/Features/Data Export/ErrorViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// ErrorViewController.swift
// Multisig
//
// Created by Dmitrii Bespalov on 06.06.24.
// Copyright © 2024 Core Contributors GmbH. All rights reserved.
//

import UIKit

class ErrorViewController: UIViewController {

@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var descriptionLabel: UILabel!
@IBOutlet weak var textView: UITextView!
@IBOutlet weak var button: UIButton!

var titleText = "Operation failed"
var bodyText = "Error details are below:"
var errorText = ""
var buttonTitle = "Done"
var imageName = "square.and.arrow.up.trianglebadge.exclamationmark"

var completion: () -> Void = {}

override func viewDidLoad() {
super.viewDidLoad()
imageView.image = UIImage(systemName: imageName)
titleLabel.text = titleText
descriptionLabel.text = bodyText
textView.text = errorText
button.setText(buttonTitle, .filled)

titleLabel.setStyle(.headline)
descriptionLabel.setStyle(.body)
textView.setStyle(.bodyMedium)
}

@IBAction func done(_ sender: Any) {
completion()
}

}
Loading

0 comments on commit 9346226

Please sign in to comment.