-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
662a919
commit 2c073ab
Showing
6 changed files
with
591 additions
and
536 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
106 changes: 106 additions & 0 deletions
106
WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/ImageDownloader.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
// | ||
// Copyright 2024 Wultra s.r.o. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions | ||
// and limitations under the License. | ||
// | ||
|
||
import UIKit | ||
|
||
/// Simple image URL downloader with a simple cache implementation | ||
internal class ImageDownloader { | ||
|
||
public static let shared = ImageDownloader() | ||
|
||
public class Callback { | ||
|
||
fileprivate let callback: (UIImage?) -> Void | ||
fileprivate(set) var canceled = false | ||
|
||
public init(callback: @escaping (UIImage?) -> Void) { | ||
self.callback = callback | ||
} | ||
|
||
public func cancel() { | ||
canceled = true | ||
} | ||
|
||
fileprivate func setResult(_ image: UIImage?) { | ||
guard canceled == false else { | ||
return | ||
} | ||
callback(image) | ||
} | ||
} | ||
|
||
private var cache: NSCache<NSString, UIImage> | ||
|
||
private var waitingList = [URL: [Callback]]() | ||
private let lock = WMTLock() | ||
|
||
public init(byteCacheSize: Int = 20_000_000) { // ~20 mb | ||
cache = NSCache() | ||
cache.totalCostLimit = byteCacheSize | ||
} | ||
|
||
/// Downloads image for given URL | ||
/// - Parameters: | ||
/// - url: URL where the image is | ||
/// - allowCache: If the image can be cached or loaded from cache | ||
/// - delayError: Should error be delayed? For example, when the URL does not exist (404), it will fail in almost instant and it's better | ||
/// for the UI to "simulate communication". | ||
/// - completion: Completion with nil on error. Always invoked on main thread | ||
public func downloadImage(at url: URL, allowCache: Bool = true, delayError: Bool = true, _ callback: Callback) { | ||
|
||
if allowCache, let cached = cache.object(forKey: NSString(string: url.absoluteString)) { | ||
callback.setResult(cached) | ||
return | ||
} | ||
|
||
lock.synchronized { | ||
if var list = waitingList[url] { | ||
list.append(callback) | ||
waitingList[url] = list | ||
} else { | ||
waitingList[url] = [callback] | ||
} | ||
} | ||
|
||
DispatchQueue.global().async { [weak self] in | ||
|
||
let started = Date() | ||
let data = try? Data(contentsOf: url) | ||
let elapsed = Date().timeIntervalSince(started) | ||
let delay = delayError && data == nil && elapsed < 0.8 | ||
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + (delay ? 0.7 : 0) ) { | ||
|
||
guard let self else { | ||
return | ||
} | ||
|
||
self.lock.synchronized { | ||
if let data, let image = UIImage(data: data) { | ||
if allowCache { | ||
self.cache.setObject(image, forKey: NSString(string: url.absoluteString), cost: data.count) | ||
} | ||
self.waitingList[url]?.forEach { $0.setResult(image) } | ||
} else { | ||
self.waitingList[url]?.forEach { $0.setResult(nil) } | ||
} | ||
|
||
self.waitingList.removeValue(forKey: url) | ||
} | ||
} | ||
} | ||
} | ||
} |
204 changes: 204 additions & 0 deletions
204
...leTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationListVisual.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,204 @@ | ||
// | ||
// Copyright 2024 Wultra s.r.o. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions | ||
// and limitations under the License. | ||
// | ||
|
||
import UIKit | ||
|
||
public struct WMTUserOperationListVisual { | ||
public let header: String? | ||
public let title: String? | ||
public let message: String? | ||
public let style: String? | ||
public let thumbnailImageURL: URL? | ||
public let template: WMTTemplates.ListTemplate? | ||
|
||
private let downloader = ImageDownloader.shared | ||
|
||
public init( | ||
header: String? = nil, | ||
title: String? = nil, | ||
message: String? = nil, | ||
style: String? = nil, | ||
thumbnailImageURL: URL? = nil, | ||
template: WMTTemplates.ListTemplate? = nil | ||
) { | ||
self.header = header | ||
self.title = title | ||
self.message = message | ||
self.style = style | ||
self.thumbnailImageURL = thumbnailImageURL | ||
self.template = template | ||
} | ||
|
||
public func downloadThumbnail(callback: @escaping (UIImage?) -> Void) { | ||
|
||
guard let url = thumbnailImageURL else { | ||
callback(nil) | ||
return | ||
} | ||
|
||
downloader.downloadImage( | ||
at: url, | ||
ImageDownloader.Callback { img in | ||
if let img { | ||
callback(img) | ||
} else { | ||
callAgain(callback: callback) | ||
} | ||
} | ||
) | ||
} | ||
|
||
public func callAgain(callback: @escaping (UIImage?) -> Void) { | ||
DispatchQueue.global().asyncAfter(deadline: .now() + 1) { | ||
self.downloadThumbnail(callback: callback) | ||
} | ||
} | ||
} | ||
|
||
// MARK: WMTUserOperation List Visual preparation extension | ||
extension WMTUserOperation { | ||
|
||
internal func prepareVisualListDetail() -> WMTUserOperationListVisual { | ||
let listTemplate = self.ui?.templates?.list | ||
let attributes = self.formData.attributes | ||
let headerAtrr = listTemplate?.header?.replacePlaceholders(from: attributes) | ||
|
||
var title: String? { | ||
if let titleAttr = listTemplate?.title?.replacePlaceholders(from: attributes) { | ||
return titleAttr | ||
} | ||
|
||
if !self.formData.message.isEmpty { | ||
return self.formData.title | ||
} | ||
|
||
return nil | ||
} | ||
|
||
var message: String? { | ||
if let messageAttr = listTemplate?.message?.replacePlaceholders(from: attributes) { | ||
return messageAttr | ||
} | ||
|
||
if !self.formData.message.isEmpty { | ||
return self.formData.message | ||
} | ||
|
||
return nil | ||
} | ||
|
||
var imageUrl: URL? { | ||
if let imgAttr = listTemplate?.image, | ||
let imgAttrCell = self.formData.attributes | ||
.compactMap({ $0 as? WMTOperationAttributeImage }) | ||
.first(where: { $0.label.id == imgAttr }) { | ||
return URL(string: imgAttrCell.thumbnailUrl) | ||
} | ||
|
||
if let imgAttrCell = self.formData.attributes | ||
.compactMap({ $0 as? WMTOperationAttributeImage }) | ||
.first { | ||
return URL(string: imgAttrCell.thumbnailUrl) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
return WMTUserOperationListVisual( | ||
header: headerAtrr, | ||
title: title, | ||
message: message, | ||
style: self.ui?.templates?.list?.style, | ||
thumbnailImageURL: imageUrl, | ||
template: listTemplate | ||
) | ||
} | ||
} | ||
|
||
// MARK: Helpers | ||
|
||
internal extension String { | ||
|
||
// Function to replace placeholders in the template with actual values | ||
func replacePlaceholders(from attributes: [WMTOperationAttribute]) -> String? { | ||
var result = self | ||
|
||
if let placeholders = extractPlaceholders() { | ||
for placeholder in placeholders { | ||
if let value = findAttributeValue(for: placeholder, from: attributes) { | ||
result = result.replacingOccurrences(of: "${\(placeholder)}", with: value) | ||
} else { | ||
D.debug("Placeholder Attribute: \(placeholder) in WMTUserAttributes not found.") | ||
return nil | ||
} | ||
} | ||
} | ||
return result | ||
} | ||
|
||
private func extractPlaceholders() -> [String]? { | ||
do { | ||
let regex = try NSRegularExpression(pattern: "\\$\\{(.*?)\\}", options: []) | ||
let matches = regex.matches(in: self, options: [], range: NSRange(location: 0, length: self.count)) | ||
|
||
var attributeIds: [String] = [] | ||
for match in matches { | ||
if let range = Range(match.range(at: 1), in: self) { | ||
let key = String(self[range]) | ||
attributeIds.append(key) | ||
} | ||
} | ||
return attributeIds | ||
} catch { | ||
D.warning("Error creating NSRegularExpression: \(error) in WMTListParser.") | ||
return nil | ||
} | ||
} | ||
|
||
private func findAttributeValue(for attributeId: String, from attributes: [WMTOperationAttribute]) -> String? { | ||
for attribute in attributes where attribute.label.id == attributeId { | ||
switch attribute.type { | ||
case .amount: | ||
guard let attr = attribute as? WMTOperationAttributeAmount else { return nil } | ||
return attr.valueFormatted ?? "\(attr.amountFormatted) \(attr.currencyFormatted)" | ||
|
||
case .amountConversion: | ||
guard let attr = attribute as? WMTOperationAttributeAmountConversion else { return nil } | ||
if let sourceValue = attr.source.valueFormatted, | ||
let targetValue = attr.target.valueFormatted { | ||
return "\(sourceValue) → \(targetValue)" | ||
} else { | ||
let source = "\(attr.source.amountFormatted) \(attr.source.currencyFormatted)" | ||
let target = "\(attr.target.amountFormatted) \(attr.target.currencyFormatted)" | ||
return "\(source) → \(target)" | ||
} | ||
|
||
case .keyValue: | ||
guard let attr = attribute as? WMTOperationAttributeKeyValue else { return nil } | ||
return attr.value | ||
case .note: | ||
guard let attr = attribute as? WMTOperationAttributeNote else { return nil } | ||
return attr.note | ||
case .heading: | ||
guard let attr = attribute as? WMTOperationAttributeHeading else { return nil } | ||
return attr.label.value | ||
case .partyInfo, .image, .unknown: | ||
return nil | ||
} | ||
} | ||
return nil | ||
} | ||
} |
Oops, something went wrong.