Skip to content

Commit

Permalink
Add Coinzix login
Browse files Browse the repository at this point in the history
  • Loading branch information
esen committed Jun 22, 2023
1 parent 950ba8a commit 7cf6355
Show file tree
Hide file tree
Showing 15 changed files with 627 additions and 117 deletions.
1 change: 1 addition & 0 deletions .github/workflows/deploy_appstore.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,4 @@ jobs:
XCCONFIG_PROD_SHARED_CLOUD_CONTAINER_ID: ${{ secrets.XCCONFIG_PROD_SHARED_CLOUD_CONTAINER_ID }}
XCCONFIG_PROD_PRIVATE_CLOUD_CONTAINER_ID: ${{ secrets.XCCONFIG_PROD_PRIVATE_CLOUD_CONTAINER_ID }}
XCCONFIG_PROD_OPEN_SEA_API_KEY: ${{ secrets.XCCONFIG_PROD_OPEN_SEA_API_KEY }}
XCCONFIG_PROD_COINZIX_HCAPTCHA_KEY: ${{ secrets.XCCONFIG_PROD_COINZIX_HCAPTCHA_KEY }}
1 change: 1 addition & 0 deletions .github/workflows/deploy_dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,4 @@ jobs:
XCCONFIG_DEV_SHARED_CLOUD_CONTAINER_ID: ${{ secrets.XCCONFIG_DEV_SHARED_CLOUD_CONTAINER_ID }}
XCCONFIG_DEV_PRIVATE_CLOUD_CONTAINER_ID: ${{ secrets.XCCONFIG_DEV_PRIVATE_CLOUD_CONTAINER_ID }}
XCCONFIG_DEV_OPEN_SEA_API_KEY: ${{ secrets.XCCONFIG_DEV_OPEN_SEA_API_KEY }}
XCCONFIG_DEV_COINZIX_HCAPTCHA_KEY: ${{ secrets.XCCONFIG_DEV_COINZIX_HCAPTCHA_KEY }}
282 changes: 170 additions & 112 deletions UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ market_api_url = https:/$()/api-dev.blocksdecoded.com
hs_provider_api_key =
trongrid_api_key =
wallet_connect_v2_project_key =
coinzix_hcaptcha_key =
default_words =
shared_cloud_container_id =
private_cloud_container_id =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ market_api_url = https:/$()/api.blocksdecoded.com
hs_provider_api_key =
trongrid_api_key =
wallet_connect_v2_project_key =
coinzix_hcaptcha_key =
shared_cloud_container_id =
private_cloud_container_id =
open_sea_api_key =
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ class AppConfigProvider {
(Bundle.main.object(forInfoDictionaryKey: "WallectConnectV2ProjectKey") as? String).flatMap { $0.isEmpty ? nil : $0 }
}

var coinzixHCaptchaKey: String? {
(Bundle.main.object(forInfoDictionaryKey: "CoinzixHCaptchaKey") as? String).flatMap { $0.isEmpty ? nil : $0 }
}

var defaultWords: String {
Bundle.main.object(forInfoDictionaryKey: "DefaultWords") as? String ?? ""
}
Expand Down
2 changes: 2 additions & 0 deletions UnstoppableWallet/UnstoppableWallet/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CoinzixHCaptchaKey</key>
<string>${coinzix_hcaptcha_key}</string>
<key>ArbiscanApiKey</key>
<string>${arbiscan_api_key}</string>
<key>BscscanApiKey</key>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,16 @@ class RestoreCexViewController: ThemeViewController {
}

private func openRestore(cex: Cex) {
let viewController: UIViewController
let viewController: UIViewController?

switch cex {
case .binance: viewController = RestoreBinanceModule.viewController(returnViewController: returnViewController)
case .coinzix: viewController = RestoreBinanceModule.viewController(returnViewController: returnViewController)
case .coinzix: viewController = RestoreCoinzixModule.viewController(returnViewController: returnViewController)
}

navigationController?.pushViewController(viewController, animated: true)
if let viewController = viewController {
navigationController?.pushViewController(viewController, animated: true)
}
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import UIKit

struct RestoreCoinzixModule {

static func viewController(returnViewController: UIViewController?) -> UIViewController? {
guard let hCaptchaKey = App.shared.appConfigProvider.coinzixHCaptchaKey else {
return nil
}

let service = RestoreCoinzixService(
networkManager: App.shared.networkManager,
accountFactory: App.shared.accountFactory,
accountManager: App.shared.accountManager
)
let viewModel = RestoreCoinzixViewModel(service: service)

return RestoreCoinzixViewController(hCaptchaKey: hCaptchaKey, viewModel: viewModel, returnViewController: returnViewController)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import Combine
import ObjectMapper
import HsToolKit
import HsExtensions

class RestoreCoinzixService {
private let networkManager: NetworkManager
private let accountFactory: AccountFactory
private let accountManager: AccountManager
private var tasks = Set<AnyTask>()

var username: String = "" {
didSet {
syncState()
}
}

var password: String = "" {
didSet {
syncState()
}
}

@PostPublished private(set) var state: State = .notReady

init(networkManager: NetworkManager, accountFactory: AccountFactory, accountManager: AccountManager) {
self.networkManager = networkManager
self.accountFactory = accountFactory
self.accountManager = accountManager
}

private func syncState() {
state = username.trimmingCharacters(in: .whitespaces).isEmpty || password.trimmingCharacters(in: .whitespaces).isEmpty ? .notReady : .idle(error: nil)
}

private func createAccount(secretKey: String, token: String) {
let type: AccountType = .cex(type: .coinzix(authToken: token, secret: secretKey))
let name = accountFactory.nextAccountName(cex: .coinzix)
let account = accountFactory.account(type: type, origin: .restored, backedUp: true, name: name)

accountManager.save(account: account)

state = .loggedIn
}

}

extension RestoreCoinzixService {

func login(captchaToken: String) {
state = .loggingIn

Task { [weak self, username, password, networkManager] in
do {
let (secretKey, token) = try await CoinzixCexProvider.login(username: username, password: password, captchaToken: captchaToken, networkManager: networkManager)
self?.createAccount(secretKey: secretKey, token: token)
} catch {
self?.state = .idle(error: error)
}
}.store(in: &tasks)
}

}

extension RestoreCoinzixService {

enum State {
case notReady
case idle(error: Error?)
case loggingIn
case loggedIn
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import Combine
import UIKit
import SnapKit
import ThemeKit
import ComponentKit
import SectionsTableView
import HUD
import HCaptcha

class RestoreCoinzixViewController: KeyboardAwareViewController {
private let wrapperViewHeight: CGFloat = .heightButton + .margin16 + .heightButton
private let hcaptcha: HCaptcha
private let viewModel: RestoreCoinzixViewModel
private var webView: UIView?
private var cancellables = Set<AnyCancellable>()

private weak var returnViewController: UIViewController?

private let tableView = SectionsTableView(style: .grouped)

private let usernameCell = TextFieldCell()
private let passwordCell = PasswordInputCell()

private let buttonsHolder = BottomGradientHolder()
private let loginButton = PrimaryButton()
private let logginInButton = PrimaryButton()

private var isLoaded = false

init?(hCaptchaKey: String, viewModel: RestoreCoinzixViewModel, returnViewController: UIViewController?) {
guard let hcaptcha = try? HCaptcha(apiKey: hCaptchaKey, baseURL: URL(string: "https://api.coinzix.com")) else {
return nil
}

self.hcaptcha = hcaptcha
self.viewModel = viewModel
self.returnViewController = returnViewController

super.init(scrollViews: [tableView], accessoryView: buttonsHolder)
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
super.viewDidLoad()

title = Cex.coinzix.title

navigationItem.rightBarButtonItem = UIBarButtonItem(title: "button.cancel".localized, style: .plain, target: self, action: #selector(onTapCancel))

view.addSubview(tableView)
tableView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}

tableView.registerCell(forClass: DescriptionCell.self)
tableView.sectionDataSource = self
tableView.separatorStyle = .none
tableView.backgroundColor = .clear

usernameCell.inputPlaceholder = "restore.coinzix.sample_username".localized
usernameCell.autocapitalizationType = .none
usernameCell.onChangeText = { [weak self] in self?.viewModel.onChange(username: $0 ?? "") }

passwordCell.set(textSecure: true)
passwordCell.onTextSecurityChange = { [weak self] in self?.passwordCell.set(textSecure: $0) }
passwordCell.inputPlaceholder = "restore.coinzix.sample_password".localized
passwordCell.onChangeText = { [weak self] in self?.viewModel.onChange(password: $0 ?? "") }


view.addSubview(buttonsHolder)
buttonsHolder.snp.makeConstraints { make in
make.height.equalTo(wrapperViewHeight).priority(.high)
make.leading.trailing.bottom.equalToSuperview()
}

let stackView = UIStackView()
buttonsHolder.addSubview(stackView)
stackView.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(CGFloat.margin24)
}

stackView.axis = .vertical
stackView.spacing = .margin16

stackView.addArrangedSubview(loginButton)
loginButton.set(style: .yellow)
loginButton.setTitle("restore.coinzix.login".localized, for: .normal)
loginButton.addTarget(self, action: #selector(onTapLogin), for: .touchUpInside)

stackView.addArrangedSubview(logginInButton)
logginInButton.set(style: .yellow, accessoryType: .spinner)
logginInButton.isEnabled = false
logginInButton.setTitle("restore.coinzix.login".localized, for: .normal)

let signUpButton = PrimaryButton()
stackView.addArrangedSubview(signUpButton)
signUpButton.set(style: .transparent)
signUpButton.setTitle("restore.coinzix.sign_up".localized, for: .normal)
signUpButton.addTarget(self, action: #selector(onTapSignUp), for: .touchUpInside)

viewModel.$loginEnabled
.receive(on: DispatchQueue.main)
.sink { [weak self] enabled in self?.loginButton.isEnabled = enabled }
.store(in: &cancellables)

viewModel.$loginVisible
.receive(on: DispatchQueue.main)
.sink { [weak self] visible in self?.loginButton.isHidden = !visible }
.store(in: &cancellables)

viewModel.$logginInVisible
.receive(on: DispatchQueue.main)
.sink { [weak self] visible in self?.logginInButton.isHidden = !visible }
.store(in: &cancellables)

viewModel.errorPublisher
.receive(on: DispatchQueue.main)
.sink { text in HudHelper.instance.showErrorBanner(title: text) }
.store(in: &cancellables)

viewModel.successPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] in
HudHelper.instance.show(banner: .imported)
(self?.returnViewController ?? self)?.dismiss(animated: true)
}
.store(in: &cancellables)

additionalContentInsets = UIEdgeInsets(top: 0, left: 0, bottom: -.margin16, right: 0)
additionalInsetsOnlyForClosedKeyboard = false
ignoreSafeAreaForAccessoryView = false

tableView.buildSections()
isLoaded = true

hcaptcha.configureWebView { [weak self] webview in
webview.frame = self?.view.bounds ?? CGRect.zero
self?.webView = webview
}
}

@objc private func onTapCancel() {
dismiss(animated: true)
}

@objc private func onTapLogin() {
view.endEditing(true)

hcaptcha.validate(on: self.view) { [weak self] result in
do {
self?.viewModel.login(captchaToken: try result.dematerialize())
} catch {
print("ERROR: \(error)")
}

self?.webView?.removeFromSuperview()
}
}

@objc private func onTapSignUp() {
UrlManager.open(url: "https://coinzix.com/sign-up")
}

}

extension RestoreCoinzixViewController: SectionsDataSource {

func buildSections() -> [SectionProtocol] {
[
Section(
id: "description",
headerState: .margin(height: .margin12),
footerState: .margin(height: .margin32),
rows: [
Row<DescriptionCell>(
id: "description-cell",
dynamicHeight: { containerWidth in
DescriptionCell.height(containerWidth: containerWidth, text: "restore.coinzix.description".localized, font: .subhead2, ignoreBottomMargin: true)
},
bind: { cell, _ in
cell.label.text = "restore.coinzix.description".localized
cell.label.font = .subhead2
cell.label.textColor = .themeGray
}
)
]
),
Section(
id: "username",
headerState: .margin(height: .margin12),
rows: [
StaticRow(
cell: usernameCell,
id: "username",
height: .heightSingleLineCell
)
]
),
Section(
id: "password",
headerState: .margin(height: .margin16),
footerState: .margin(height: .margin32),
rows: [
StaticRow(
cell: passwordCell,
id: "password",
height: .heightSingleLineCell
)
]
)
]
}

}
Loading

0 comments on commit 7cf6355

Please sign in to comment.