diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/Package.resolved b/Package.resolved
new file mode 100644
index 0000000..1550cab
--- /dev/null
+++ b/Package.resolved
@@ -0,0 +1,23 @@
+{
+ "pins" : [
+ {
+ "identity" : "networking-apple",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/wultra/networking-apple.git",
+ "state" : {
+ "revision" : "3157cd4c5bc93f504b624b3438302eb053b30bf6",
+ "version" : "1.3.2"
+ }
+ },
+ {
+ "identity" : "powerauth-mobile-sdk-spm",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/wultra/powerauth-mobile-sdk-spm.git",
+ "state" : {
+ "revision" : "d86feec12ccfbc766f2307fc8a292791be13bbfa",
+ "version" : "1.8.1"
+ }
+ }
+ ],
+ "version" : 2
+}
diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift
new file mode 100644
index 0000000..a08f5d7
--- /dev/null
+++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift
@@ -0,0 +1,408 @@
+//
+// 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
+
+class WMTUserOperationVisualParser {
+ static func prepareDetail(operation: WMTUserOperation) -> WMTUserOperationVisual? {
+ return operation.prepareVisualDetail()
+ }
+
+ static func prepareList(operation: WMTUserOperation) -> WMTUserOperationListVisual? {
+ return operation.prepareVisualListDetail()
+ }
+}
+
+// MARK: WMTUserOperation Detail Visual preparation extension
+extension WMTUserOperation {
+ func prepareVisualDetail() -> WMTUserOperationVisual? {
+ return WMTUserOperationVisual(sections: [])
+ }
+}
+
+// MARK: WMTUserOperation List Visual preparation extension
+extension WMTUserOperation {
+
+ func prepareVisualListDetail() -> WMTUserOperationListVisual? {
+ guard let listTemplate = self.ui?.templates?.list else {
+ return nil
+ }
+ let attributes = self.formData.attributes
+
+ let headerAtrr = listTemplate.header?.replacePlaceholders(from: attributes)
+
+ var title: String? = nil
+ if let titleAttr = listTemplate.title?.replacePlaceholders(from: attributes) {
+ title = titleAttr
+ } else if !self.formData.message.isEmpty {
+ title = self.formData.title
+ }
+
+ var message: String? = nil
+ if let messageAttr = listTemplate.message?.replacePlaceholders(from: attributes) {
+ message = messageAttr
+ } else if !self.formData.message.isEmpty {
+ message = self.formData.message
+ }
+
+ var imageUrl: URL? = nil
+ if let imgAttr = listTemplate.image, let imgAttrCell = self.formData.attributes.first(where: { $0.label.id == imgAttr }) as? WMTOperationAttributeImage {
+ let imageUrl = URL(string: imgAttrCell.thumbnailUrl)
+ }
+
+ return WMTUserOperationListVisual(
+ header: headerAtrr,
+ title: title,
+ message: message,
+ style: self.ui?.templates?.list?.style,
+ thumbnailImage: imageUrl,
+ template: listTemplate
+ )
+ }
+}
+
+
+struct WMTUserOperationListVisual {
+ let header: String?
+ let title: String?
+ let message: String?
+ let style: String?
+ let thumbnailImage: URL?
+
+ let template: WMTTemplates.ListTemplate
+}
+
+extension WMTUserOperation {
+
+ func provideData() -> WMTUserOperationVisual? {
+
+ guard let detailTemplate = self.ui?.templates?.detail else {
+ var attrs = self.formData.attributes
+ if attrs.isEmpty {
+ return WMTUserOperationVisual(sections: [createHeaderVisual()])
+ } else {
+ let headerSection = createHeaderVisual()
+ let dataSections: WMTUserOperationVisualSection = .init(cells: attrs.getRemainingCells())
+
+ return WMTUserOperationVisual(sections: [headerSection, dataSections])
+ }
+ }
+
+ return createTemplateRichData(from: detailTemplate)
+ }
+
+ // Default header
+ func createHeaderVisual(style: String? = nil) -> WMTUserOperationVisualSection {
+ let defaultHeaderCell = WMTUserOperationHeaderVisualCell(value: self.formData.title)
+ let defaultMessageCell = WMTUserOperationMessageVisualCell(value: self.formData.message)
+
+ return WMTUserOperationVisualSection(
+ style: style,
+ title: nil,
+ cells: [defaultHeaderCell, defaultMessageCell]
+ )
+ }
+
+ func createTemplateRichData(from detailTemplate: WMTTemplates.DetailTemplate) -> WMTUserOperationVisual {
+ var attrs = self.formData.attributes
+
+ guard let sectionsTemplate = detailTemplate.sections else {
+ // Sections not specified, but style might be
+ let headerSection = createHeaderVisual(style: detailTemplate.style)
+ let dataSections: WMTUserOperationVisualSection = .init(cells: attrs.getRemainingCells())
+
+ return WMTUserOperationVisual(sections: [headerSection, dataSections])
+ }
+
+ var sections = [WMTUserOperationVisualSection]()
+
+ if detailTemplate.showTitleAndMessage == true {
+ let headerSection = createHeaderVisual(style: detailTemplate.style)
+ let dataSection = attrs.popSections(from: sectionsTemplate)
+ sections.append(headerSection)
+ sections.append(contentsOf: dataSection)
+ sections.append(.init(cells: attrs.getRemainingCells()))
+ return .init(sections: sections)
+
+ } else {
+ let dataSections = attrs.popSections(from: sectionsTemplate)
+ sections.append(contentsOf: dataSections)
+ sections.append(.init(cells: attrs.getRemainingCells()))
+ return .init(sections: sections)
+ }
+ }
+}
+
+public struct WMTUserOperationVisual {
+ let sections: [WMTUserOperationVisualSection]
+}
+
+public struct WMTUserOperationVisualSection {
+ let style: String?
+ let title: String? // not an id, actual value
+ let cells: [WMTUserOperationVisualCell]
+
+ init(style: String? = nil, title: String? = nil, cells: [WMTUserOperationVisualCell]) {
+ self.style = style
+ self.title = title
+ self.cells = cells
+ }
+}
+
+public protocol WMTUserOperationVisualCell { }
+
+public struct WMTUserOperationHeaderVisualCell: WMTUserOperationVisualCell {
+ let value: String
+}
+
+public struct WMTUserOperationMessageVisualCell: WMTUserOperationVisualCell {
+ let value: String
+}
+
+public struct WMTUserOperationStringValueAttributeVisualCell: WMTUserOperationVisualCell {
+ let header: String
+ let defaultFormattedStringValue: String
+ let attribute: WMTOperationAttribute
+ let cellTemplate: WMTTemplates.DetailTemplate.Section.Cell?
+}
+
+public struct WMTUserOperationImageVisualCell: WMTUserOperationVisualCell {
+ let urlThumbnail: URL
+ let urlFull: URL?
+ let attribute: WMTOperationAttributeImage
+ let cellTemplate: WMTTemplates.DetailTemplate.Section.Cell?
+}
+
+extension WMTUserOperationImageVisualCell {
+ func downloadFull(callback: (Result) -> Void) {
+ // ImageDownloader.shared. ....
+ }
+ func downloadThumbnail(callback: (Result) -> Void) {
+ // ImageDownloader.shared. ....
+ }
+}
+
+
+// MARK: Helpers
+
+private 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.print("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:
+ let attr = attribute as! WMTOperationAttributeAmount
+ return attr.valueFormatted ?? "\(attr.amountFormatted) \(attr.currencyFormatted)"
+
+ case .amountConversion:
+ let attr = attribute as! WMTOperationAttributeAmountConversion
+ 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:
+ return (attribute as! WMTOperationAttributeKeyValue).value
+ case .note:
+ return (attribute as! WMTOperationAttributeNote).note
+ case .heading:
+ return (attribute as! WMTOperationAttributeHeading).label.value
+ case .partyInfo, .image, .unknown:
+ return nil
+ }
+ }
+ return nil
+ }
+}
+
+
+private extension Array where Element: WMTOperationAttribute {
+
+ mutating func pop(id: String?) -> T? {
+ guard let id = id else {
+ return nil
+ }
+ return pop(id: id)
+ }
+
+ mutating func pop(id: String) -> T? {
+ guard let index = firstIndex(where: { $0.label.id == id }) else {
+ return nil
+ }
+ guard let attr = self[index] as? T else {
+ return nil
+ }
+ remove(at: index)
+ return attr
+ }
+
+ mutating func pop(ids: [String]) -> [WMTOperationAttribute] {
+ var result = [WMTOperationAttribute]()
+ for id in ids {
+ guard let index = firstIndex(where: { $0.label.id == id }) else {
+ continue
+ }
+ result.append(self[index])
+ remove(at: index)
+ }
+ return result
+ }
+
+ mutating func popFirst(ids: [String]) -> WMTOperationAttribute? {
+ for id in ids {
+ guard let index = firstIndex(where: { $0.label.id == id }) else {
+ continue
+ }
+ remove(at: index)
+ return self[index]
+ }
+ return nil
+ }
+
+ mutating func popFirstGen(ids: [String]) -> T? {
+ for id in ids {
+ guard let index = firstIndex(where: { $0.label.id == id }) else {
+ continue
+ }
+ guard let attr = self[index] as? T else {
+ continue
+ }
+ remove(at: index)
+ return attr
+ }
+ return nil
+ }
+
+ mutating func popSections(from sections: [WMTTemplates.DetailTemplate.Section]) -> [WMTUserOperationVisualSection] {
+ return sections.map { popSection(from: $0) }
+ }
+
+ mutating func popSection(from section: WMTTemplates.DetailTemplate.Section) -> WMTUserOperationVisualSection {
+ let sectionFilled = WMTUserOperationVisualSection(
+ style: section.style,
+ title: pop(id: section.title)?.label.value,
+ cells: popCells(from: section)
+ )
+ return sectionFilled
+ }
+
+ mutating func popCells(from section: WMTTemplates.DetailTemplate.Section) -> [WMTUserOperationVisualCell] {
+ return section.cells?.compactMap { createCellAndPopAttr(from: $0) } ?? []
+ }
+
+ func getRemainingCells() -> [WMTUserOperationVisualCell] {
+ var cells = [WMTUserOperationVisualCell]()
+ for attr in self {
+ if let cell = createCell(from: attr) {
+ cells.append(cell)
+ }
+ }
+ return cells
+ }
+
+ mutating func createCellAndPopAttr(from templateCell: WMTTemplates.DetailTemplate.Section.Cell) -> WMTUserOperationVisualCell? {
+ guard let attr = pop(id: templateCell.name) else {
+ D.warning("Template Attribute '\(templateCell.name)', not found in FormData Attributes")
+ return nil
+ }
+ return createCell(from: attr, templateCell: templateCell)
+ }
+
+ private func createCell(from attr: WMTOperationAttribute, templateCell: WMTTemplates.DetailTemplate.Section.Cell? = nil) -> WMTUserOperationVisualCell? {
+ let value: String
+
+ switch attr.type {
+ case .amount:
+ let amount = attr as! WMTOperationAttributeAmount
+ value = amount.valueFormatted ?? "\(amount.amountFormatted) \(amount.currencyFormatted)"
+ case .amountConversion:
+ let conversion = attr as! WMTOperationAttributeAmountConversion
+ if let sourceValue = conversion.source.valueFormatted, let targetValue = conversion.target.valueFormatted {
+ value = "\(sourceValue) → \(targetValue)"
+ } else {
+ let source = "\(conversion.source.amountFormatted) \(conversion.source.currencyFormatted)"
+ let target = "\(conversion.target.amountFormatted) \(conversion.target.currencyFormatted)"
+ value = "\(source) → \(target)"
+ }
+ case .keyValue:
+ let keyValue = attr as! WMTOperationAttributeKeyValue
+ value = keyValue.value
+ case .note:
+ let note = attr as! WMTOperationAttributeNote
+ value = note.note
+ case .image:
+ let image = attr as! WMTOperationAttributeImage
+ return WMTUserOperationImageVisualCell(
+ urlThumbnail: URL(string: image.thumbnailUrl) ?? URL(string: "error")!,
+ urlFull: image.originalUrl != nil ? URL(string: image.originalUrl!) : nil,
+ attribute: image,
+ cellTemplate: templateCell
+ )
+ case .heading:
+ value = ""
+ case .partyInfo, .unknown:
+ D.warning("Using unsuported Attribute in Templates")
+ value = ""
+ }
+
+ return WMTUserOperationStringValueAttributeVisualCell(
+ header: attr.label.value,
+ defaultFormattedStringValue: value,
+ attribute: attr,
+ cellTemplate: templateCell
+ )
+ }
+}