From daf1e81041b94381c4e344d522ffdaa3612bebd3 Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Tue, 23 Jan 2024 10:21:58 +0000 Subject: [PATCH 1/4] - move collectibles details code into a completion block to avoid race condition - remove an unnecessary extra call to load - add error handling to video player --- .../Detail/CollectibleDetailAVCell.swift | 28 ++++++++++ .../Detail/CollectibleDetailImageCell.swift | 4 +- .../CollectiblesDetailsViewModel.swift | 55 +++++++++---------- 3 files changed, 58 insertions(+), 29 deletions(-) diff --git a/Kukai Mobile/Modules/Collectibles/Cells/Detail/CollectibleDetailAVCell.swift b/Kukai Mobile/Modules/Collectibles/Cells/Detail/CollectibleDetailAVCell.swift index 28fe82b8..ca3128a9 100644 --- a/Kukai Mobile/Modules/Collectibles/Cells/Detail/CollectibleDetailAVCell.swift +++ b/Kukai Mobile/Modules/Collectibles/Cells/Detail/CollectibleDetailAVCell.swift @@ -9,6 +9,7 @@ import UIKit import AVKit import KukaiCoreSwift import MediaPlayer +import OSLog class CollectibleDetailAVCell: UICollectionViewCell { @@ -30,6 +31,7 @@ class CollectibleDetailAVCell: UICollectionViewCell { private var playbackWillKeepUpObserver: NSKeyValueObservation? = nil private var rateObserver: NSKeyValueObservation? = nil + private var errorObserver: NSKeyValueObservation? = nil private var commandCentreTargetStop: Any? = nil private var commandCentreTargetToggle: Any? = nil private var commandCentreTargetPlay: Any? = nil @@ -87,6 +89,29 @@ class CollectibleDetailAVCell: UICollectionViewCell { } }) + self.errorObserver = avplayerController.player?.currentItem?.observe(\.status, changeHandler: { [weak self] item, change in + switch item.status { + case .readyToPlay: + // Handled elsewhere + break + + case .failed: + self?.mediaActivityView.stopAnimating() + self?.mediaActivityView.isHidden = true + Logger.app.error("AVPlayer - Error: \(String(describing: item.error)), Message: \(String(describing: item.error?.localizedDescription))") + + case .unknown: + self?.mediaActivityView.stopAnimating() + self?.mediaActivityView.isHidden = true + Logger.app.error("AVPlayer - unknown: \(String(describing: item.error)), Message: \(String(describing: item.error?.localizedDescription))") + + @unknown default: + self?.mediaActivityView.stopAnimating() + self?.mediaActivityView.isHidden = true + Logger.app.error("AVPlayer - default/unknown: \(String(describing: item.error)), Message: \(String(describing: item.error?.localizedDescription))") + } + }) + // if allowsExternalPlayback set to false, during airplay via the command centre, iOS correctly picks up that its a song and shows the album artwork + title + album // With this setup, videos however do not cast to the external device @@ -137,6 +162,9 @@ class CollectibleDetailAVCell: UICollectionViewCell { rateObserver?.invalidate() rateObserver = nil + errorObserver?.invalidate() + errorObserver = nil + clearCommandCenterCommands() } diff --git a/Kukai Mobile/Modules/Collectibles/Cells/Detail/CollectibleDetailImageCell.swift b/Kukai Mobile/Modules/Collectibles/Cells/Detail/CollectibleDetailImageCell.swift index 8e6838b9..664053bb 100644 --- a/Kukai Mobile/Modules/Collectibles/Cells/Detail/CollectibleDetailImageCell.swift +++ b/Kukai Mobile/Modules/Collectibles/Cells/Detail/CollectibleDetailImageCell.swift @@ -33,7 +33,9 @@ class CollectibleDetailImageCell: UICollectionViewCell { // Load image if not only perfroming collectionview layout logic if !layoutOnly { - MediaProxyService.load(url: mediaContent.mediaURL, to: imageView, withCacheType: .temporary, fallback: UIImage.unknownThumb()) + MediaProxyService.load(url: mediaContent.mediaURL, to: imageView, withCacheType: .temporary, fallback: UIImage.unknownThumb()) { [weak self] _ in + self?.activityIndicator.isHidden = true + } } setup = true diff --git a/Kukai Mobile/Modules/Collectibles/CollectiblesDetailsViewModel.swift b/Kukai Mobile/Modules/Collectibles/CollectiblesDetailsViewModel.swift index 10e34a16..7cb00865 100644 --- a/Kukai Mobile/Modules/Collectibles/CollectiblesDetailsViewModel.swift +++ b/Kukai Mobile/Modules/Collectibles/CollectiblesDetailsViewModel.swift @@ -220,42 +220,41 @@ class CollectiblesDetailsViewModel: ViewModel, UICollectionViewDiffableDataSourc section1Content.append(self.descriptionData) self.currentSnapshot.appendItems(section1Content, toSection: 0) - ds.apply(self.currentSnapshot, animatingDifferences: animate) - self.state = .success(nil) - - - // If unbale to determine contentn type, we need to do a network request to find it - if response.needsMediaTypeVerification { - self.mediaContentForFailedOfflineFetch(forNFT: self.nft) { [weak self] mediaContent in + ds.apply(self.currentSnapshot, animatingDifferences: animate) { + + // If unbale to determine content type, we need to do a network request to find it + if response.needsMediaTypeVerification { + self.mediaContentForFailedOfflineFetch(forNFT: self.nft) { [weak self] mediaContent in + + if let newMediaContent = mediaContent { + self?.replace(existingMediaContent: response.mediaContent, with: newMediaContent) + } else { + // Unbale to determine type and unable to locate URL, or fetch packet from URL. Default to missing image palceholder + let blankMediaContent = MediaContent(isImage: true, isThumbnail: false, mediaURL: nil, mediaURL2: nil, width: 100, height: 100) + self?.replace(existingMediaContent: response.mediaContent, with: blankMediaContent) + } + } + } + + // If we don't have the full image cached, download it and replace the thumbnail with the real thing + else if response.needsToDownloadFullImage { + let newURL = MediaProxyService.url(fromUri: self.nft?.displayURI, ofFormat: MediaProxyService.Format.large.rawFormat()) + let isDualURL = (response.mediaContent.mediaURL2 != nil) - if let newMediaContent = mediaContent { + MediaProxyService.cacheImage(url: newURL) { [weak self] size in + let mediaURL1 = isDualURL ? response.mediaContent.mediaURL : newURL + let mediaURL2 = isDualURL ? newURL : nil + let width = Double(size?.width ?? 300) + let height = Double(size?.height ?? 300) + let newMediaContent = MediaContent(isImage: response.mediaContent.isImage, isThumbnail: false, mediaURL: mediaURL1, mediaURL2: mediaURL2, width: width, height: height) self?.replace(existingMediaContent: response.mediaContent, with: newMediaContent) - } else { - // Unbale to determine type and unable to locate URL, or fetch packet from URL. Default to missing image palceholder - let blankMediaContent = MediaContent(isImage: true, isThumbnail: false, mediaURL: nil, mediaURL2: nil, width: 100, height: 100) - self?.replace(existingMediaContent: response.mediaContent, with: blankMediaContent) } } } - // If we don't have the full image cached, download it and replace the thumbnail with the real thing - else if response.needsToDownloadFullImage { - let newURL = MediaProxyService.url(fromUri: self.nft?.displayURI, ofFormat: MediaProxyService.Format.large.rawFormat()) - let isDualURL = (response.mediaContent.mediaURL2 != nil) - - MediaProxyService.cacheImage(url: newURL) { [weak self] size in - let mediaURL1 = isDualURL ? response.mediaContent.mediaURL : newURL - let mediaURL2 = isDualURL ? newURL : nil - let width = Double(size?.width ?? 300) - let height = Double(size?.height ?? 300) - let newMediaContent = MediaContent(isImage: response.mediaContent.isImage, isThumbnail: false, mediaURL: mediaURL1, mediaURL2: mediaURL2, width: width, height: height) - self?.replace(existingMediaContent: response.mediaContent, with: newMediaContent) - } - } + self.state = .success(nil) } - ds.apply(self.currentSnapshot, animatingDifferences: animate) - // Load remote data after UI let address = DependencyManager.shared.selectedWalletAddress ?? "" From 520ee2f7ec322ca1f7ca4c1d80d5e3b08b822e78 Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Tue, 23 Jan 2024 11:25:07 +0000 Subject: [PATCH 2/4] - update library to support animated webp - update imageViews to better support A-webp - add error message to media cell when unable to play content --- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../Localization/en.lproj/Localizable.strings | 1 + ...llectiblesCollectionItemLargeWithTextCell.swift | 3 ++- .../Cells/Detail/CollectibleDetailAVCell.swift | 1 + .../Cells/Detail/CollectibleDetailImageCell.swift | 3 ++- .../Cells/List/CollectiblesCollectionCell.swift | 1 - .../Cells/List/CollectiblesCollectionCell.xib | 14 +++++++------- .../List/CollectiblesCollectionLargeCell.swift | 3 ++- .../CollectiblesCollectionSinglePageCell.swift | 3 ++- .../Collectibles/Cells/List/SearchResultCell.swift | 3 ++- 10 files changed, 20 insertions(+), 14 deletions(-) diff --git a/Kukai Mobile.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Kukai Mobile.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4bef8cd4..10bdc195 100644 --- a/Kukai Mobile.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Kukai Mobile.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -69,7 +69,7 @@ "location" : "https://github.com/kukai-wallet/kukai-core-swift", "state" : { "branch" : "develop", - "revision" : "6a7d1f937117de5d88b9a936e74c64084a56be2b" + "revision" : "f3205473ddd59a6cfc7107c106e73dd9fe830247" } }, { diff --git a/Kukai Mobile/Localization/en.lproj/Localizable.strings b/Kukai Mobile/Localization/en.lproj/Localizable.strings index 53afb9cf..503b5fd8 100644 --- a/Kukai Mobile/Localization/en.lproj/Localizable.strings +++ b/Kukai Mobile/Localization/en.lproj/Localizable.strings @@ -52,3 +52,4 @@ "error-wc2-unrecoverable"="An unknown error occured with the connection. This operation can't continue. Please check the other application and try agian"; "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"; diff --git a/Kukai Mobile/Modules/Collectibles/Cells/Collection/CollectiblesCollectionItemLargeWithTextCell.swift b/Kukai Mobile/Modules/Collectibles/Cells/Collection/CollectiblesCollectionItemLargeWithTextCell.swift index ee269fa7..878f85c8 100644 --- a/Kukai Mobile/Modules/Collectibles/Cells/Collection/CollectiblesCollectionItemLargeWithTextCell.swift +++ b/Kukai Mobile/Modules/Collectibles/Cells/Collection/CollectiblesCollectionItemLargeWithTextCell.swift @@ -6,10 +6,11 @@ // import UIKit +import SDWebImage class CollectiblesCollectionItemLargeWithTextCell: UICollectionViewCell, UITableViewCellImageDownloading { - @IBOutlet weak var iconView: UIImageView! + @IBOutlet weak var iconView: SDAnimatedImageView! @IBOutlet weak var titleLabel: UILabel! @IBOutlet weak var quantityView: UIView! @IBOutlet weak var quantityLabel: UILabel! diff --git a/Kukai Mobile/Modules/Collectibles/Cells/Detail/CollectibleDetailAVCell.swift b/Kukai Mobile/Modules/Collectibles/Cells/Detail/CollectibleDetailAVCell.swift index ca3128a9..a434e683 100644 --- a/Kukai Mobile/Modules/Collectibles/Cells/Detail/CollectibleDetailAVCell.swift +++ b/Kukai Mobile/Modules/Collectibles/Cells/Detail/CollectibleDetailAVCell.swift @@ -98,6 +98,7 @@ class CollectibleDetailAVCell: UICollectionViewCell { case .failed: self?.mediaActivityView.stopAnimating() self?.mediaActivityView.isHidden = true + self?.parentViewController()?.windowError(withTitle: "error".localized(), description: "error-collectible-media-generic".localized()) Logger.app.error("AVPlayer - Error: \(String(describing: item.error)), Message: \(String(describing: item.error?.localizedDescription))") case .unknown: diff --git a/Kukai Mobile/Modules/Collectibles/Cells/Detail/CollectibleDetailImageCell.swift b/Kukai Mobile/Modules/Collectibles/Cells/Detail/CollectibleDetailImageCell.swift index 664053bb..92364ec4 100644 --- a/Kukai Mobile/Modules/Collectibles/Cells/Detail/CollectibleDetailImageCell.swift +++ b/Kukai Mobile/Modules/Collectibles/Cells/Detail/CollectibleDetailImageCell.swift @@ -7,11 +7,12 @@ import UIKit import KukaiCoreSwift +import SDWebImage class CollectibleDetailImageCell: UICollectionViewCell { @IBOutlet weak var activityIndicator: UIActivityIndicatorView! - @IBOutlet weak var imageView: UIImageView! + @IBOutlet weak var imageView: SDAnimatedImageView! @IBOutlet weak var aspectRatioConstraint: NSLayoutConstraint! public var setup = false diff --git a/Kukai Mobile/Modules/Collectibles/Cells/List/CollectiblesCollectionCell.swift b/Kukai Mobile/Modules/Collectibles/Cells/List/CollectiblesCollectionCell.swift index c306bdc4..ed1e82ef 100644 --- a/Kukai Mobile/Modules/Collectibles/Cells/List/CollectiblesCollectionCell.swift +++ b/Kukai Mobile/Modules/Collectibles/Cells/List/CollectiblesCollectionCell.swift @@ -46,7 +46,6 @@ class CollectiblesCollectionCell: UICollectionViewCell, UITableViewCellImageDown func setupImages(imageURLs: [URL?]) { // Images 1-4 display if urls present - emptyStyle(forImageView: collectionImage1) if imageURLs.count > 0 { MediaProxyService.load(url: imageURLs[0], to: collectionImage1, withCacheType: .temporary, fallback: UIImage.unknownGroup()) { [weak self] imageSize in diff --git a/Kukai Mobile/Modules/Collectibles/Cells/List/CollectiblesCollectionCell.xib b/Kukai Mobile/Modules/Collectibles/Cells/List/CollectiblesCollectionCell.xib index 449253b0..78f6a1ba 100644 --- a/Kukai Mobile/Modules/Collectibles/Cells/List/CollectiblesCollectionCell.xib +++ b/Kukai Mobile/Modules/Collectibles/Cells/List/CollectiblesCollectionCell.xib @@ -1,9 +1,9 @@ - + - + @@ -45,7 +45,7 @@ - + @@ -58,7 +58,7 @@ - + @@ -71,7 +71,7 @@ - + @@ -84,7 +84,7 @@ - + @@ -100,7 +100,7 @@ - + diff --git a/Kukai Mobile/Modules/Collectibles/Cells/List/CollectiblesCollectionLargeCell.swift b/Kukai Mobile/Modules/Collectibles/Cells/List/CollectiblesCollectionLargeCell.swift index bb44c66b..f31f6154 100644 --- a/Kukai Mobile/Modules/Collectibles/Cells/List/CollectiblesCollectionLargeCell.swift +++ b/Kukai Mobile/Modules/Collectibles/Cells/List/CollectiblesCollectionLargeCell.swift @@ -6,10 +6,11 @@ // import UIKit +import SDWebImage class CollectiblesCollectionLargeCell: UICollectionViewCell { - @IBOutlet weak var iconView: UIImageView! + @IBOutlet weak var iconView: SDAnimatedImageView! @IBOutlet weak var titleLabel: UILabel! @IBOutlet var quantityView: UIView! @IBOutlet var quantityLabel: UILabel! diff --git a/Kukai Mobile/Modules/Collectibles/Cells/List/CollectiblesCollectionSinglePageCell.swift b/Kukai Mobile/Modules/Collectibles/Cells/List/CollectiblesCollectionSinglePageCell.swift index e6f5e838..f37329be 100644 --- a/Kukai Mobile/Modules/Collectibles/Cells/List/CollectiblesCollectionSinglePageCell.swift +++ b/Kukai Mobile/Modules/Collectibles/Cells/List/CollectiblesCollectionSinglePageCell.swift @@ -6,10 +6,11 @@ // import UIKit +import SDWebImage class CollectiblesCollectionSinglePageCell: UICollectionViewCell { - @IBOutlet weak var iconView: UIImageView! + @IBOutlet weak var iconView: SDAnimatedImageView! @IBOutlet weak var titleLabel: UILabel! @IBOutlet weak var subTitleLabel: UILabel! @IBOutlet weak var buttonView: UIView! diff --git a/Kukai Mobile/Modules/Collectibles/Cells/List/SearchResultCell.swift b/Kukai Mobile/Modules/Collectibles/Cells/List/SearchResultCell.swift index e4547385..4a6df828 100644 --- a/Kukai Mobile/Modules/Collectibles/Cells/List/SearchResultCell.swift +++ b/Kukai Mobile/Modules/Collectibles/Cells/List/SearchResultCell.swift @@ -6,10 +6,11 @@ // import UIKit +import SDWebImage class SearchResultCell: UICollectionViewCell { - @IBOutlet weak var iconView: UIImageView! + @IBOutlet weak var iconView: SDAnimatedImageView! @IBOutlet weak var titleLabel: UILabel! @IBOutlet weak var quantityView: UIView! @IBOutlet weak var quantityLabel: UILabel! From 8feddfd47851f0e4e11911aa7a76d41cf2d06933 Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Tue, 23 Jan 2024 14:36:39 +0000 Subject: [PATCH 3/4] - add onramp viewmodel - update onramp screen to rely on model - add logic to generate urls for each service --- Kukai Mobile.xcodeproj/project.pbxproj | 4 + .../Localization/en.lproj/Localizable.strings | 1 + .../Side Menu/OnrampViewController.swift | 76 +++++---- .../Modules/Side Menu/OnrampViewModel.swift | 158 ++++++++++++++++++ 4 files changed, 210 insertions(+), 29 deletions(-) create mode 100644 Kukai Mobile/Modules/Side Menu/OnrampViewModel.swift diff --git a/Kukai Mobile.xcodeproj/project.pbxproj b/Kukai Mobile.xcodeproj/project.pbxproj index a88b25c5..995ee4b6 100644 --- a/Kukai Mobile.xcodeproj/project.pbxproj +++ b/Kukai Mobile.xcodeproj/project.pbxproj @@ -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 */; }; @@ -425,6 +426,7 @@ C003AEB32B470D6E00AEC4A8 /* SendAbstractConfirmViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendAbstractConfirmViewController.swift; sourceTree = ""; }; C003CECA27FDE9C900F64B4C /* CloudKitService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitService.swift; sourceTree = ""; }; C003CECC27FDED5D00F64B4C /* CKRecord+extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CKRecord+extensions.swift"; sourceTree = ""; }; + C005B6D12B5FDEE3006E265F /* OnrampViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnrampViewModel.swift; sourceTree = ""; }; C005BA312822D3DC00D8369D /* AddWalletViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddWalletViewController.swift; sourceTree = ""; }; C006176929DB33C8005A1330 /* WalletCreatedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletCreatedViewController.swift; sourceTree = ""; }; C0081FD527D8FE2300F7FEFF /* ActivityViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityViewController.swift; sourceTree = ""; }; @@ -1471,6 +1473,7 @@ C06E0D60287C3E28007A580B /* WalletConnectViewModel.swift */, C055EF1D2A2F95310031CB5F /* ShowQRViewController.swift */, C0172A072A98EC6400163179 /* OnrampViewController.swift */, + C005B6D12B5FDEE3006E265F /* OnrampViewModel.swift */, ); path = "Side Menu"; sourceTree = ""; @@ -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 */, diff --git a/Kukai Mobile/Localization/en.lproj/Localizable.strings b/Kukai Mobile/Localization/en.lproj/Localizable.strings index 503b5fd8..ddb26d03 100644 --- a/Kukai Mobile/Localization/en.lproj/Localizable.strings +++ b/Kukai Mobile/Localization/en.lproj/Localizable.strings @@ -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"; diff --git a/Kukai Mobile/Modules/Side Menu/OnrampViewController.swift b/Kukai Mobile/Modules/Side Menu/OnrampViewController.swift index b5c543e2..3f8ccb75 100644 --- a/Kukai Mobile/Modules/Side Menu/OnrampViewController.swift +++ b/Kukai Mobile/Modules/Side Menu/OnrampViewController.swift @@ -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) { @@ -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) + } + } } } diff --git a/Kukai Mobile/Modules/Side Menu/OnrampViewModel.swift b/Kukai Mobile/Modules/Side Menu/OnrampViewModel.swift new file mode 100644 index 00000000..e6ae32bc --- /dev/null +++ b/Kukai Mobile/Modules/Side Menu/OnrampViewModel.swift @@ -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? = 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() + 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) -> 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) -> 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) -> 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) -> 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¤cyCode=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())) + } + } + } +} From 60056022b4b69c5bdaa419fd227a2e958e562874 Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Tue, 23 Jan 2024 15:24:47 +0000 Subject: [PATCH 4/4] - switch to using email validator - update library to fix issue with torus getAddress --- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 2 +- .../EnterAddressComponent/EnterAddressComponent.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Kukai Mobile.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Kukai Mobile.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 10bdc195..7a5490d4 100644 --- a/Kukai Mobile.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Kukai Mobile.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -69,7 +69,7 @@ "location" : "https://github.com/kukai-wallet/kukai-core-swift", "state" : { "branch" : "develop", - "revision" : "f3205473ddd59a6cfc7107c106e73dd9fe830247" + "revision" : "30b765ac3c40815d01c726a9aeed8c583d4dc073" } }, { diff --git a/Kukai Mobile/Controls/EnterAddressComponent/EnterAddressComponent.swift b/Kukai Mobile/Controls/EnterAddressComponent/EnterAddressComponent.swift index 46097997..d82ae6a9 100644 --- a/Kukai Mobile/Controls/EnterAddressComponent/EnterAddressComponent.swift +++ b/Kukai Mobile/Controls/EnterAddressComponent/EnterAddressComponent.swift @@ -312,7 +312,7 @@ extension EnterAddressComponent: AddressTypeDelegate { sendToIcon.image = AddressTypeViewController.imageFor(addressType: type) addressTypeButton.setTitle("Google", for: .normal) textField.placeholder = "Enter Google Account" - textField.validator = GmailValidator() + textField.validator = EmailValidator() case .reddit: sendToIcon.image = AddressTypeViewController.imageFor(addressType: type) @@ -330,7 +330,7 @@ extension EnterAddressComponent: AddressTypeDelegate { sendToIcon.image = AddressTypeViewController.imageFor(addressType: type) addressTypeButton.setTitle("Email", for: .normal) textField.placeholder = "Enter email address" - textField.validator = NoWhiteSpaceStringValidator() + textField.validator = EmailValidator() } if !textField.revalidateTextfield() {