Skip to content

Commit

Permalink
- add onramp viewmodel
Browse files Browse the repository at this point in the history
- update onramp screen to rely on model
- add logic to generate urls for each service
  • Loading branch information
simonmcl committed Jan 23, 2024
1 parent 520ee2f commit 8feddfd
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 29 deletions.
4 changes: 4 additions & 0 deletions Kukai Mobile.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
C003AEB42B470D6E00AEC4A8 /* SendAbstractConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C003AEB32B470D6E00AEC4A8 /* SendAbstractConfirmViewController.swift */; };
C003CECB27FDE9C900F64B4C /* CloudKitService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C003CECA27FDE9C900F64B4C /* CloudKitService.swift */; };
C003CECD27FDED5D00F64B4C /* CKRecord+extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C003CECC27FDED5D00F64B4C /* CKRecord+extensions.swift */; };
C005B6D22B5FDEE3006E265F /* OnrampViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005B6D12B5FDEE3006E265F /* OnrampViewModel.swift */; };
C005BA322822D3DC00D8369D /* AddWalletViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C005BA312822D3DC00D8369D /* AddWalletViewController.swift */; };
C006176A29DB33C8005A1330 /* WalletCreatedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C006176929DB33C8005A1330 /* WalletCreatedViewController.swift */; };
C0081FD627D8FE2300F7FEFF /* ActivityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0081FD527D8FE2300F7FEFF /* ActivityViewController.swift */; };
Expand Down Expand Up @@ -425,6 +426,7 @@
C003AEB32B470D6E00AEC4A8 /* SendAbstractConfirmViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendAbstractConfirmViewController.swift; sourceTree = "<group>"; };
C003CECA27FDE9C900F64B4C /* CloudKitService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitService.swift; sourceTree = "<group>"; };
C003CECC27FDED5D00F64B4C /* CKRecord+extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CKRecord+extensions.swift"; sourceTree = "<group>"; };
C005B6D12B5FDEE3006E265F /* OnrampViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnrampViewModel.swift; sourceTree = "<group>"; };
C005BA312822D3DC00D8369D /* AddWalletViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddWalletViewController.swift; sourceTree = "<group>"; };
C006176929DB33C8005A1330 /* WalletCreatedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletCreatedViewController.swift; sourceTree = "<group>"; };
C0081FD527D8FE2300F7FEFF /* ActivityViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityViewController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1471,6 +1473,7 @@
C06E0D60287C3E28007A580B /* WalletConnectViewModel.swift */,
C055EF1D2A2F95310031CB5F /* ShowQRViewController.swift */,
C0172A072A98EC6400163179 /* OnrampViewController.swift */,
C005B6D12B5FDEE3006E265F /* OnrampViewModel.swift */,
);
path = "Side Menu";
sourceTree = "<group>";
Expand Down Expand Up @@ -1923,6 +1926,7 @@
C0FBC88F292C12CE00B29921 /* HiddenTokensBalancesViewController.swift in Sources */,
C001D5B22A027F410089EC7A /* CollectionDetailsViewModel.swift in Sources */,
C0AC1D3E2771E5AB002F66C0 /* BottomSheetLargeSegue.swift in Sources */,
C005B6D22B5FDEE3006E265F /* OnrampViewModel.swift in Sources */,
C07FC40927C538A90056FA47 /* ImageHeadingCell.swift in Sources */,
C05B0A332A03FAC9005AA803 /* CollectiblesCollectionHeaderMediumCell.swift in Sources */,
C065AF0F2AB09C270061CC64 /* SideMenuBackupViewController.swift in Sources */,
Expand Down
1 change: 1 addition & 0 deletions Kukai Mobile/Localization/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,4 @@
"error-wc2-cant-continue"="Unable to continue with this request due to system error";
"error-beacon-not-supported"="Beacon QRCodes are not supported, only Wallet Connect. Please make sure you are using the kukai option. If you are, please contact the dApp support team and ask them to update their beacon version";
"error-collectible-media-generic"="Unable to play media at this time, please try again later";
"error-onramp-generic"="Unable to access this provider at this time, please try again later";
76 changes: 47 additions & 29 deletions Kukai Mobile/Modules/Side Menu/OnrampViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,48 +6,51 @@
//

import UIKit
import Combine
import SafariServices

class OnrampViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
class OnrampViewController: UIViewController, UITableViewDelegate {

@IBOutlet weak var tableView: UITableView!

private let ramps = [
(title: "Coinbase", subtitle: "Transfer from Coinbase", image: "coinbase"),
(title: "Transak", subtitle: "Bank transfers & local payment methods in 120+ countries", image: "transak"),
(title: "Moonpay", subtitle: "Cards & banks transfers", image: "moonpay")
]
public let viewModel = OnrampViewModel()

private var bag = [AnyCancellable]()
private var safariViewController: SFSafariViewController? = nil

override func viewDidLoad() {
super.viewDidLoad()
let _ = self.view.addGradientBackgroundFull()

tableView.dataSource = self
// Setup data
viewModel.makeDataSource(withTableView: tableView)
tableView.dataSource = viewModel.dataSource
tableView.delegate = self

viewModel.$state.sink { [weak self] state in
guard let self = self else { return }

switch state {
case .loading:
let _ = ""

case .success(_):
let _ = ""

case .failure(_, let message):
self.windowError(withTitle: "error".localized(), description: message)
}
}.store(in: &bag)
}

@IBAction func infoButtonTapped(_ sender: Any) {
self.alert(errorWithMessage: "Under Construction")
}

func numberOfSections(in tableView: UITableView) -> Int {
return 1
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return ramps.count
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

viewModel.refresh(animate: true)
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "TitleSubtitleImageContainerCell", for: indexPath) as? TitleSubtitleImageContainerCell else {
return UITableViewCell()
}

let ramp = ramps[indexPath.row]
cell.iconView.image = UIImage(named: ramp.image)
cell.titleLabel.text = ramp.title
cell.subtitleLabel.text = ramp.subtitle

return cell
@IBAction func infoButtonTapped(_ sender: Any) {
self.alert(errorWithMessage: "Under Construction")
}

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
Expand All @@ -59,6 +62,21 @@ class OnrampViewController: UIViewController, UITableViewDataSource, UITableView
}

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.alert(errorWithMessage: "Under Construction")
self.showLoadingView()

viewModel.url(forIndexPath: indexPath) { [weak self] result in
self?.hideLoadingView()
guard let res = try? result.get() else {
self?.windowError(withTitle: "error".localized(), description: "error-onramp-generic".localized())
return
}

self?.safariViewController = SFSafariViewController(url: res)

if let vc = self?.safariViewController {
vc.modalPresentationStyle = .pageSheet
self?.present(vc, animated: true)
}
}
}
}
158 changes: 158 additions & 0 deletions Kukai Mobile/Modules/Side Menu/OnrampViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
//
// OnrampViewModel.swift
// Kukai Mobile
//
// Created by Simon Mcloughlin on 23/01/2024.
//

import UIKit
import KukaiCoreSwift

public struct OnrampOption: Codable, Hashable {
let title: String
let subtitle: String
let imageName: String
let key: String
}

class OnrampViewModel: ViewModel, UITableViewDiffableDataSourceHandler {

typealias SectionEnum = Int
typealias CellDataType = AnyHashable

var dataSource: UITableViewDiffableDataSource<Int, AnyHashable>? = nil
var ramps: [OnrampOption] = []

func makeDataSource(withTableView tableView: UITableView) {

dataSource = UITableViewDiffableDataSource(tableView: tableView, cellProvider: { tableView, indexPath, item in

if let obj = item as? OnrampOption, let cell = tableView.dequeueReusableCell(withIdentifier: "TitleSubtitleImageContainerCell", for: indexPath) as? TitleSubtitleImageContainerCell {
cell.iconView.image = UIImage(named: obj.imageName)
cell.titleLabel.text = obj.title
cell.subtitleLabel.text = obj.subtitle
return cell

} else {
return UITableViewCell()
}
})

dataSource?.defaultRowAnimation = .fade
}

func refresh(animate: Bool, successMessage: String? = nil) {
guard let ds = dataSource else {
state = .failure(KukaiError.internalApplicationError(error: ViewModelError.dataSourceNotCreated), "Unable to process data at this time")
return
}

// Build snapshot
var snapshot = NSDiffableDataSourceSnapshot<Int, AnyHashable>()
snapshot.appendSections([0])

snapshot.appendItems([
OnrampOption(title: "Coinbase", subtitle: "Transfer from Coinbase", imageName: "coinbase", key: "coinbase"),
OnrampOption(title: "Transak", subtitle: "Bank transfers & local payment methods in 120+ countries", imageName: "transak", key: "transak"),
OnrampOption(title: "Moonpay", subtitle: "Cards & banks transfers", imageName: "moonpay", key: "moonpay")
], toSection: 0)

ds.applySnapshotUsingReloadData(snapshot)

self.state = .success(nil)
}

public func url(forIndexPath indexPath: IndexPath, completion: @escaping ((Result<URL, KukaiError>) -> Void)) {
guard let ramp = dataSource?.itemIdentifier(for: indexPath) as? OnrampOption, let currentAddress = DependencyManager.shared.selectedWalletAddress else {
completion(Result.failure(KukaiError.unknown()))
return
}

var baseURLString = ""
switch ramp.key {
case "coinbase":
baseURLString = "https://pay.coinbase.com"
buildCoinbaseURL(withBaseURL: baseURLString, andAddress: currentAddress, completion: completion)

case "transak":
baseURLString = "https://global.transak.com"
buildTransakURL(withBaseURL: baseURLString, andAddress: currentAddress, completion: completion)

case "moonpay":
baseURLString = "https://buy.moonpay.com"
signMoonPayUrl(withBaseURL: baseURLString, andAddress: currentAddress, completion: completion)

default:
completion(Result.failure(KukaiError.unknown()))
}

}

private func buildCoinbaseURL(withBaseURL: String, andAddress address: String, completion: @escaping ((Result<URL, KukaiError>) -> Void)) {
let appID = "aa41d510-15f9-4426-87bd-3a506b6e22c0"
let walletData: [[String: Any]] = [
[
"address": address,
"blockchains": [ "tezos" ]
]
]

guard let jsonData = try? JSONSerialization.data(withJSONObject: walletData),
let jsonString = String(data: jsonData, encoding: .utf8),
let url = URL(string: "\(withBaseURL)/buy/select-asset?appId=\(appID)&destinationWallets=\(jsonString)") else {
completion(Result.failure(KukaiError.unknown()))
return
}

completion(Result.success(url))
}

private func buildTransakURL(withBaseURL: String, andAddress address: String, completion: @escaping ((Result<URL, KukaiError>) -> Void)) {
let apiKey = "f1336570-699b-4181-9bd1-cdd57206981f" // "3b0e81f3-37dc-41f3-9837-bd8d2c350313" : "f1336570-699b-4181-9bd1-cdd57206981f"
let walletData: [String: Any] = [
"coins": [
"XTZ": [ address ]
]
]

guard let jsonData = try? JSONSerialization.data(withJSONObject: walletData),
let jsonString = String(data: jsonData, encoding: .utf8),
let url = URL(string: "\(withBaseURL)?apiKey=\(apiKey)&cryptoCurrencyCode=XTZ&walletAddressesData=\(jsonString)&disableWalletAddressForm=true") else {
completion(Result.failure(KukaiError.unknown()))
return
}

completion(Result.success(url))
}

private func signMoonPayUrl(withBaseURL: String, andAddress address: String, completion: @escaping ((Result<URL, KukaiError>) -> Void)) {
guard address.prefix(2).lowercased() == "tz", let kukaiServiceURL = URL(string: "https://utils.kukai.network/moonpay/sign") else {
completion(Result.failure(KukaiError.unknown()))
return
}

let key = "pk_live_rP9HlBRO54nY4QKLxc6ONl4Prrm6vymK" // "pk_test_M23P0Zc5SvBORSFV63sfWKi7n5QbGZR" : "pk_live_rP9HlBRO54nY4QKLxc6ONl4Prrm6vymK"
var query = "?apiKey=\(key)&colorCode=%237178E3&currencyCode=xtz&walletAddress=\(address)"

let params: [String: Any] = [
"dev": false,
"url": query
]

let data = try? JSONSerialization.data(withJSONObject: params)
DependencyManager.shared.tezosNodeClient.networkService.request(url: kukaiServiceURL, isPOST: true, withBody: data, forReturnType: Data.self) { result in
guard let res = try? result.get(), let sigString = String(data: res, encoding: .utf8), let sanitised = sigString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
completion(Result.failure(result.getFailure()))
return
}

query += "&signature=\(sanitised))"

if let url = URL(string: "\(withBaseURL)\(query)") {
completion(Result.success(url))
} else {
completion(Result.failure(KukaiError.unknown()))
}
}
}
}

0 comments on commit 8feddfd

Please sign in to comment.