From 21375c4e7576671d48c84960a65b2b200273cd63 Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Thu, 27 Jun 2024 09:11:05 +0200 Subject: [PATCH 01/29] Implement `WMTTemplates` --- .../project.pbxproj | 4 + .../UserOperation/WMTOperationUIData.swift | 11 +- .../Model/UserOperation/WMTTemplates.swift | 217 ++++++++++++++++++ .../OperationUIDataTests.swift | 92 ++++++++ docs/Changelog.md | 1 + docs/Using-Operations-Service.md | 106 +++++++++ 6 files changed, 429 insertions(+), 2 deletions(-) create mode 100644 WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift diff --git a/WultraMobileTokenSDK.xcodeproj/project.pbxproj b/WultraMobileTokenSDK.xcodeproj/project.pbxproj index c91279e..d03e504 100644 --- a/WultraMobileTokenSDK.xcodeproj/project.pbxproj +++ b/WultraMobileTokenSDK.xcodeproj/project.pbxproj @@ -72,6 +72,7 @@ EA6DDF1A29F804D60011E234 /* WMTPostApprovalScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6DDF1929F804D60011E234 /* WMTPostApprovalScreen.swift */; }; EA6DDF1C29F807230011E234 /* OperationUIDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6DDF1B29F807230011E234 /* OperationUIDataTests.swift */; }; EA7A6E582B0E639800C1D4F4 /* WMTOperationDetailRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7A6E572B0E639800C1D4F4 /* WMTOperationDetailRequest.swift */; }; + EA9795132C2C18450073E861 /* WMTTemplates.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9795122C2C18450073E861 /* WMTTemplates.swift */; }; EA9CE2BE2AEAA9FD00FE4E35 /* WMTProximityCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9CE2BD2AEAA9FD00FE4E35 /* WMTProximityCheck.swift */; }; EA9CE2C22AEBDB0D00FE4E35 /* WMTPACUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9CE2C12AEBDB0D00FE4E35 /* WMTPACUtils.swift */; }; EAB7054A2AF1161500756AC2 /* PACParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB705492AF1161500756AC2 /* PACParserTests.swift */; }; @@ -159,6 +160,7 @@ EA6DDF1929F804D60011E234 /* WMTPostApprovalScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPostApprovalScreen.swift; sourceTree = ""; }; EA6DDF1B29F807230011E234 /* OperationUIDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationUIDataTests.swift; sourceTree = ""; }; EA7A6E572B0E639800C1D4F4 /* WMTOperationDetailRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOperationDetailRequest.swift; sourceTree = ""; }; + EA9795122C2C18450073E861 /* WMTTemplates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTTemplates.swift; sourceTree = ""; }; EA9CE2BD2AEAA9FD00FE4E35 /* WMTProximityCheck.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WMTProximityCheck.swift; sourceTree = ""; }; EA9CE2C12AEBDB0D00FE4E35 /* WMTPACUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WMTPACUtils.swift; sourceTree = ""; }; EAB705492AF1161500756AC2 /* PACParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PACParserTests.swift; sourceTree = ""; }; @@ -232,6 +234,7 @@ DCE5EAAF26BD81150061861A /* WMTOperationHistoryEntry.swift */, EA294F3C29F6A07A00A0494E /* WMTOperationUIData.swift */, EA9CE2BD2AEAA9FD00FE4E35 /* WMTProximityCheck.swift */, + EA9795122C2C18450073E861 /* WMTTemplates.swift */, ); path = UserOperation; sourceTree = ""; @@ -617,6 +620,7 @@ DC81D1CD244F640600F80CD6 /* WMTPush.swift in Sources */, DC6E52D6259C964600FC25BE /* WMTOperationExpirationWatcher.swift in Sources */, DCC5CCDA244DBBE2004679AC /* WMTRejectionData.swift in Sources */, + EA9795132C2C18450073E861 /* WMTTemplates.swift in Sources */, DC48803F292282FF00DB844B /* WMTInboxMessageDetail.swift in Sources */, EA6DDF0F29F8036B0011E234 /* WMTPreApprovalScreen.swift in Sources */, DCAB7BC824580B4C0006989D /* WMTQROperationParser.swift in Sources */, diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationUIData.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationUIData.swift index c70546a..2f16e02 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationUIData.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationUIData.swift @@ -33,10 +33,15 @@ open class WMTOperationUIData: Codable { /// Type of PostApprovalScrren is presented with different classes (Starting with `WMTPostApprovalScreen*`) public let postApprovalScreen: WMTPostApprovalScreen? + /// Detailed information about displaying the operation data + /// + /// Contains prearranged styles for the operation attributes for the app to display + public let templates: WMTTemplates? + // MARK: - INTERNALS private enum Keys: String, CodingKey { - case flipButtons, blockApprovalOnCall, preApprovalScreen, postApprovalScreen + case flipButtons, blockApprovalOnCall, preApprovalScreen, postApprovalScreen, templates } public required init(from decoder: Decoder) throws { @@ -45,13 +50,15 @@ open class WMTOperationUIData: Codable { blockApprovalOnCall = try? c.decode(Bool.self, forKey: .blockApprovalOnCall) preApprovalScreen = try? c.decode(WMTPreApprovalScreen.self, forKey: .preApprovalScreen) postApprovalScreen = try? c.decode(WMTPostApprovalScreenDecodable.self, forKey: .postApprovalScreen).postApprovalObject + templates = try? c.decode(WMTTemplates.self, forKey: .templates) } - public init(flipButtons: Bool?, blockApprovalOnCall: Bool?, preApprovalScreen: WMTPreApprovalScreen?, postApprovalScreen: WMTPostApprovalScreen?) { + public init(flipButtons: Bool?, blockApprovalOnCall: Bool?, preApprovalScreen: WMTPreApprovalScreen?, postApprovalScreen: WMTPostApprovalScreen?, templates: WMTTemplates? = nil) { self.flipButtons = flipButtons self.blockApprovalOnCall = blockApprovalOnCall self.preApprovalScreen = preApprovalScreen self.postApprovalScreen = postApprovalScreen + self.templates = templates } } diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift new file mode 100644 index 0000000..ad310f5 --- /dev/null +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift @@ -0,0 +1,217 @@ +// +// 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 Foundation + +/// Detailed information about displaying operation data +/// +/// Contains prearranged styles for the operation attributes for the app to display +public class WMTTemplates: Codable { + + /// The template how the operation should look like in the list of operations + let list: ListTemplate? + + /// The template for how the operation data should look like + let detail: DetailTemplate? + + // MARK: - Internals + + private enum Keys: String, CodingKey { + case list, detail + } + + public required init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: Keys.self) + list = try? c.decode(ListTemplate.self, forKey: .list) + detail = try? c.decode(DetailTemplate.self, forKey: .detail) + } + + public init(list: ListTemplate?, detail: DetailTemplate?) { + self.list = list + self.detail = detail + } + + /// This typealias specifies that attributes using it should refer to `WMTOperationAttributes`. + /// + /// AttributeName is supposed to be `WMTOperationAttribute.AttributeLabel.id` + public typealias AttributeName = String + + /// ListTemplate defines how the operation should look in the list (active operations, history) + /// + /// List cell usually contains header, title, message(subtitle) and image + public class ListTemplate: Codable { + + /// Prearranged name which can be processed by the app + let style: String? + + /// Attribute which will be used for the header + let header: AttributeName? + + /// Attribute which will be used for the title + let title: AttributeName? + + /// Attribute which will be used for the message + let message: AttributeName? + + /// Attribute which will be used for the image + let image: AttributeName? + + // MARK: - Internals + + private enum Keys: CodingKey { + case style, header, title, message, image + } + + public required init(from decoder: any Decoder) throws { + let c = try decoder.container(keyedBy: Keys.self) + self.style = try? c.decode(String.self, forKey: .style) + self.header = try? c.decode(AttributeName.self, forKey: .header) + self.title = try? c.decode(AttributeName.self, forKey: .title) + self.message = try? c.decode(AttributeName.self, forKey: .message) + self.image = try? c.decode(AttributeName.self, forKey: .image) + } + + public init(style: String?, header: AttributeName?, title: AttributeName?, message: AttributeName?, image: AttributeName?) { + self.style = style + self.header = header + self.title = title + self.message = message + self.image = image + } + } + + /// DetailTemplate defines how the operation details should appear. + /// + /// Each operation can be divided into sections with multiple cells. + /// Attributes not mentioned in the `DetailTemplate` should be displayed without custom styling. + public class DetailTemplate: Codable { + + /// Predefined style name that can be processed by the app to customize the overall look of the operation. + let style: String? + + /// Indicates if the header should be created from form data (title, message, image) or customized for a specific operation + let automaticHeaderSection: Bool? + + /// Sections of the operation data. + let sections: [Section]? + + // MARK: - Internals + + private enum Keys: String, CodingKey { + case style, sections + case automaticHeaderSection = "headerSection" + } + + public required init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: Keys.self) + style = try? c.decode(String.self, forKey: .style) + automaticHeaderSection = try? c.decode(Bool.self, forKey: .automaticHeaderSection) + sections = try? c.decode([Section].self, forKey: .sections) + } + + public init(style: String?, automaticHeaderSection: Bool?, sections: [Section]?) { + self.style = style + self.automaticHeaderSection = automaticHeaderSection + self.sections = sections + } + + /// Operation data can be divided into sections + public class Section: Codable { + + /// Prearranged name which can be processed by the app to customize the section + let style: String? + + /// Attribute for section title + let title: AttributeName? + + /// Each section can have multiple cells of data + let cells: [Cell]? + + // MARK: - Internals + + private enum Keys: String, CodingKey { + case style, title, cells + } + + public required init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: Keys.self) + style = try? c.decode(String.self, forKey: .style) + title = try? c.decode(AttributeName.self, forKey: .title) + cells = try? c.decode([Cell].self, forKey: .cells) + } + + public init(style: String?, title: AttributeName?, cells: [Cell]?) { + self.style = style + self.title = title + self.cells = cells + } + + /// Each section can have multiple cells of data + public class Cell: Codable { + + /// Prearranged name which can be processed by the app to customize the cell + let style: String? + + /// Which attribute shall be used + let name: AttributeName? + + /// Should be the title visible or hidden + let visibleTitle: Bool? + + /// Should be the content copyable + let canCopy: Bool? + + /// Define if the cell should be collapsable + let collapsable: Collapsable? + + public enum Collapsable: String, Codable { + /// The cell should not be collapsable + case no = "NO" + + /// The cell should be collapsable and in collapsed state + case collapsed = "COLLAPSED" + + /// The cell should be collapsable and in expanded state + case yes = "YES" + } + + // MARK: - Internals + + private enum Keys: String, CodingKey { + case style, name, visibleTitle, canCopy, collapsable + } + + public required init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: Keys.self) + style = try? c.decode(String.self, forKey: .style) + name = try? c.decode(AttributeName.self, forKey: .name) + visibleTitle = try? c.decode(Bool.self, forKey: .visibleTitle) + canCopy = try? c.decode(Bool.self, forKey: .canCopy) + collapsable = try? c.decode(Collapsable.self, forKey: .collapsable) + } + + public init(style: String?, name: AttributeName?, visibleTitle: Bool?, canCopy: Bool?, collapsable: Collapsable?) { + self.style = style + self.name = name + self.visibleTitle = visibleTitle + self.canCopy = canCopy + self.collapsable = collapsable + } + } + } + } +} + diff --git a/WultraMobileTokenSDKTests/OperationUIDataTests.swift b/WultraMobileTokenSDKTests/OperationUIDataTests.swift index 7cbd21a..885ab67 100644 --- a/WultraMobileTokenSDKTests/OperationUIDataTests.swift +++ b/WultraMobileTokenSDKTests/OperationUIDataTests.swift @@ -193,6 +193,43 @@ class OperationUIDataTests: XCTestCase { XCTAssertEqual(resultAttributeLabel?.value, uiAttributeLabel?.value) } + func testTemplates() { + guard let uiResult = prepareUIData(response: operationWithTemplates) else { + XCTFail("Failed to parse JSON data") + return + } + + // Anything in templates can be nil -> the app should handle nils/defaults + XCTAssertEqual(uiResult.templates?.list?.style, "POSITIVE") + XCTAssertEqual(uiResult.templates?.list?.header, nil) + XCTAssertEqual(uiResult.templates?.list?.title, "operation.account") + XCTAssertEqual(uiResult.templates?.list?.message, "operation.amount") + XCTAssertEqual(uiResult.templates?.list?.image, "operation.image") + + XCTAssertEqual(uiResult.templates?.detail?.style, nil) + XCTAssertEqual(uiResult.templates?.detail?.automaticHeaderSection, false) + + XCTAssertEqual(uiResult.templates?.detail?.sections?[0].style, "MONEY") + XCTAssertEqual(uiResult.templates?.detail?.sections?[0].title, "operation.money.header") + XCTAssertEqual(uiResult.templates?.detail?.sections?[0].cells?[0].style, nil) + XCTAssertEqual(uiResult.templates?.detail?.sections?[0].cells?[0].name, "operation.amount") + XCTAssertEqual(uiResult.templates?.detail?.sections?[0].cells?[0].visibleTitle, false) + XCTAssertEqual(uiResult.templates?.detail?.sections?[0].cells?[0].canCopy, true) + XCTAssertEqual(uiResult.templates?.detail?.sections?[0].cells?[0].collapsable, .no) + + XCTAssertEqual(uiResult.templates?.detail?.sections?[0].cells?[1].style, "CONVERSION") + XCTAssertEqual(uiResult.templates?.detail?.sections?[0].cells?[1].name, "operation.conversion") + XCTAssertEqual(uiResult.templates?.detail?.sections?[0].cells?[1].visibleTitle, nil) + XCTAssertEqual(uiResult.templates?.detail?.sections?[0].cells?[1].canCopy, true) + XCTAssertEqual(uiResult.templates?.detail?.sections?[0].cells?[1].collapsable, .no) + + XCTAssertEqual(uiResult.templates?.detail?.sections?[0].cells?[2].style, nil) + XCTAssertEqual(uiResult.templates?.detail?.sections?[0].cells?[2].name, "operation.conversion2") + XCTAssertEqual(uiResult.templates?.detail?.sections?[0].cells?[2].visibleTitle, true) + XCTAssertEqual(uiResult.templates?.detail?.sections?[0].cells?[2].canCopy, false) + XCTAssertEqual(uiResult.templates?.detail?.sections?[0].cells?[2].collapsable, .collapsed) + } + // MARK: Helpers private func prepareResult(response: String) -> WMTUserOperation? { @@ -204,6 +241,11 @@ class OperationUIDataTests: XCTestCase { let result = try? jsonDecoder.decode(WMTPostApprovalScreenGeneric.self, from: response.data(using: .utf8)!) return result } + + private func prepareUIData(response: String) -> WMTOperationUIData? { + let result = try? jsonDecoder.decode(WMTOperationUIData.self, from: response.data(using: .utf8)!) + return result + } private let jsonDecoder: JSONDecoder = { let decoder = JSONDecoder() @@ -419,4 +461,54 @@ class OperationUIDataTests: XCTestCase { } """ }() + + private let operationWithTemplates: String = { + """ + { + "flipButtons": false, + "blockApprovalOnCall": true, + "templates": { + "list": { + "style": "POSITIVE", + "header": null, + "title": "operation.account", + "message": "operation.amount", + "image": "operation.image" + }, + "detail": { + "style": null, + "headerSection": false, + "sections": [ + { + "style": "MONEY", + "title": "operation.money.header", + "cells": [ + { + "name": "operation.amount", + "visibleTitle": false, + "style": null, + "canCopy": true, + "collapsable": "NO" + }, + { + "style": "CONVERSION", + "name": "operation.conversion", + "canCopy": true, + "collapsable": "NO" + }, + { + "name": "operation.conversion2", + "visibleTitle": true, + "style": null, + "canCopy": false, + "collapsable": "COLLAPSED" + } + ] + } + ] + } + } + } + """ + }() } diff --git a/docs/Changelog.md b/docs/Changelog.md index 6a4dd89..8a975a3 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -2,6 +2,7 @@ ## 1.11.0 (TBA) +- Added WMTTemplates to WMTOperationUIData [(#162)](https://github.com/wultra/mtoken-sdk-ios/pull/162) - Extended PushParser to support parsing of inbox notifications [(#158)](https://github.com/wultra/mtoken-sdk-ios/pull/158) - Added statusReason to UserOperation [(#156)](https://github.com/wultra/mtoken-sdk-ios/pull/156) diff --git a/docs/Using-Operations-Service.md b/docs/Using-Operations-Service.md index 85fddb4..608e02c 100644 --- a/docs/Using-Operations-Service.md +++ b/docs/Using-Operations-Service.md @@ -573,6 +573,11 @@ open class WMTOperationUIData: Codable { /// /// Type of PostApprovalScrren is presented with different classes (Starting with `WMTPostApprovalScreen*`) public let postApprovalScreen: WMTPostApprovalScreen? + + /// Detailed information about displaying the operation data + /// + /// Contains prearranged styles for the operation attributes for the app to display + public let templates: WMTTemplates? } ``` @@ -728,6 +733,107 @@ public struct WMTPACData: Decodable { - Accepted formats: - notice that totp key in JWT and in query shall be `potp`! + + +## WMTTemplates + +`WMTTemplates` is part of `WMTOperationUIData`. +`WMTTemplates` class provides detailed information about displaying operation data within the application. + + +`typealias AttributeName = String` is used across the `WMTTemplates`. It explicitly says that the String that will be assigned to properties is actually `WMTOperationAttributes.AttributeLabel.id` and its **value** shall displayed. + +Definition of the `WMTTemplates `: + +```swift +public class WMTTemplates: Codable { + /// The template how the operation should look like in the list of operations + let list: ListTemplate? + + /// The template for how the operation data should look like + let detail: DetailTemplate? +} +``` + +`ListTemplate` and `DetailTemplate` go as follows: + +```swift +public class ListTemplate: Codable { + + /// Prearranged name which can be processed by the app + let style: String? + + /// Attribute which will be used for the header + let header: AttributeName? + + /// Attribute which will be used for the title + let title: AttributeName? + + /// Attribute which will be used for the message + let message: AttributeName? + + /// Attribute which will be used for the image + let image: AttributeName? +} + +public class DetailTemplate: Codable { + + /// Predefined style name that can be processed by the app to customize the overall look of the operation. + let style: String? + + /// Indicates if the header should be created from form data (title, message, image) or customized for a specific operation + let automaticHeaderSection: Bool? + + /// Sections of the operation data. + let sections: [Section]? + + /// Operation data can be divided into sections + public class Section: Codable { + + /// Prearranged name which can be processed by the app to customize the section + let style: String? + + /// Attribute for section title + let title: AttributeName? + + /// Each section can have multiple cells of data + let cells: [Cell]? + + /// Each section can have multiple cells of data + public class Cell: Codable { + + /// Prearranged name which can be processed by the app to customize the cell + let style: String? + + /// Which attribute shall be used + let name: AttributeName? + + /// Should be the title visible or hidden + let visibleTitle: Bool? + + /// Should be the content copyable + let canCopy: Bool? + + /// Define if the cell should be collapsable + let collapsable: Collapsable? + + public enum Collapsable: String, Codable { + /// The cell should not be collapsable + case no = "NO" + + /// The cell should be collapsable and in collapsed state + case collapsed = "COLLAPSED" + + /// The cell should be collapsable and in expanded state + case yes = "YES" + } + } + } +} + +``` + + ## Error handling From 17b190cdc45e26187a470de57a82d2cd0e454306 Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Thu, 27 Jun 2024 16:21:07 +0200 Subject: [PATCH 02/29] Extend ListTemplate types to accommodate more information than just AttributeId --- .../Model/UserOperation/WMTTemplates.swift | 71 ++++++++++++++----- .../OperationUIDataTests.swift | 29 +++++--- 2 files changed, 73 insertions(+), 27 deletions(-) diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift index ad310f5..8234591 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift @@ -46,8 +46,8 @@ public class WMTTemplates: Codable { /// This typealias specifies that attributes using it should refer to `WMTOperationAttributes`. /// - /// AttributeName is supposed to be `WMTOperationAttribute.AttributeLabel.id` - public typealias AttributeName = String + /// AttributeId is supposed to be `WMTOperationAttribute.AttributeLabel.id` + public typealias AttributeId = String /// ListTemplate defines how the operation should look in the list (active operations, history) /// @@ -58,16 +58,52 @@ public class WMTTemplates: Codable { let style: String? /// Attribute which will be used for the header - let header: AttributeName? + let header: AttributeFormatted? /// Attribute which will be used for the title - let title: AttributeName? + let title: AttributeFormatted? /// Attribute which will be used for the message - let message: AttributeName? + let message: AttributeFormatted? /// Attribute which will be used for the image - let image: AttributeName? + let image: AttributeId? + + /// AttributeId with additional text + /// + /// Processing of the value depends on the `type` + public class AttributeFormatted: Codable { + + /// Type describes if there is additional parsing required + let type: AttributeType + + /// This value might contain AttributeId and additional characters and might require additional parsing + /// + /// Example might be `"${operation.date} - ${operation.place}"` + let value: String + + public enum AttributeType: String, Codable { + /// Plain means that value contains only AttributeId + case plain = "PLAIN" + /// Formatted means that value requires additional parsing + case formatted = "FORMATTED" + } + + enum Keys: String, CodingKey { + case type, value + } + + public init(type: AttributeType, value: String) { + self.type = type + self.value = value + } + + public required init(from decoder: any Decoder) throws { + let c = try decoder.container(keyedBy: Keys.self) + self.type = try c.decode(AttributeType.self, forKey: .type) + self.value = try c.decode(String.self, forKey: .value) + } + } // MARK: - Internals @@ -78,13 +114,13 @@ public class WMTTemplates: Codable { public required init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: Keys.self) self.style = try? c.decode(String.self, forKey: .style) - self.header = try? c.decode(AttributeName.self, forKey: .header) - self.title = try? c.decode(AttributeName.self, forKey: .title) - self.message = try? c.decode(AttributeName.self, forKey: .message) - self.image = try? c.decode(AttributeName.self, forKey: .image) + self.header = try? c.decode(AttributeFormatted.self, forKey: .header) + self.title = try? c.decode(AttributeFormatted.self, forKey: .title) + self.message = try? c.decode(AttributeFormatted.self, forKey: .message) + self.image = try? c.decode(AttributeId.self, forKey: .image) } - public init(style: String?, header: AttributeName?, title: AttributeName?, message: AttributeName?, image: AttributeName?) { + public init(style: String?, header: AttributeFormatted?, title: AttributeFormatted?, message: AttributeFormatted?, image: AttributeId?) { self.style = style self.header = header self.title = title @@ -135,7 +171,7 @@ public class WMTTemplates: Codable { let style: String? /// Attribute for section title - let title: AttributeName? + let title: AttributeId? /// Each section can have multiple cells of data let cells: [Cell]? @@ -149,11 +185,11 @@ public class WMTTemplates: Codable { public required init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: Keys.self) style = try? c.decode(String.self, forKey: .style) - title = try? c.decode(AttributeName.self, forKey: .title) + title = try? c.decode(AttributeId.self, forKey: .title) cells = try? c.decode([Cell].self, forKey: .cells) } - public init(style: String?, title: AttributeName?, cells: [Cell]?) { + public init(style: String?, title: AttributeId?, cells: [Cell]?) { self.style = style self.title = title self.cells = cells @@ -166,7 +202,7 @@ public class WMTTemplates: Codable { let style: String? /// Which attribute shall be used - let name: AttributeName? + let name: AttributeId? /// Should be the title visible or hidden let visibleTitle: Bool? @@ -197,13 +233,13 @@ public class WMTTemplates: Codable { public required init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: Keys.self) style = try? c.decode(String.self, forKey: .style) - name = try? c.decode(AttributeName.self, forKey: .name) + name = try? c.decode(AttributeId.self, forKey: .name) visibleTitle = try? c.decode(Bool.self, forKey: .visibleTitle) canCopy = try? c.decode(Bool.self, forKey: .canCopy) collapsable = try? c.decode(Collapsable.self, forKey: .collapsable) } - public init(style: String?, name: AttributeName?, visibleTitle: Bool?, canCopy: Bool?, collapsable: Collapsable?) { + public init(style: String?, name: AttributeId?, visibleTitle: Bool?, canCopy: Bool?, collapsable: Collapsable?) { self.style = style self.name = name self.visibleTitle = visibleTitle @@ -214,4 +250,3 @@ public class WMTTemplates: Codable { } } } - diff --git a/WultraMobileTokenSDKTests/OperationUIDataTests.swift b/WultraMobileTokenSDKTests/OperationUIDataTests.swift index 885ab67..094e427 100644 --- a/WultraMobileTokenSDKTests/OperationUIDataTests.swift +++ b/WultraMobileTokenSDKTests/OperationUIDataTests.swift @@ -194,16 +194,18 @@ class OperationUIDataTests: XCTestCase { } func testTemplates() { - guard let uiResult = prepareUIData(response: operationWithTemplates) else { + guard let uiResult = prepareUIData(response: uiDataWithTemplates) else { XCTFail("Failed to parse JSON data") return } - // Anything in templates can be nil -> the app should handle nils/defaults XCTAssertEqual(uiResult.templates?.list?.style, "POSITIVE") - XCTAssertEqual(uiResult.templates?.list?.header, nil) - XCTAssertEqual(uiResult.templates?.list?.title, "operation.account") - XCTAssertEqual(uiResult.templates?.list?.message, "operation.amount") + XCTAssertEqual(uiResult.templates?.list?.header?.type, .formatted) + XCTAssertEqual(uiResult.templates?.list?.header?.value, "${operation.request_no} Withdrawal Initiation") + XCTAssertEqual(uiResult.templates?.list?.title?.type, .plain) + XCTAssertEqual(uiResult.templates?.list?.title?.value, "operation.amount") + XCTAssertEqual(uiResult.templates?.list?.message?.type, .formatted) + XCTAssertEqual(uiResult.templates?.list?.message?.value, "${operation.date} - ${operation.place}") XCTAssertEqual(uiResult.templates?.list?.image, "operation.image") XCTAssertEqual(uiResult.templates?.detail?.style, nil) @@ -462,7 +464,7 @@ class OperationUIDataTests: XCTestCase { """ }() - private let operationWithTemplates: String = { + private let uiDataWithTemplates: String = { """ { "flipButtons": false, @@ -470,9 +472,18 @@ class OperationUIDataTests: XCTestCase { "templates": { "list": { "style": "POSITIVE", - "header": null, - "title": "operation.account", - "message": "operation.amount", + "header": { + "type": "FORMATTED", + "value": "${operation.request_no} Withdrawal Initiation" + }, + "title": { + "type": "PLAIN", + "value": "operation.amount" + }, + "message": { + "type": "FORMATTED", + "value": "${operation.date} - ${operation.place}" + }, "image": "operation.image" }, "detail": { From 8f154c7f6174b395875c6fc72c13ba6780759a87 Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Thu, 4 Jul 2024 14:15:45 +0200 Subject: [PATCH 03/29] Change internal classes properties access --- .../Model/UserOperation/WMTTemplates.swift | 54 +++++++++---------- .../OperationUIDataTests.swift | 2 +- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift index 8234591..ce8317e 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift @@ -22,10 +22,10 @@ import Foundation public class WMTTemplates: Codable { /// The template how the operation should look like in the list of operations - let list: ListTemplate? + public let list: ListTemplate? /// The template for how the operation data should look like - let detail: DetailTemplate? + public let detail: DetailTemplate? // MARK: - Internals @@ -35,6 +35,7 @@ public class WMTTemplates: Codable { public required init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: Keys.self) + list = try? c.decode(ListTemplate.self, forKey: .list) detail = try? c.decode(DetailTemplate.self, forKey: .detail) } @@ -55,19 +56,19 @@ public class WMTTemplates: Codable { public class ListTemplate: Codable { /// Prearranged name which can be processed by the app - let style: String? + public let style: String? /// Attribute which will be used for the header - let header: AttributeFormatted? + public let header: AttributeFormatted? /// Attribute which will be used for the title - let title: AttributeFormatted? + public let title: AttributeFormatted? /// Attribute which will be used for the message - let message: AttributeFormatted? + public let message: AttributeFormatted? /// Attribute which will be used for the image - let image: AttributeId? + public let image: AttributeId? /// AttributeId with additional text /// @@ -75,12 +76,12 @@ public class WMTTemplates: Codable { public class AttributeFormatted: Codable { /// Type describes if there is additional parsing required - let type: AttributeType + public let type: AttributeType /// This value might contain AttributeId and additional characters and might require additional parsing /// /// Example might be `"${operation.date} - ${operation.place}"` - let value: String + public let value: String public enum AttributeType: String, Codable { /// Plain means that value contains only AttributeId @@ -136,19 +137,18 @@ public class WMTTemplates: Codable { public class DetailTemplate: Codable { /// Predefined style name that can be processed by the app to customize the overall look of the operation. - let style: String? + public let style: String? /// Indicates if the header should be created from form data (title, message, image) or customized for a specific operation - let automaticHeaderSection: Bool? + public let automaticHeaderSection: Bool? /// Sections of the operation data. - let sections: [Section]? + public let sections: [Section]? // MARK: - Internals private enum Keys: String, CodingKey { - case style, sections - case automaticHeaderSection = "headerSection" + case style, sections, automaticHeaderSection } public required init(from decoder: Decoder) throws { @@ -168,13 +168,13 @@ public class WMTTemplates: Codable { public class Section: Codable { /// Prearranged name which can be processed by the app to customize the section - let style: String? + public let style: String? /// Attribute for section title - let title: AttributeId? + public let title: AttributeId? /// Each section can have multiple cells of data - let cells: [Cell]? + public let cells: [Cell]? // MARK: - Internals @@ -198,20 +198,20 @@ public class WMTTemplates: Codable { /// Each section can have multiple cells of data public class Cell: Codable { - /// Prearranged name which can be processed by the app to customize the cell - let style: String? - /// Which attribute shall be used - let name: AttributeId? + public let name: AttributeId + + /// Prearranged name which can be processed by the app to customize the cell + public let style: String? /// Should be the title visible or hidden - let visibleTitle: Bool? + public let visibleTitle: Bool? /// Should be the content copyable - let canCopy: Bool? + public let canCopy: Bool? /// Define if the cell should be collapsable - let collapsable: Collapsable? + public let collapsable: Collapsable? public enum Collapsable: String, Codable { /// The cell should not be collapsable @@ -233,15 +233,15 @@ public class WMTTemplates: Codable { public required init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: Keys.self) style = try? c.decode(String.self, forKey: .style) - name = try? c.decode(AttributeId.self, forKey: .name) + name = try c.decode(AttributeId.self, forKey: .name) visibleTitle = try? c.decode(Bool.self, forKey: .visibleTitle) canCopy = try? c.decode(Bool.self, forKey: .canCopy) collapsable = try? c.decode(Collapsable.self, forKey: .collapsable) } - public init(style: String?, name: AttributeId?, visibleTitle: Bool?, canCopy: Bool?, collapsable: Collapsable?) { - self.style = style + public init(style: String?, name: AttributeId, visibleTitle: Bool?, canCopy: Bool?, collapsable: Collapsable?) { self.name = name + self.style = style self.visibleTitle = visibleTitle self.canCopy = canCopy self.collapsable = collapsable diff --git a/WultraMobileTokenSDKTests/OperationUIDataTests.swift b/WultraMobileTokenSDKTests/OperationUIDataTests.swift index 094e427..5588b62 100644 --- a/WultraMobileTokenSDKTests/OperationUIDataTests.swift +++ b/WultraMobileTokenSDKTests/OperationUIDataTests.swift @@ -488,7 +488,7 @@ class OperationUIDataTests: XCTestCase { }, "detail": { "style": null, - "headerSection": false, + "automaticHeaderSection": false, "sections": [ { "style": "MONEY", From 2b7059a212f52483ebe014832c9e5550f266340b Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Tue, 9 Jul 2024 14:15:52 +0200 Subject: [PATCH 04/29] Add formated templates - same as Id but to be distinguieshed that it can contain two ids --- .../Model/UserOperation/WMTTemplates.swift | 47 ++++--------------- 1 file changed, 9 insertions(+), 38 deletions(-) diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift index ce8317e..a6dd097 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift @@ -48,7 +48,13 @@ public class WMTTemplates: Codable { /// This typealias specifies that attributes using it should refer to `WMTOperationAttributes`. /// /// AttributeId is supposed to be `WMTOperationAttribute.AttributeLabel.id` - public typealias AttributeId = String + public typealias AttributeId = String + + /// This typealias specifies that attributes using might refer to `WMTOperationAttributes` + /// and additional characters and micht require additional parsing . + /// + /// Example might be `"${operation.date} - ${operation.place}"` + public typealias AttributeFormatted = String /// ListTemplate defines how the operation should look in the list (active operations, history) /// @@ -68,43 +74,8 @@ public class WMTTemplates: Codable { public let message: AttributeFormatted? /// Attribute which will be used for the image - public let image: AttributeId? - - /// AttributeId with additional text - /// - /// Processing of the value depends on the `type` - public class AttributeFormatted: Codable { - - /// Type describes if there is additional parsing required - public let type: AttributeType - - /// This value might contain AttributeId and additional characters and might require additional parsing - /// - /// Example might be `"${operation.date} - ${operation.place}"` - public let value: String - - public enum AttributeType: String, Codable { - /// Plain means that value contains only AttributeId - case plain = "PLAIN" - /// Formatted means that value requires additional parsing - case formatted = "FORMATTED" - } - - enum Keys: String, CodingKey { - case type, value - } - - public init(type: AttributeType, value: String) { - self.type = type - self.value = value - } - - public required init(from decoder: any Decoder) throws { - let c = try decoder.container(keyedBy: Keys.self) - self.type = try c.decode(AttributeType.self, forKey: .type) - self.value = try c.decode(String.self, forKey: .value) - } - } + public let image: AttributeFormatted? + // MARK: - Internals From 227449ffcd398dfcc37f851c4420aa8224532f2f Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Tue, 9 Jul 2024 14:16:05 +0200 Subject: [PATCH 05/29] DRAFT: hardcoded data for testing --- .../UserOperation/WMTUserOperation.swift | 118 +++++++++++++++++- 1 file changed, 117 insertions(+), 1 deletion(-) diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift index 19679a6..ac1e6c6 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift @@ -57,7 +57,7 @@ open class WMTUserOperation: WMTOperation, Codable { /// Additional UI data to present /// /// Additional UI data such as Pre-Approval Screen or Post-Approval Screen should be presented. - public let ui: WMTOperationUIData? + public let ui = prepareTemplates(response: uiTemplates) /// Proximity Check Data to be passed when OTP is handed to the app public var proximityCheck: WMTProximityCheck? @@ -67,3 +67,119 @@ open class WMTUserOperation: WMTOperation, Codable { /// Max 32 characters are expected. Possible values depend on the backend implementation and configuration. public let statusReason: String? } + + +private let jsonDecoder: JSONDecoder = { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + return decoder +}() + +private func prepareTemplates(response: String) -> WMTOperationUIData? { + let result = try? jsonDecoder.decode(WMTOperationUIData.self, from: response.data(using: .utf8)!) + return result +} + +private let uiTemplates: String = { +""" +{ + "templates": { + "list": { + "header": "${operation.request_no} Withdrawal Initiation", + "message": "${operation.tx_amount}", + "title": "${operation.account} · ${operation.enterprise}" + }, + "detail": { + "style": null, + "automaticHeaderSection": false, + "sections": [ + { + "style": "HEADER", + "cells": [ + { + "style": "ROUND", + "name": "operation.image" + }, + { + "style": "HEADER_TITLE", + "name": "operation.dapp_originUrl" + } + ] + }, + { + "style": "HEADER_WARNING", + "title": null, + "cells": [ + { + "style": "WARNING_NOTE", + "name": "operation.blind_note" + } + ] + }, + { + "style": "MONEY", + "title": null, + "cells": [ + { + "name": "operation.request_no" + }, + { + "name": "operation.tx_amount", + "canCopy": true + }, + { + "name": "operation.account" + }, + { + "name": "operation.network" + }, + { + "name": "operation.address" + }, + { + "name": "operation.fee_amount" + }, + { + "name": "operation.scheme" + } + ] + }, + { + "style": "DESCRIPTION", + "cells": [ + { + "name": "operation.initiated_by", + "visibleTitle": true + }, + { + "style": "CONVERSION", + "name": "operation.location", + "canCopy": true, + "collapsable": "NO" + }, + { + "name": "operation.initiated_at", + "visibleTitle": false, + "style": null, + "canCopy": false + }, + { + "name": "operation.enterprise" + } + ] + }, + { + "style": "DATA", + "cells": [ + { + "name": "operation.tx_data", + "collapsable": "COLLAPSED" + } + ] + } + ] + } + } +} +""" +}() From 0302bb0b399e1e3b7bd919eb68e5da1303b39669 Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Tue, 9 Jul 2024 14:31:22 +0200 Subject: [PATCH 06/29] Revert "Merge branch 'develop' into issues/161-templates" This reverts commit 7fc395f8ca2a1b6a99ea990839e8e9d5de67aa77, reversing changes made to 227449ffcd398dfcc37f851c4420aa8224532f2f. --- Cartfile | 2 +- Cartfile.resolved | 2 +- Deploy/WultraMobileTokenSDK.podspec | 2 +- WultraMobileTokenSDK.podspec | 4 +- WultraMobileTokenSDK/Common/WMTLogger.swift | 115 ++++-------------- .../UserOperation/WMTOperationFormData.swift | 2 +- .../Service/WMTOperationsImpl.swift | 4 +- .../Utils/WMTOperationExpirationWatcher.swift | 12 +- .../IntegrationProxy.swift | 2 +- .../IntegrationTests.swift | 2 +- docs/Changelog.md | 15 +-- docs/Logging.md | 27 ++-- 12 files changed, 61 insertions(+), 128 deletions(-) diff --git a/Cartfile b/Cartfile index f5a715d..40b13dc 100644 --- a/Cartfile +++ b/Cartfile @@ -1 +1 @@ -github "wultra/networking-apple" "1.4.0" +github "wultra/networking-apple" "1.3.0" diff --git a/Cartfile.resolved b/Cartfile.resolved index 28d9b28..72d5757 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,3 +1,3 @@ binary "https://raw.githubusercontent.com/wultra/powerauth-mobile-sdk-spm/1.8.0/PowerAuth2.json" "1.8.0" binary "https://raw.githubusercontent.com/wultra/powerauth-mobile-sdk-spm/1.8.0/PowerAuthCore.json" "1.8.0" -github "wultra/networking-apple" "1.4.0" +github "wultra/networking-apple" "1.3.0" diff --git a/Deploy/WultraMobileTokenSDK.podspec b/Deploy/WultraMobileTokenSDK.podspec index 34c1d92..df2ea20 100644 --- a/Deploy/WultraMobileTokenSDK.podspec +++ b/Deploy/WultraMobileTokenSDK.podspec @@ -19,7 +19,7 @@ Pod::Spec.new do |s| s.subspec 'Common' do |sub| sub.source_files = 'WultraMobileTokenSDK/Common/**/*.swift' sub.dependency 'PowerAuth2', '~> 1.8.0' - sub.dependency 'WultraPowerAuthNetworking', '~> 1.4.0' + sub.dependency 'WultraPowerAuthNetworking', '~> 1.3.0' end # 'Operations' subspec diff --git a/WultraMobileTokenSDK.podspec b/WultraMobileTokenSDK.podspec index 8a8d534..49fdd27 100644 --- a/WultraMobileTokenSDK.podspec +++ b/WultraMobileTokenSDK.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'WultraMobileTokenSDK' - s.version = '1.12.0' + s.version = '1.8.0' # Metadata s.license = { :type => 'Apache License, Version 2.0', :file => 'LICENSE' } s.summary = 'High level PowerAuth based library written in swift' @@ -19,7 +19,7 @@ Pod::Spec.new do |s| s.subspec 'Common' do |sub| sub.source_files = 'WultraMobileTokenSDK/Common/**/*.swift' sub.dependency 'PowerAuth2', '~> 1.8.0' - sub.dependency 'WultraPowerAuthNetworking', '~> 1.4.0' + sub.dependency 'WultraPowerAuthNetworking', '~> 1.3.0' end # 'Operations' subspec diff --git a/WultraMobileTokenSDK/Common/WMTLogger.swift b/WultraMobileTokenSDK/Common/WMTLogger.swift index ad8bd23..ab36922 100644 --- a/WultraMobileTokenSDK/Common/WMTLogger.swift +++ b/WultraMobileTokenSDK/Common/WMTLogger.swift @@ -16,67 +16,49 @@ import Foundation -/// WMTLogger provides simple logging facility. -/// -/// Note that HTTP logs are managed by the underlying Networking library (via `WPNLogger` class). +/// WMTLogger provides simple logging facility available for DEBUG build of the library. public class WMTLogger { - /// Verbose level of the logger. + /// Defines verbose level for this simple debugging facility. public enum VerboseLevel: Int { /// Silences all messages. case off = 0 - /// Only errors will be logged. + /// Only errors will be printed to the debug console. case errors = 1 - /// Errors and warnings will be logged. + /// Errors and warnings will be printed to the debug console. case warnings = 2 - /// Error, warning and info messages will be logged. - case info = 3 - /// All messages will logged - including debug messages - case debug = 4 + /// All messages will be printed to the debug console. + case all = 3 } - /// Logger delegate - public static weak var delegate: WMTLoggerDelegate? - - /// Current verbose level. `warnings` by default + /// Current verbose level. Note that value is ignored for non-DEBUG builds. public static var verboseLevel: VerboseLevel = .warnings - /// Prints simple message to the system console. - static func debug(_ message: @autoclosure () -> String) { - log(message(), level: .debug) - } - - /// Prints simple message to the system console. - static func info(_ message: @autoclosure () -> String) { - log(message(), level: .info) + /// Prints simple message to the debug console. + static func print(_ message: @autoclosure () -> String) { + #if DEBUG || WMT_ENABLE_LOGGING + if verboseLevel == .all { + Swift.print("[WMT] \(message())") + } + #endif } - /// Prints warning message to the system console. + /// Prints warning message to the debug console. static func warning(_ message: @autoclosure () -> String) { - log(message(), level: .warning) + #if DEBUG || WMT_ENABLE_LOGGING + if verboseLevel.rawValue >= VerboseLevel.warnings.rawValue { + Swift.print("[WMT] WARNING: \(message())") + } + #endif } - /// Prints error message to the system console. + /// Prints error message to the debug console. static func error(_ message: @autoclosure () -> String) { - log(message(), level: .error) - } - - private static func log(_ message: @autoclosure () -> String, level: WMTLogLevel) { - let levelAllowed = level.minVerboseLevel.rawValue <= verboseLevel.rawValue - let forceReport = delegate?.wmtFollowVerboseLevel == false - guard levelAllowed || forceReport else { - // not logging - return - } - - let msg = message() - - if levelAllowed { - print("[WMT:\(level.logName)] \(msg)") - } - if levelAllowed || forceReport { - delegate?.wmtLog(message: msg, logLevel: level) + #if DEBUG || WMT_ENABLE_LOGGING + if verboseLevel != .off { + Swift.print("[WMT] ERROR: \(message())") } + #endif } #if DEBUG @@ -102,51 +84,4 @@ public class WMTLogger { #endif } -/// Delegate that can further process logs from the library -public protocol WMTLoggerDelegate: AnyObject { - - /// If the delegate should follow selected verbosity level. - /// - /// When set to true, then (for example) if `errors` is selected as a `verboseLevel`, only `error` logLevel will be called. - /// When set to false, all methods might be called no matter the selected `verboseLevel`. - var wmtFollowVerboseLevel: Bool { get } - - /// Log was recorded - /// - Parameters: - /// - message: Message of the log - /// - logLevel: Log level - func wmtLog(message: String, logLevel: WMTLogLevel) -} - -/// Level of the log -public enum WMTLogLevel { - /// Debug logs. Might contain sensitive data like body of the request etc. - /// You should only use this level during development. - case debug - /// Regular library logic logs - case info - /// Non-critical warning - case warning - /// Error happened - case error - - fileprivate var minVerboseLevel: WMTLogger.VerboseLevel { - return switch self { - case .debug: .debug - case .info: .info - case .warning: .warnings - case .error: .errors - } - } - - fileprivate var logName: String { - return switch self { - case .debug: "DEBUG" - case .info: "INFO" - case .warning: "WARNING" - case .error: "ERROR" - } - } -} - internal typealias D = WMTLogger diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationFormData.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationFormData.swift index b3b84db..e4ad53c 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationFormData.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationFormData.swift @@ -59,7 +59,7 @@ public class WMTOperationFormData: Codable { } } } catch { - D.error("No attributes in WMTOperationFormData: \(error)") + D.print("No attributes in WMTOperationFormData: \(error)") } attributes = operationAttributes diff --git a/WultraMobileTokenSDK/Operations/Service/WMTOperationsImpl.swift b/WultraMobileTokenSDK/Operations/Service/WMTOperationsImpl.swift index 5aed671..7b16cfd 100644 --- a/WultraMobileTokenSDK/Operations/Service/WMTOperationsImpl.swift +++ b/WultraMobileTokenSDK/Operations/Service/WMTOperationsImpl.swift @@ -374,7 +374,7 @@ class WMTOperationsImpl: WMTOperations, WMTService { adjustedInterval = interval } - D.info("Operations polling started with \(adjustedInterval) seconds interval") + D.print("Operations polling started with \(adjustedInterval) seconds interval") pollingTimer = Timer.scheduledTimer(withTimeInterval: adjustedInterval, repeats: true) { [weak self] _ in self?.refreshOperations() } @@ -393,7 +393,7 @@ class WMTOperationsImpl: WMTOperations, WMTService { } pollingTimer = nil timer.invalidate() - D.info("Operations polling stopped") + D.print("Operations polling stopped") } // MARK: - private functions diff --git a/WultraMobileTokenSDK/Operations/Utils/WMTOperationExpirationWatcher.swift b/WultraMobileTokenSDK/Operations/Utils/WMTOperationExpirationWatcher.swift index d036410..deac2b5 100644 --- a/WultraMobileTokenSDK/Operations/Utils/WMTOperationExpirationWatcher.swift +++ b/WultraMobileTokenSDK/Operations/Utils/WMTOperationExpirationWatcher.swift @@ -134,7 +134,7 @@ public class WMTOperationExpirationWatcher { if opsToWatch.isEmpty { D.warning("WMTOperationExpirationWatcher: All operations are already watched") } else { - D.debug("WMTOperationExpirationWatcher: Adding \(opsToWatch.count) operation to watch.") + D.print("WMTOperationExpirationWatcher: Adding \(opsToWatch.count) operation to watch.") self.operationsToWatch.append(contentsOf: opsToWatch) self.prepareTimer() } @@ -175,10 +175,10 @@ public class WMTOperationExpirationWatcher { // when nil is provided, we consider it as "stop all" if let operations = operations { self.operationsToWatch.removeAll(where: { current in operations.contains(where: { toRemove in toRemove.equals(other: current) }) }) - D.debug("WMTOperationExpirationWatcher: Stoped watching \(operations.count) operations.") + D.print("WMTOperationExpirationWatcher: Stoped watching \(operations.count) operations.") } else { self.operationsToWatch.removeAll() - D.debug("WMTOperationExpirationWatcher: Stoped watching all operations.") + D.print("WMTOperationExpirationWatcher: Stoped watching all operations.") } self.prepareTimer() } @@ -194,7 +194,7 @@ public class WMTOperationExpirationWatcher { timer = nil guard operationsToWatch.isEmpty == false else { - D.debug("WMTOperationExpirationWatcher: No operations to watch.") + D.print("WMTOperationExpirationWatcher: No operations to watch.") return } @@ -211,7 +211,7 @@ public class WMTOperationExpirationWatcher { // The 0.1 addition is a correction of the Timer class which can fire slightly (in order of 0.000x seconds) earlier than scheduled. let interval = max(5, firstOp.operationExpires.timeIntervalSince1970 - self.currentDateProvider.currentDate.timeIntervalSince1970) + 0.1 - D.debug("WMTOperationExpirationWatcher: Scheduling operation expire check in \(Int(interval)) seconds.") + D.print("WMTOperationExpirationWatcher: Scheduling operation expire check in \(Int(interval)) seconds.") self.timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { [weak self] _ in guard let self = self else { @@ -230,7 +230,7 @@ public class WMTOperationExpirationWatcher { self.operationsToWatch.removeAll(where: { $0.isExpired(currentDate) }) self.prepareTimer() DispatchQueue.main.async { - D.info("WMTOperationExpirationWatcher: Reporting \(expiredOps.count) expired operations.") + D.print("WMTOperationExpirationWatcher: Reporting \(expiredOps.count) expired operations.") self.delegate?.operationsExpired(expiredOps) } } diff --git a/WultraMobileTokenSDKTests/IntegrationProxy.swift b/WultraMobileTokenSDKTests/IntegrationProxy.swift index b0497df..e8bffc5 100644 --- a/WultraMobileTokenSDKTests/IntegrationProxy.swift +++ b/WultraMobileTokenSDKTests/IntegrationProxy.swift @@ -31,7 +31,7 @@ class IntegrationProxy { typealias Callback = (_ error: String?) -> Void func prepareActivation(pin: String, callback: @escaping Callback) { - WPNLogger.verboseLevel = .debug + WPNLogger.verboseLevel = .all guard let configPath = Bundle.init(for: IntegrationProxy.self).path(forResource: "config", ofType: "json", inDirectory: "Configs") else { callback("Config file config.json is not present.") return diff --git a/WultraMobileTokenSDKTests/IntegrationTests.swift b/WultraMobileTokenSDKTests/IntegrationTests.swift index 689aa0e..b3af71b 100644 --- a/WultraMobileTokenSDKTests/IntegrationTests.swift +++ b/WultraMobileTokenSDKTests/IntegrationTests.swift @@ -35,7 +35,7 @@ class IntegrationTests: XCTestCase { override func setUp() { super.setUp() - WMTLogger.verboseLevel = .debug + WMTLogger.verboseLevel = .all proxy = IntegrationProxy() let exp = XCTestExpectation(description: "setup expectation") diff --git a/docs/Changelog.md b/docs/Changelog.md index 7ae2a10..294970c 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -1,16 +1,11 @@ # Changelog -## 1.11.1 (July 2024) -- Added WMTTemplates to WMTOperationUIData [(#162)](https://github.com/wultra/mtoken-sdk-ios/pull/162) - -- Dependency `networking-apple` is now required in version `1.4.x` +## 1.11.0 (TBA) -## 1.11.0 (July 2024) - -- Added `resultTexts` to the `UserOperation` [(#160)](https://github.com/wultra/mtoken-sdk-ios/pull/160) -- Extended `PushParser` to support parsing of inbox notifications [(#158)](https://github.com/wultra/mtoken-sdk-ios/pull/158) -- Added `statusReason` to the `UserOperation` [(#156)](https://github.com/wultra/mtoken-sdk-ios/pull/156) -- Improved logging options [(#164)](https://github.com/wultra/mtoken-sdk-ios/pull/164) +- Added WMTTemplates to WMTOperationUIData [(#162)](https://github.com/wultra/mtoken-sdk-ios/pull/162) +- Added resultTexts to UserOperation [(#160)](https://github.com/wultra/mtoken-sdk-ios/pull/160) +- Extended PushParser to support parsing of inbox notifications [(#158)](https://github.com/wultra/mtoken-sdk-ios/pull/158) +- Added statusReason to UserOperation [(#156)](https://github.com/wultra/mtoken-sdk-ios/pull/156) ## 1.10.0 (Apr 18, 2024) diff --git a/docs/Logging.md b/docs/Logging.md index a0b942e..1312278 100644 --- a/docs/Logging.md +++ b/docs/Logging.md @@ -1,23 +1,26 @@ # Logging -You can set up logging for the library using the `WMTLogger` class. +For logging purposes, WMT uses `WMTLogger` class that prints to the console. -Networking traffic is logged in its own logger described in the [networking library documentation](https://github.com/wultra/networking-apple). +Note that logging to the console is available only when the library is compiled in the `Debug` mode or with `WMT_ENABLE_LOGGING` Swift compile condition. ### Verbosity Level -You can limit the amount of logged information via the `verboseLevel` property. +You can limit the amount of logged information via `verboseLevel` property. -| Level | Description | -| ---------------------- | ------------------------------------------------- | -| `off` | Silences all messages. | -| `errors` | Only errors will be logged. | -| `warnings` _(default)_ | Errors and warnings will be logged. | -| `info` | Error, warning and info messages will be logged. | -| `debug` | All messages will be logged. | +| Level | Description | +| --- | --- | +| `off` | Silences all messages. | +| `errors` | Only errors will be printed to the debug console. | +| `warnings` _(default)_ | Errors and warnings will be printed to the debug console. | +| `all` | All messages will be printed to the debug console. | -### Logger Delegate +Example configuration: -In case you want to process logs on your own (for example log into a file or some cloud service), you can set `WMTLogger.delegate`. +```swift +import WultraMobileTokenSDK + +WMTLogger.verboseLevel = .all +``` \ No newline at end of file From 37e38d3cd94215ea886b55973ed6b12c3339158d Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Tue, 9 Jul 2024 14:32:32 +0200 Subject: [PATCH 07/29] Fix tests and swiftlint --- .../Operations/Model/UserOperation/WMTTemplates.swift | 1 - .../Model/UserOperation/WMTUserOperation.swift | 1 - WultraMobileTokenSDKTests/OperationUIDataTests.swift | 9 +++------ 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift index a6dd097..4a74310 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift @@ -76,7 +76,6 @@ public class WMTTemplates: Codable { /// Attribute which will be used for the image public let image: AttributeFormatted? - // MARK: - Internals private enum Keys: CodingKey { diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift index ac1e6c6..6e4bd1d 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift @@ -68,7 +68,6 @@ open class WMTUserOperation: WMTOperation, Codable { public let statusReason: String? } - private let jsonDecoder: JSONDecoder = { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 diff --git a/WultraMobileTokenSDKTests/OperationUIDataTests.swift b/WultraMobileTokenSDKTests/OperationUIDataTests.swift index 5588b62..3ce41e6 100644 --- a/WultraMobileTokenSDKTests/OperationUIDataTests.swift +++ b/WultraMobileTokenSDKTests/OperationUIDataTests.swift @@ -200,12 +200,9 @@ class OperationUIDataTests: XCTestCase { } XCTAssertEqual(uiResult.templates?.list?.style, "POSITIVE") - XCTAssertEqual(uiResult.templates?.list?.header?.type, .formatted) - XCTAssertEqual(uiResult.templates?.list?.header?.value, "${operation.request_no} Withdrawal Initiation") - XCTAssertEqual(uiResult.templates?.list?.title?.type, .plain) - XCTAssertEqual(uiResult.templates?.list?.title?.value, "operation.amount") - XCTAssertEqual(uiResult.templates?.list?.message?.type, .formatted) - XCTAssertEqual(uiResult.templates?.list?.message?.value, "${operation.date} - ${operation.place}") + XCTAssertEqual(uiResult.templates?.list?.header, "${operation.request_no} Withdrawal Initiation") + XCTAssertEqual(uiResult.templates?.list?.title, "operation.amount") + XCTAssertEqual(uiResult.templates?.list?.message, "${operation.date} - ${operation.place}") XCTAssertEqual(uiResult.templates?.list?.image, "operation.image") XCTAssertEqual(uiResult.templates?.detail?.style, nil) From 39758430f08236b8509258f9acef231274d6b86f Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Tue, 9 Jul 2024 16:55:32 +0200 Subject: [PATCH 08/29] Update tamplate image to be AttributeId, rename automatic --- .../Model/UserOperation/WMTTemplates.swift | 10 ++++----- .../UserOperation/WMTUserOperation.swift | 1 + .../OperationUIDataTests.swift | 21 ++++++------------- 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift index 4a74310..b9a65e2 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift @@ -74,7 +74,7 @@ public class WMTTemplates: Codable { public let message: AttributeFormatted? /// Attribute which will be used for the image - public let image: AttributeFormatted? + public let image: AttributeId? // MARK: - Internals @@ -110,7 +110,7 @@ public class WMTTemplates: Codable { public let style: String? /// Indicates if the header should be created from form data (title, message, image) or customized for a specific operation - public let automaticHeaderSection: Bool? + public let showTitleAndMessage: Bool? /// Sections of the operation data. public let sections: [Section]? @@ -118,19 +118,19 @@ public class WMTTemplates: Codable { // MARK: - Internals private enum Keys: String, CodingKey { - case style, sections, automaticHeaderSection + case style, sections, showTitleAndMessage } public required init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: Keys.self) style = try? c.decode(String.self, forKey: .style) - automaticHeaderSection = try? c.decode(Bool.self, forKey: .automaticHeaderSection) + showTitleAndMessage = try? c.decode(Bool.self, forKey: .showTitleAndMessage) sections = try? c.decode([Section].self, forKey: .sections) } public init(style: String?, automaticHeaderSection: Bool?, sections: [Section]?) { self.style = style - self.automaticHeaderSection = automaticHeaderSection + self.showTitleAndMessage = automaticHeaderSection self.sections = sections } diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift index 6e4bd1d..c4a6db2 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift @@ -58,6 +58,7 @@ open class WMTUserOperation: WMTOperation, Codable { /// /// Additional UI data such as Pre-Approval Screen or Post-Approval Screen should be presented. public let ui = prepareTemplates(response: uiTemplates) +// public let ui: WMTOperationUIData? /// Proximity Check Data to be passed when OTP is handed to the app public var proximityCheck: WMTProximityCheck? diff --git a/WultraMobileTokenSDKTests/OperationUIDataTests.swift b/WultraMobileTokenSDKTests/OperationUIDataTests.swift index 3ce41e6..8de6a92 100644 --- a/WultraMobileTokenSDKTests/OperationUIDataTests.swift +++ b/WultraMobileTokenSDKTests/OperationUIDataTests.swift @@ -201,12 +201,12 @@ class OperationUIDataTests: XCTestCase { XCTAssertEqual(uiResult.templates?.list?.style, "POSITIVE") XCTAssertEqual(uiResult.templates?.list?.header, "${operation.request_no} Withdrawal Initiation") - XCTAssertEqual(uiResult.templates?.list?.title, "operation.amount") - XCTAssertEqual(uiResult.templates?.list?.message, "${operation.date} - ${operation.place}") + XCTAssertEqual(uiResult.templates?.list?.title, "${operation.account} · ${operation.enterprise}") + XCTAssertEqual(uiResult.templates?.list?.message, "${operation.tx_amount}") XCTAssertEqual(uiResult.templates?.list?.image, "operation.image") XCTAssertEqual(uiResult.templates?.detail?.style, nil) - XCTAssertEqual(uiResult.templates?.detail?.automaticHeaderSection, false) + XCTAssertEqual(uiResult.templates?.detail?.showTitleAndMessage, false) XCTAssertEqual(uiResult.templates?.detail?.sections?[0].style, "MONEY") XCTAssertEqual(uiResult.templates?.detail?.sections?[0].title, "operation.money.header") @@ -469,18 +469,9 @@ class OperationUIDataTests: XCTestCase { "templates": { "list": { "style": "POSITIVE", - "header": { - "type": "FORMATTED", - "value": "${operation.request_no} Withdrawal Initiation" - }, - "title": { - "type": "PLAIN", - "value": "operation.amount" - }, - "message": { - "type": "FORMATTED", - "value": "${operation.date} - ${operation.place}" - }, + "header": "${operation.request_no} Withdrawal Initiation", + "message": "${operation.tx_amount}", + "title": "${operation.account} · ${operation.enterprise}", "image": "operation.image" }, "detail": { From 311c7dc544d583f7b2065afefcf251db03e1d07b Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Mon, 15 Jul 2024 16:43:36 +0200 Subject: [PATCH 09/29] Introduce Templates parser --- .../contents.xcworkspacedata | 7 + Package.resolved | 23 + .../WMTUserOperationVisualParser.swift | 408 ++++++++++++++++++ 3 files changed, 438 insertions(+) create mode 100644 .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata create mode 100644 Package.resolved create mode 100644 WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift 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 + ) + } +} From 345786d2dbc88f0414972080aa04c93bfd4734cd Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Mon, 15 Jul 2024 16:46:11 +0200 Subject: [PATCH 10/29] Inverse showTitleAndMessage check to have true as a default --- .../WMTUserOperationVisualParser.swift | 13 ++++++------- .../Model/UserOperation/WMTTemplates.swift | 15 +++++++-------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift index a08f5d7..6102b62 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift @@ -129,19 +129,18 @@ extension WMTUserOperation { var sections = [WMTUserOperationVisualSection]() - if detailTemplate.showTitleAndMessage == true { + if detailTemplate.showTitleAndMessage == false { + let dataSections = attrs.popSections(from: sectionsTemplate) + sections.append(contentsOf: dataSections) + sections.append(.init(cells: attrs.getRemainingCells())) + return .init(sections: sections) + } else { 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) } } } diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift index b9a65e2..861df4e 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift @@ -21,10 +21,10 @@ import Foundation /// Contains prearranged styles for the operation attributes for the app to display public class WMTTemplates: Codable { - /// The template how the operation should look like in the list of operations + /// How the operation should look like in the list of operations public let list: ListTemplate? - /// The template for how the operation data should look like + /// How the operation detail should look like public let detail: DetailTemplate? // MARK: - Internals @@ -45,15 +45,14 @@ public class WMTTemplates: Codable { self.detail = detail } - /// This typealias specifies that attributes using it should refer to `WMTOperationAttributes`. - /// - /// AttributeId is supposed to be `WMTOperationAttribute.AttributeLabel.id` - public typealias AttributeId = String + /// Value of the `AttributeId` is referencing an existing `WMTOperationAttribute` by `WMTOperationAttribute.AttributeLabel.id` + public typealias AttributeId = String - /// This typealias specifies that attributes using might refer to `WMTOperationAttributes` - /// and additional characters and micht require additional parsing . + /// Value of the `AttributeFormatted` typealias contains placeholders for operation attributes, + /// which are specified using the syntax `${operation.attribute}`. /// /// Example might be `"${operation.date} - ${operation.place}"` + /// Placeholders in `AttributeFormatted` need to be parsed and replaced with actual attribute values. public typealias AttributeFormatted = String /// ListTemplate defines how the operation should look in the list (active operations, history) From 6ccd45a82b01795f2739289417da31aadd650e55 Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Mon, 15 Jul 2024 17:01:21 +0200 Subject: [PATCH 11/29] Remove lint warnings and errors --- .../project.pbxproj | 12 ++++++ .../WMTUserOperationVisualParser.swift | 42 ++++++++++--------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/WultraMobileTokenSDK.xcodeproj/project.pbxproj b/WultraMobileTokenSDK.xcodeproj/project.pbxproj index 035cf79..59a0c1c 100644 --- a/WultraMobileTokenSDK.xcodeproj/project.pbxproj +++ b/WultraMobileTokenSDK.xcodeproj/project.pbxproj @@ -73,6 +73,7 @@ EA6DDF1C29F807230011E234 /* OperationUIDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6DDF1B29F807230011E234 /* OperationUIDataTests.swift */; }; EA74F7B32C2561BB004340B9 /* WMTResultTexts.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA74F7B22C2561BB004340B9 /* WMTResultTexts.swift */; }; EA7A6E582B0E639800C1D4F4 /* WMTOperationDetailRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7A6E572B0E639800C1D4F4 /* WMTOperationDetailRequest.swift */; }; + EA951B502C412E43006C76B5 /* WMTUserOperationVisualParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA951B4F2C412E43006C76B5 /* WMTUserOperationVisualParser.swift */; }; EA9795132C2C18450073E861 /* WMTTemplates.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9795122C2C18450073E861 /* WMTTemplates.swift */; }; EA9CE2BE2AEAA9FD00FE4E35 /* WMTProximityCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9CE2BD2AEAA9FD00FE4E35 /* WMTProximityCheck.swift */; }; EA9CE2C22AEBDB0D00FE4E35 /* WMTPACUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9CE2C12AEBDB0D00FE4E35 /* WMTPACUtils.swift */; }; @@ -162,6 +163,7 @@ EA6DDF1B29F807230011E234 /* OperationUIDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationUIDataTests.swift; sourceTree = ""; }; EA74F7B22C2561BB004340B9 /* WMTResultTexts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTResultTexts.swift; sourceTree = ""; }; EA7A6E572B0E639800C1D4F4 /* WMTOperationDetailRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOperationDetailRequest.swift; sourceTree = ""; }; + EA951B4F2C412E43006C76B5 /* WMTUserOperationVisualParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTUserOperationVisualParser.swift; sourceTree = ""; }; EA9795122C2C18450073E861 /* WMTTemplates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTTemplates.swift; sourceTree = ""; }; EA9CE2BD2AEAA9FD00FE4E35 /* WMTProximityCheck.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WMTProximityCheck.swift; sourceTree = ""; }; EA9CE2C12AEBDB0D00FE4E35 /* WMTPACUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WMTPACUtils.swift; sourceTree = ""; }; @@ -228,6 +230,7 @@ DC3D0B352480F368000DC4D9 /* UserOperation */ = { isa = PBXGroup; children = ( + EA951B512C412F84006C76B5 /* TemplateParser */, EA6DDF0D29F8031F0011E234 /* Screens */, DC059A3C244DD30900B24878 /* Attributes */, DCC5CCAD2449F7AC004679AC /* WMTUserOperation.swift */, @@ -458,6 +461,14 @@ path = Screens; sourceTree = ""; }; + EA951B512C412F84006C76B5 /* TemplateParser */ = { + isa = PBXGroup; + children = ( + EA951B4F2C412E43006C76B5 /* WMTUserOperationVisualParser.swift */, + ); + path = TemplateParser; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -608,6 +619,7 @@ buildActionMask = 2147483647; files = ( DC81D1C9244F38DB00F80CD6 /* WMTPushEndpoints.swift in Sources */, + EA951B502C412E43006C76B5 /* WMTUserOperationVisualParser.swift in Sources */, DC0268DF29965495000BB9FA /* WMTOperationListResponse.swift in Sources */, DC8CB202244DCBE2009DDAA3 /* WMTOperations.swift in Sources */, DC48803E292282FF00DB844B /* WMTInboxMessage.swift in Sources */, diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift index 6102b62..b63f45a 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift @@ -44,23 +44,25 @@ extension WMTUserOperation { let headerAtrr = listTemplate.header?.replacePlaceholders(from: attributes) - var title: String? = nil + var title: String? if let titleAttr = listTemplate.title?.replacePlaceholders(from: attributes) { title = titleAttr } else if !self.formData.message.isEmpty { title = self.formData.title } - var message: String? = nil + var message: String? if let messageAttr = listTemplate.message?.replacePlaceholders(from: attributes) { message = messageAttr } else if !self.formData.message.isEmpty { message = self.formData.message } - var imageUrl: URL? = nil + var imageUrl: URL? if let imgAttr = listTemplate.image, let imgAttrCell = self.formData.attributes.first(where: { $0.label.id == imgAttr }) as? WMTOperationAttributeImage { - let imageUrl = URL(string: imgAttrCell.thumbnailUrl) + imageUrl = URL(string: imgAttrCell.thumbnailUrl) + } else { + imageUrl = nil } return WMTUserOperationListVisual( @@ -74,7 +76,6 @@ extension WMTUserOperation { } } - struct WMTUserOperationListVisual { let header: String? let title: String? @@ -90,7 +91,7 @@ extension WMTUserOperation { func provideData() -> WMTUserOperationVisual? { guard let detailTemplate = self.ui?.templates?.detail else { - var attrs = self.formData.attributes + let attrs = self.formData.attributes if attrs.isEmpty { return WMTUserOperationVisual(sections: [createHeaderVisual()]) } else { @@ -171,7 +172,7 @@ public struct WMTUserOperationMessageVisualCell: WMTUserOperationVisualCell { let value: String } -public struct WMTUserOperationStringValueAttributeVisualCell: WMTUserOperationVisualCell { +public struct WMTUserOperationValueAttributeVisualCell: WMTUserOperationVisualCell { let header: String let defaultFormattedStringValue: String let attribute: WMTOperationAttribute @@ -194,7 +195,6 @@ extension WMTUserOperationImageVisualCell { } } - // MARK: Helpers private extension String { @@ -239,11 +239,11 @@ private extension String { for attribute in attributes where attribute.label.id == attributeId { switch attribute.type { case .amount: - let attr = attribute as! WMTOperationAttributeAmount + guard let attr = attribute as? WMTOperationAttributeAmount else { return nil } return attr.valueFormatted ?? "\(attr.amountFormatted) \(attr.currencyFormatted)" case .amountConversion: - let attr = attribute as! WMTOperationAttributeAmountConversion + guard let attr = attribute as? WMTOperationAttributeAmountConversion else { return nil } if let sourceValue = attr.source.valueFormatted, let targetValue = attr.target.valueFormatted { return "\(sourceValue) → \(targetValue)" @@ -254,11 +254,14 @@ private extension String { } case .keyValue: - return (attribute as! WMTOperationAttributeKeyValue).value + guard let attr = attribute as? WMTOperationAttributeKeyValue else { return nil } + return attr.value case .note: - return (attribute as! WMTOperationAttributeNote).note + guard let attr = attribute as? WMTOperationAttributeNote else { return nil } + return attr.note case .heading: - return (attribute as! WMTOperationAttributeHeading).label.value + guard let attr = attribute as? WMTOperationAttributeHeading else { return nil } + return attr.label.value case .partyInfo, .image, .unknown: return nil } @@ -267,7 +270,6 @@ private extension String { } } - private extension Array where Element: WMTOperationAttribute { mutating func pop(id: String?) -> T? { @@ -365,10 +367,10 @@ private extension Array where Element: WMTOperationAttribute { switch attr.type { case .amount: - let amount = attr as! WMTOperationAttributeAmount + guard let amount = attr as? WMTOperationAttributeAmount else { return nil } value = amount.valueFormatted ?? "\(amount.amountFormatted) \(amount.currencyFormatted)" case .amountConversion: - let conversion = attr as! WMTOperationAttributeAmountConversion + guard let conversion = attr as? WMTOperationAttributeAmountConversion else { return nil } if let sourceValue = conversion.source.valueFormatted, let targetValue = conversion.target.valueFormatted { value = "\(sourceValue) → \(targetValue)" } else { @@ -377,13 +379,13 @@ private extension Array where Element: WMTOperationAttribute { value = "\(source) → \(target)" } case .keyValue: - let keyValue = attr as! WMTOperationAttributeKeyValue + guard let keyValue = attr as? WMTOperationAttributeKeyValue else { return nil} value = keyValue.value case .note: - let note = attr as! WMTOperationAttributeNote + guard let note = attr as? WMTOperationAttributeNote else { return nil } value = note.note case .image: - let image = attr as! WMTOperationAttributeImage + guard let image = attr as? WMTOperationAttributeImage else { return nil } return WMTUserOperationImageVisualCell( urlThumbnail: URL(string: image.thumbnailUrl) ?? URL(string: "error")!, urlFull: image.originalUrl != nil ? URL(string: image.originalUrl!) : nil, @@ -397,7 +399,7 @@ private extension Array where Element: WMTOperationAttribute { value = "" } - return WMTUserOperationStringValueAttributeVisualCell( + return WMTUserOperationValueAttributeVisualCell( header: attr.label.value, defaultFormattedStringValue: value, attribute: attr, From e34851135face1818a602fd660d1d76c54676e19 Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Wed, 17 Jul 2024 09:53:18 +0200 Subject: [PATCH 12/29] Change accessing attributes --- .../WMTUserOperationVisualParser.swift | 177 ++++++++++++++---- .../UserOperation/WMTUserOperation.swift | 5 +- 2 files changed, 141 insertions(+), 41 deletions(-) diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift index b63f45a..d002413 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift @@ -16,27 +16,20 @@ import UIKit -class WMTUserOperationVisualParser { - static func prepareDetail(operation: WMTUserOperation) -> WMTUserOperationVisual? { - return operation.prepareVisualDetail() - } - - static func prepareList(operation: WMTUserOperation) -> WMTUserOperationListVisual? { +public class WMTUserOperationVisualParser { + public static func prepareForList(operation: WMTUserOperation) -> WMTUserOperationListVisual? { return operation.prepareVisualListDetail() } -} - -// MARK: WMTUserOperation Detail Visual preparation extension -extension WMTUserOperation { - func prepareVisualDetail() -> WMTUserOperationVisual? { - return WMTUserOperationVisual(sections: []) + + public static func prepareForDetail(operation: WMTUserOperation) -> WMTUserOperationVisual? { + return operation.prepareVisualDetail() } } // MARK: WMTUserOperation List Visual preparation extension extension WMTUserOperation { - func prepareVisualListDetail() -> WMTUserOperationListVisual? { + internal func prepareVisualListDetail() -> WMTUserOperationListVisual? { guard let listTemplate = self.ui?.templates?.list else { return nil } @@ -70,25 +63,37 @@ extension WMTUserOperation { title: title, message: message, style: self.ui?.templates?.list?.style, - thumbnailImage: imageUrl, + thumbnailImageURL: imageUrl, template: listTemplate ) } } -struct WMTUserOperationListVisual { - let header: String? - let title: String? - let message: String? - let style: String? - let thumbnailImage: URL? +public struct WMTUserOperationListVisual { + public let header: String? + public let title: String? + public let message: String? + public let style: String? + public let thumbnailImageURL: URL? - let template: WMTTemplates.ListTemplate + public let template: WMTTemplates.ListTemplate +} + +extension WMTUserOperationListVisual { + func downloadThumbnail(callback: @escaping (UIImage?) -> Void) { + if let thumbnailImageURL { + ImageDownloader.shared.downloadImage(at: thumbnailImageURL, ImageDownloader.Callback(callback: callback)) + } else { + callback(nil) + } + + } } +// MARK: WMTUserOperation Detail Visual preparation extension extension WMTUserOperation { - func provideData() -> WMTUserOperationVisual? { + internal func prepareVisualDetail() -> WMTUserOperationVisual? { guard let detailTemplate = self.ui?.templates?.detail else { let attrs = self.formData.attributes @@ -106,7 +111,7 @@ extension WMTUserOperation { } // Default header - func createHeaderVisual(style: String? = nil) -> WMTUserOperationVisualSection { + private func createHeaderVisual(style: String? = nil) -> WMTUserOperationVisualSection { let defaultHeaderCell = WMTUserOperationHeaderVisualCell(value: self.formData.title) let defaultMessageCell = WMTUserOperationMessageVisualCell(value: self.formData.message) @@ -117,7 +122,7 @@ extension WMTUserOperation { ) } - func createTemplateRichData(from detailTemplate: WMTTemplates.DetailTemplate) -> WMTUserOperationVisual { + private func createTemplateRichData(from detailTemplate: WMTTemplates.DetailTemplate) -> WMTUserOperationVisual { var attrs = self.formData.attributes guard let sectionsTemplate = detailTemplate.sections else { @@ -147,15 +152,15 @@ extension WMTUserOperation { } public struct WMTUserOperationVisual { - let sections: [WMTUserOperationVisualSection] + public let sections: [WMTUserOperationVisualSection] } public struct WMTUserOperationVisualSection { - let style: String? - let title: String? // not an id, actual value - let cells: [WMTUserOperationVisualCell] + public let style: String? + public let title: String? // not an id, actual value + public let cells: [WMTUserOperationVisualCell] - init(style: String? = nil, title: String? = nil, cells: [WMTUserOperationVisualCell]) { + public init(style: String? = nil, title: String? = nil, cells: [WMTUserOperationVisualCell]) { self.style = style self.title = title self.cells = cells @@ -165,25 +170,27 @@ public struct WMTUserOperationVisualSection { public protocol WMTUserOperationVisualCell { } public struct WMTUserOperationHeaderVisualCell: WMTUserOperationVisualCell { - let value: String + public let value: String } public struct WMTUserOperationMessageVisualCell: WMTUserOperationVisualCell { - let value: String + public let value: String } public struct WMTUserOperationValueAttributeVisualCell: WMTUserOperationVisualCell { - let header: String - let defaultFormattedStringValue: String - let attribute: WMTOperationAttribute - let cellTemplate: WMTTemplates.DetailTemplate.Section.Cell? + public let header: String + public let defaultFormattedStringValue: String + public let style: String? + public let attribute: WMTOperationAttribute + public 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? + public let urlThumbnail: URL + public let urlFull: URL? + public let style: String? + public let attribute: WMTOperationAttributeImage + public let cellTemplate: WMTTemplates.DetailTemplate.Section.Cell? } extension WMTUserOperationImageVisualCell { @@ -389,6 +396,7 @@ private extension Array where Element: WMTOperationAttribute { return WMTUserOperationImageVisualCell( urlThumbnail: URL(string: image.thumbnailUrl) ?? URL(string: "error")!, urlFull: image.originalUrl != nil ? URL(string: image.originalUrl!) : nil, + style: templateCell?.style, attribute: image, cellTemplate: templateCell ) @@ -402,8 +410,99 @@ private extension Array where Element: WMTOperationAttribute { return WMTUserOperationValueAttributeVisualCell( header: attr.label.value, defaultFormattedStringValue: value, + style: templateCell?.style, attribute: attr, cellTemplate: templateCell ) } } + + +/// Simple image URL downloader with a simple cache implementation +public 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 + + 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) + } + } + } + } +} diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift index c4a6db2..8a22742 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift @@ -87,11 +87,12 @@ private let uiTemplates: String = { "list": { "header": "${operation.request_no} Withdrawal Initiation", "message": "${operation.tx_amount}", - "title": "${operation.account} · ${operation.enterprise}" + "title": "${operation.account} · ${operation.enterprise}", + "image": "operation.image" }, "detail": { "style": null, - "automaticHeaderSection": false, + "showTitleAndMessage": false, "sections": [ { "style": "HEADER", From 80749e948c8887090670b70c2eb759106d1087ea Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Thu, 18 Jul 2024 13:29:07 +0200 Subject: [PATCH 13/29] Improve Visual parser --- .../WMTUserOperationVisualParser.swift | 256 ++++++++++++------ 1 file changed, 172 insertions(+), 84 deletions(-) diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift index d002413..be362e9 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift @@ -17,47 +17,138 @@ import UIKit public class WMTUserOperationVisualParser { - public static func prepareForList(operation: WMTUserOperation) -> WMTUserOperationListVisual? { + public static func prepareForList(operation: WMTUserOperation) -> WMTUserOperationListVisual { return operation.prepareVisualListDetail() } - public static func prepareForDetail(operation: WMTUserOperation) -> WMTUserOperationVisual? { + public static func prepareForDetail(operation: WMTUserOperation) -> WMTUserOperationVisual { return operation.prepareVisualDetail() } } +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 + + 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 struct WMTUserOperationVisual { + public let sections: [WMTUserOperationVisualSection] +} + +public struct WMTUserOperationVisualSection { + public let style: String? + public let title: String? // not an id, actual value + public let cells: [WMTUserOperationVisualCell] + + public 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 { + public let value: String +} + +public struct WMTUserOperationMessageVisualCell: WMTUserOperationVisualCell { + public let value: String +} + +public struct WMTUserOperationValueAttributeVisualCell: WMTUserOperationVisualCell { + public let header: String + public let defaultFormattedStringValue: String + public let style: String? + public let attribute: WMTOperationAttribute + public let cellTemplate: WMTTemplates.DetailTemplate.Section.Cell? +} + +public struct WMTUserOperationImageVisualCell: WMTUserOperationVisualCell { + public let urlThumbnail: URL + public let urlFull: URL? + public let style: String? + public let attribute: WMTOperationAttributeImage + public let cellTemplate: WMTTemplates.DetailTemplate.Section.Cell? + + private let downloader = ImageDownloader.shared +} + + + // MARK: WMTUserOperation List Visual preparation extension extension WMTUserOperation { - internal func prepareVisualListDetail() -> WMTUserOperationListVisual? { - guard let listTemplate = self.ui?.templates?.list else { - return nil - } + internal func prepareVisualListDetail() -> WMTUserOperationListVisual { + let listTemplate = self.ui?.templates?.list let attributes = self.formData.attributes + let headerAtrr = listTemplate?.header?.replacePlaceholders(from: attributes) - let headerAtrr = listTemplate.header?.replacePlaceholders(from: attributes) - - var title: String? - if let titleAttr = listTemplate.title?.replacePlaceholders(from: attributes) { - title = titleAttr - } else if !self.formData.message.isEmpty { - title = self.formData.title + 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) { - message = messageAttr - } else if !self.formData.message.isEmpty { - message = self.formData.message + + 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.first(where: { $0.label.id == imgAttr }) as? WMTOperationAttributeImage { - imageUrl = URL(string: imgAttrCell.thumbnailUrl) - } else { - imageUrl = 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, @@ -69,31 +160,39 @@ extension WMTUserOperation { } } -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 -} extension WMTUserOperationListVisual { - func downloadThumbnail(callback: @escaping (UIImage?) -> Void) { - if let thumbnailImageURL { - ImageDownloader.shared.downloadImage(at: thumbnailImageURL, ImageDownloader.Callback(callback: callback)) - } else { + public func downloadThumbnail(callback: @escaping (UIImage?) -> Void) { + + guard let url = thumbnailImageURL else { callback(nil) + return } + // Use ImageDownloader to download the image + downloader.downloadImage( + at: url, + ImageDownloader.Callback { img in + if let img { + callback(img) + } else { + callAgain(callback: callback) + } + } + ) + } + + private func callAgain(callback: @escaping (UIImage?) -> Void) { + DispatchQueue.global().asyncAfter(deadline: .now() + 1) { + self.downloadThumbnail(callback: callback) + } } } // MARK: WMTUserOperation Detail Visual preparation extension extension WMTUserOperation { - internal func prepareVisualDetail() -> WMTUserOperationVisual? { + internal func prepareVisualDetail() -> WMTUserOperationVisual { guard let detailTemplate = self.ui?.templates?.detail else { let attrs = self.formData.attributes @@ -151,54 +250,43 @@ extension WMTUserOperation { } } -public struct WMTUserOperationVisual { - public let sections: [WMTUserOperationVisualSection] -} - -public struct WMTUserOperationVisualSection { - public let style: String? - public let title: String? // not an id, actual value - public let cells: [WMTUserOperationVisualCell] - - public init(style: String? = nil, title: String? = nil, cells: [WMTUserOperationVisualCell]) { - self.style = style - self.title = title - self.cells = cells +extension WMTUserOperationImageVisualCell { + func downloadFull(callback: @escaping (UIImage?) -> Void) { + guard let url = urlFull else { + callback(nil) + return + } + + // Use ImageDownloader to download the image + downloader.downloadImage( + at: url, + ImageDownloader.Callback { img in + if let img { + callback(img) + } else { + callAgain(callback: callback) + } + } + ) } -} - -public protocol WMTUserOperationVisualCell { } - -public struct WMTUserOperationHeaderVisualCell: WMTUserOperationVisualCell { - public let value: String -} - -public struct WMTUserOperationMessageVisualCell: WMTUserOperationVisualCell { - public let value: String -} -public struct WMTUserOperationValueAttributeVisualCell: WMTUserOperationVisualCell { - public let header: String - public let defaultFormattedStringValue: String - public let style: String? - public let attribute: WMTOperationAttribute - public let cellTemplate: WMTTemplates.DetailTemplate.Section.Cell? -} - -public struct WMTUserOperationImageVisualCell: WMTUserOperationVisualCell { - public let urlThumbnail: URL - public let urlFull: URL? - public let style: String? - public let attribute: WMTOperationAttributeImage - public let cellTemplate: WMTTemplates.DetailTemplate.Section.Cell? -} - -extension WMTUserOperationImageVisualCell { - func downloadFull(callback: (Result) -> Void) { - // ImageDownloader.shared. .... + func downloadThumbnail(callback: @escaping (UIImage?) -> Void) { + downloader.downloadImage( + at: urlThumbnail, + ImageDownloader.Callback { img in + if let img { + callback(img) + } else { + callAgain(callback: callback) + } + } + ) } - func downloadThumbnail(callback: (Result) -> Void) { - // ImageDownloader.shared. .... + + private func callAgain(callback: @escaping (UIImage?) -> Void) { + DispatchQueue.global().asyncAfter(deadline: .now() + 1) { + self.downloadFull(callback: callback) + } } } From 588d4f30eeae69cc99b54157dc3aa44c7d32e40c Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Thu, 18 Jul 2024 14:24:38 +0200 Subject: [PATCH 14/29] Fix tests --- WultraMobileTokenSDKTests/OperationUIDataTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WultraMobileTokenSDKTests/OperationUIDataTests.swift b/WultraMobileTokenSDKTests/OperationUIDataTests.swift index 8de6a92..6957592 100644 --- a/WultraMobileTokenSDKTests/OperationUIDataTests.swift +++ b/WultraMobileTokenSDKTests/OperationUIDataTests.swift @@ -476,7 +476,7 @@ class OperationUIDataTests: XCTestCase { }, "detail": { "style": null, - "automaticHeaderSection": false, + "showTitleAndMessage": false, "sections": [ { "style": "MONEY", From 662a9199ef009015f40f6029a95a891a7b1746e3 Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Thu, 18 Jul 2024 14:26:37 +0200 Subject: [PATCH 15/29] Minor refactoring of pop functions --- .../WMTUserOperationVisualParser.swift | 65 ++++--------------- .../UserOperation/WMTUserOperation.swift | 5 +- 2 files changed, 17 insertions(+), 53 deletions(-) diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift index be362e9..0a2edd0 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift @@ -235,13 +235,13 @@ extension WMTUserOperation { var sections = [WMTUserOperationVisualSection]() if detailTemplate.showTitleAndMessage == false { - let dataSections = attrs.popSections(from: sectionsTemplate) + let dataSections = attrs.popCells(from: sectionsTemplate) sections.append(contentsOf: dataSections) sections.append(.init(cells: attrs.getRemainingCells())) return .init(sections: sections) } else { let headerSection = createHeaderVisual(style: detailTemplate.style) - let dataSection = attrs.popSections(from: sectionsTemplate) + let dataSection = attrs.popCells(from: sectionsTemplate) sections.append(headerSection) sections.append(contentsOf: dataSection) sections.append(.init(cells: attrs.getRemainingCells())) @@ -385,48 +385,11 @@ private extension Array where Element: WMTOperationAttribute { 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 popCells(from sections: [WMTTemplates.DetailTemplate.Section]) -> [WMTUserOperationVisualSection] { + return sections.map { popCells(from: $0) } } - mutating func popSection(from section: WMTTemplates.DetailTemplate.Section) -> WMTUserOperationVisualSection { + mutating func popCells(from section: WMTTemplates.DetailTemplate.Section) -> WMTUserOperationVisualSection { let sectionFilled = WMTUserOperationVisualSection( style: section.style, title: pop(id: section.title)?.label.value, @@ -436,7 +399,15 @@ private extension Array where Element: WMTOperationAttribute { } mutating func popCells(from section: WMTTemplates.DetailTemplate.Section) -> [WMTUserOperationVisualCell] { - return section.cells?.compactMap { createCellAndPopAttr(from: $0) } ?? [] + return section.cells?.compactMap { createCellFromTemplateCell($0) } ?? [] + } + + mutating func createCellFromTemplateCell(_ 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) } func getRemainingCells() -> [WMTUserOperationVisualCell] { @@ -448,14 +419,6 @@ private extension Array where Element: WMTOperationAttribute { } 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 diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift index 8a22742..f1a9760 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift @@ -57,8 +57,9 @@ open class WMTUserOperation: WMTOperation, Codable { /// Additional UI data to present /// /// Additional UI data such as Pre-Approval Screen or Post-Approval Screen should be presented. - public let ui = prepareTemplates(response: uiTemplates) -// public let ui: WMTOperationUIData? + //TODO: remove before merging +// public let ui = prepareTemplates(response: uiTemplates) + public let ui: WMTOperationUIData? /// Proximity Check Data to be passed when OTP is handed to the app public var proximityCheck: WMTProximityCheck? From 2c073abb3868baa12ac61787bd727446498c0d34 Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Thu, 18 Jul 2024 15:07:26 +0200 Subject: [PATCH 16/29] Divide code to separate files --- .../project.pbxproj | 16 +- .../TemplateParser/ImageDownloader.swift | 106 ++++ .../WMTUserOperationListVisual.swift | 204 +++++++ .../WMTUserOperationVisual.swift | 265 +++++++++ .../WMTUserOperationVisualParser.swift | 532 ------------------ .../UserOperation/WMTUserOperation.swift | 4 +- 6 files changed, 591 insertions(+), 536 deletions(-) create mode 100644 WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/ImageDownloader.swift create mode 100644 WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationListVisual.swift create mode 100644 WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisual.swift diff --git a/WultraMobileTokenSDK.xcodeproj/project.pbxproj b/WultraMobileTokenSDK.xcodeproj/project.pbxproj index 59a0c1c..015b270 100644 --- a/WultraMobileTokenSDK.xcodeproj/project.pbxproj +++ b/WultraMobileTokenSDK.xcodeproj/project.pbxproj @@ -73,7 +73,10 @@ EA6DDF1C29F807230011E234 /* OperationUIDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6DDF1B29F807230011E234 /* OperationUIDataTests.swift */; }; EA74F7B32C2561BB004340B9 /* WMTResultTexts.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA74F7B22C2561BB004340B9 /* WMTResultTexts.swift */; }; EA7A6E582B0E639800C1D4F4 /* WMTOperationDetailRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7A6E572B0E639800C1D4F4 /* WMTOperationDetailRequest.swift */; }; - EA951B502C412E43006C76B5 /* WMTUserOperationVisualParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA951B4F2C412E43006C76B5 /* WMTUserOperationVisualParser.swift */; }; + EA7EA22D2C494478000ECA41 /* WMTUserOperationVisualParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA951B4F2C412E43006C76B5 /* WMTUserOperationVisualParser.swift */; }; + EA7EA22E2C49447B000ECA41 /* WMTUserOperationListVisual.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA392BB62C4941CE00B6ADB7 /* WMTUserOperationListVisual.swift */; }; + EA7EA2302C494546000ECA41 /* WMTUserOperationVisual.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7EA22F2C494546000ECA41 /* WMTUserOperationVisual.swift */; }; + EA7EA2322C49480E000ECA41 /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7EA2312C49480E000ECA41 /* ImageDownloader.swift */; }; EA9795132C2C18450073E861 /* WMTTemplates.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9795122C2C18450073E861 /* WMTTemplates.swift */; }; EA9CE2BE2AEAA9FD00FE4E35 /* WMTProximityCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9CE2BD2AEAA9FD00FE4E35 /* WMTProximityCheck.swift */; }; EA9CE2C22AEBDB0D00FE4E35 /* WMTPACUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9CE2C12AEBDB0D00FE4E35 /* WMTPACUtils.swift */; }; @@ -155,6 +158,7 @@ DCE660D024CEBECA00870E53 /* IntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationTests.swift; sourceTree = ""; }; DCE660D224CEF56400870E53 /* IntegrationProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationProxy.swift; sourceTree = ""; }; EA294F3C29F6A07A00A0494E /* WMTOperationUIData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOperationUIData.swift; sourceTree = ""; }; + EA392BB62C4941CE00B6ADB7 /* WMTUserOperationListVisual.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTUserOperationListVisual.swift; sourceTree = ""; }; EA44366929F9294600DDEC1C /* WMTPostApprovaScreenReview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPostApprovaScreenReview.swift; sourceTree = ""; }; EA44366B29F9297100DDEC1C /* WMTPostApprovaScreenRedirect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPostApprovaScreenRedirect.swift; sourceTree = ""; }; EA44366D29F9298100DDEC1C /* WMTPostApprovaScreenGeneric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPostApprovaScreenGeneric.swift; sourceTree = ""; }; @@ -163,6 +167,8 @@ EA6DDF1B29F807230011E234 /* OperationUIDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationUIDataTests.swift; sourceTree = ""; }; EA74F7B22C2561BB004340B9 /* WMTResultTexts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTResultTexts.swift; sourceTree = ""; }; EA7A6E572B0E639800C1D4F4 /* WMTOperationDetailRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOperationDetailRequest.swift; sourceTree = ""; }; + EA7EA22F2C494546000ECA41 /* WMTUserOperationVisual.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTUserOperationVisual.swift; sourceTree = ""; }; + EA7EA2312C49480E000ECA41 /* ImageDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDownloader.swift; sourceTree = ""; }; EA951B4F2C412E43006C76B5 /* WMTUserOperationVisualParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTUserOperationVisualParser.swift; sourceTree = ""; }; EA9795122C2C18450073E861 /* WMTTemplates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTTemplates.swift; sourceTree = ""; }; EA9CE2BD2AEAA9FD00FE4E35 /* WMTProximityCheck.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WMTProximityCheck.swift; sourceTree = ""; }; @@ -465,6 +471,9 @@ isa = PBXGroup; children = ( EA951B4F2C412E43006C76B5 /* WMTUserOperationVisualParser.swift */, + EA392BB62C4941CE00B6ADB7 /* WMTUserOperationListVisual.swift */, + EA7EA22F2C494546000ECA41 /* WMTUserOperationVisual.swift */, + EA7EA2312C49480E000ECA41 /* ImageDownloader.swift */, ); path = TemplateParser; sourceTree = ""; @@ -619,10 +628,10 @@ buildActionMask = 2147483647; files = ( DC81D1C9244F38DB00F80CD6 /* WMTPushEndpoints.swift in Sources */, - EA951B502C412E43006C76B5 /* WMTUserOperationVisualParser.swift in Sources */, DC0268DF29965495000BB9FA /* WMTOperationListResponse.swift in Sources */, DC8CB202244DCBE2009DDAA3 /* WMTOperations.swift in Sources */, DC48803E292282FF00DB844B /* WMTInboxMessage.swift in Sources */, + EA7EA22E2C49447B000ECA41 /* WMTUserOperationListVisual.swift in Sources */, DCC5CCB52449F8E9004679AC /* WMTOperationAttributeAmount.swift in Sources */, DCC5CCD6244DBB7F004679AC /* WMTPushRegistrationData.swift in Sources */, DC3D0B392480F886000DC4D9 /* WMTLocalOperation.swift in Sources */, @@ -637,6 +646,7 @@ DC6E52D6259C964600FC25BE /* WMTOperationExpirationWatcher.swift in Sources */, DCC5CCDA244DBBE2004679AC /* WMTRejectionData.swift in Sources */, EA9795132C2C18450073E861 /* WMTTemplates.swift in Sources */, + EA7EA22D2C494478000ECA41 /* WMTUserOperationVisualParser.swift in Sources */, DC48803F292282FF00DB844B /* WMTInboxMessageDetail.swift in Sources */, EA6DDF0F29F8036B0011E234 /* WMTPreApprovalScreen.swift in Sources */, DCAB7BC824580B4C0006989D /* WMTQROperationParser.swift in Sources */, @@ -663,8 +673,10 @@ DCC3420424E3DB310045D27D /* WMTPushParser.swift in Sources */, BFEEB20529379C700047941D /* WMTInboxGetMessageDetail.swift in Sources */, DCE5EAB026BD81150061861A /* WMTOperationHistoryEntry.swift in Sources */, + EA7EA2322C49480E000ECA41 /* ImageDownloader.swift in Sources */, EACAF7B02A126B7D0021CA54 /* WMTJsonValue.swift in Sources */, DCAB7BCA24580BAC0006989D /* WMTQROperation.swift in Sources */, + EA7EA2302C494546000ECA41 /* WMTUserOperationVisual.swift in Sources */, DCC5CCBF2449F981004679AC /* WMTOperationAttributePartyInfo.swift in Sources */, DC81D1CB244F451E00F80CD6 /* WMTPushImpl.swift in Sources */, EA9CE2BE2AEAA9FD00FE4E35 /* WMTProximityCheck.swift in Sources */, diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/ImageDownloader.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/ImageDownloader.swift new file mode 100644 index 0000000..aa1b533 --- /dev/null +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/ImageDownloader.swift @@ -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 + + 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) + } + } + } + } +} diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationListVisual.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationListVisual.swift new file mode 100644 index 0000000..2506c8a --- /dev/null +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationListVisual.swift @@ -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 + } +} diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisual.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisual.swift new file mode 100644 index 0000000..d8be2a1 --- /dev/null +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisual.swift @@ -0,0 +1,265 @@ +// +// 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 WMTUserOperationVisual { + public let sections: [WMTUserOperationVisualSection] +} + +public struct WMTUserOperationVisualSection { + public let style: String? + public let title: String? // not an id, actual value + public let cells: [WMTUserOperationVisualCell] + + public 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 { + public let value: String +} + +public struct WMTUserOperationMessageVisualCell: WMTUserOperationVisualCell { + public let value: String +} + +public struct WMTUserOperationValueAttributeVisualCell: WMTUserOperationVisualCell { + public let header: String + public let defaultFormattedStringValue: String + public let style: String? + public let attribute: WMTOperationAttribute + public let cellTemplate: WMTTemplates.DetailTemplate.Section.Cell? +} + +public struct WMTUserOperationImageVisualCell: WMTUserOperationVisualCell { + public let urlThumbnail: URL + public let urlFull: URL? + public let style: String? + public let attribute: WMTOperationAttributeImage + public let cellTemplate: WMTTemplates.DetailTemplate.Section.Cell? + + private let downloader = ImageDownloader.shared +} + +// MARK: WMTUserOperation Detail Visual preparation extension +extension WMTUserOperation { + + internal func prepareVisualDetail() -> WMTUserOperationVisual { + + guard let detailTemplate = self.ui?.templates?.detail else { + let 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 + private 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] + ) + } + + private 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 == false { + let dataSections = attrs.popCells(from: sectionsTemplate) + sections.append(contentsOf: dataSections) + sections.append(.init(cells: attrs.getRemainingCells())) + return .init(sections: sections) + } else { + let headerSection = createHeaderVisual(style: detailTemplate.style) + let dataSection = attrs.popCells(from: sectionsTemplate) + sections.append(headerSection) + sections.append(contentsOf: dataSection) + sections.append(.init(cells: attrs.getRemainingCells())) + return .init(sections: sections) + } + } +} + +extension WMTUserOperationImageVisualCell { + func downloadFull(callback: @escaping (UIImage?) -> Void) { + guard let url = urlFull else { + callback(nil) + return + } + + // Use ImageDownloader to download the image + downloader.downloadImage( + at: url, + ImageDownloader.Callback { img in + if let img { + callback(img) + } else { + callAgain(callback: callback) + } + } + ) + } + + func downloadThumbnail(callback: @escaping (UIImage?) -> Void) { + downloader.downloadImage( + at: urlThumbnail, + ImageDownloader.Callback { img in + if let img { + callback(img) + } else { + callAgain(callback: callback) + } + } + ) + } + + private func callAgain(callback: @escaping (UIImage?) -> Void) { + DispatchQueue.global().asyncAfter(deadline: .now() + 1) { + self.downloadFull(callback: callback) + } + } +} + +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 popCells(from sections: [WMTTemplates.DetailTemplate.Section]) -> [WMTUserOperationVisualSection] { + return sections.map { popCells(from: $0) } + } + + mutating func popCells(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 { createCellFromTemplateCell($0) } ?? [] + } + + mutating func createCellFromTemplateCell(_ 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) + } + + func getRemainingCells() -> [WMTUserOperationVisualCell] { + var cells = [WMTUserOperationVisualCell]() + for attr in self { + if let cell = createCell(from: attr) { + cells.append(cell) + } + } + return cells + } + + private func createCell(from attr: WMTOperationAttribute, templateCell: WMTTemplates.DetailTemplate.Section.Cell? = nil) -> WMTUserOperationVisualCell? { + let value: String + + switch attr.type { + case .amount: + guard let amount = attr as? WMTOperationAttributeAmount else { return nil } + value = amount.valueFormatted ?? "\(amount.amountFormatted) \(amount.currencyFormatted)" + case .amountConversion: + guard let conversion = attr as? WMTOperationAttributeAmountConversion else { return nil } + 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: + guard let keyValue = attr as? WMTOperationAttributeKeyValue else { return nil} + value = keyValue.value + case .note: + guard let note = attr as? WMTOperationAttributeNote else { return nil } + value = note.note + case .image: + guard let image = attr as? WMTOperationAttributeImage else { return nil } + return WMTUserOperationImageVisualCell( + urlThumbnail: URL(string: image.thumbnailUrl) ?? URL(string: "error")!, + urlFull: image.originalUrl != nil ? URL(string: image.originalUrl!) : nil, + style: templateCell?.style, + attribute: image, + cellTemplate: templateCell + ) + case .heading: + value = "" + case .partyInfo, .unknown: + D.warning("Using unsuported Attribute in Templates") + value = "" + } + + return WMTUserOperationValueAttributeVisualCell( + header: attr.label.value, + defaultFormattedStringValue: value, + style: templateCell?.style, + attribute: attr, + cellTemplate: templateCell + ) + } +} diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift index 0a2edd0..577c6f4 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift @@ -25,535 +25,3 @@ public class WMTUserOperationVisualParser { return operation.prepareVisualDetail() } } - -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 - - 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 struct WMTUserOperationVisual { - public let sections: [WMTUserOperationVisualSection] -} - -public struct WMTUserOperationVisualSection { - public let style: String? - public let title: String? // not an id, actual value - public let cells: [WMTUserOperationVisualCell] - - public 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 { - public let value: String -} - -public struct WMTUserOperationMessageVisualCell: WMTUserOperationVisualCell { - public let value: String -} - -public struct WMTUserOperationValueAttributeVisualCell: WMTUserOperationVisualCell { - public let header: String - public let defaultFormattedStringValue: String - public let style: String? - public let attribute: WMTOperationAttribute - public let cellTemplate: WMTTemplates.DetailTemplate.Section.Cell? -} - -public struct WMTUserOperationImageVisualCell: WMTUserOperationVisualCell { - public let urlThumbnail: URL - public let urlFull: URL? - public let style: String? - public let attribute: WMTOperationAttributeImage - public let cellTemplate: WMTTemplates.DetailTemplate.Section.Cell? - - private let downloader = ImageDownloader.shared -} - - - -// 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 - ) - } -} - - -extension WMTUserOperationListVisual { - public func downloadThumbnail(callback: @escaping (UIImage?) -> Void) { - - guard let url = thumbnailImageURL else { - callback(nil) - return - } - - // Use ImageDownloader to download the image - downloader.downloadImage( - at: url, - ImageDownloader.Callback { img in - if let img { - callback(img) - } else { - callAgain(callback: callback) - } - } - ) - } - - private func callAgain(callback: @escaping (UIImage?) -> Void) { - DispatchQueue.global().asyncAfter(deadline: .now() + 1) { - self.downloadThumbnail(callback: callback) - } - } -} - -// MARK: WMTUserOperation Detail Visual preparation extension -extension WMTUserOperation { - - internal func prepareVisualDetail() -> WMTUserOperationVisual { - - guard let detailTemplate = self.ui?.templates?.detail else { - let 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 - private 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] - ) - } - - private 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 == false { - let dataSections = attrs.popCells(from: sectionsTemplate) - sections.append(contentsOf: dataSections) - sections.append(.init(cells: attrs.getRemainingCells())) - return .init(sections: sections) - } else { - let headerSection = createHeaderVisual(style: detailTemplate.style) - let dataSection = attrs.popCells(from: sectionsTemplate) - sections.append(headerSection) - sections.append(contentsOf: dataSection) - sections.append(.init(cells: attrs.getRemainingCells())) - return .init(sections: sections) - } - } -} - -extension WMTUserOperationImageVisualCell { - func downloadFull(callback: @escaping (UIImage?) -> Void) { - guard let url = urlFull else { - callback(nil) - return - } - - // Use ImageDownloader to download the image - downloader.downloadImage( - at: url, - ImageDownloader.Callback { img in - if let img { - callback(img) - } else { - callAgain(callback: callback) - } - } - ) - } - - func downloadThumbnail(callback: @escaping (UIImage?) -> Void) { - downloader.downloadImage( - at: urlThumbnail, - ImageDownloader.Callback { img in - if let img { - callback(img) - } else { - callAgain(callback: callback) - } - } - ) - } - - private func callAgain(callback: @escaping (UIImage?) -> Void) { - DispatchQueue.global().asyncAfter(deadline: .now() + 1) { - self.downloadFull(callback: callback) - } - } -} - -// 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: - 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 - } -} - -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 popCells(from sections: [WMTTemplates.DetailTemplate.Section]) -> [WMTUserOperationVisualSection] { - return sections.map { popCells(from: $0) } - } - - mutating func popCells(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 { createCellFromTemplateCell($0) } ?? [] - } - - mutating func createCellFromTemplateCell(_ 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) - } - - func getRemainingCells() -> [WMTUserOperationVisualCell] { - var cells = [WMTUserOperationVisualCell]() - for attr in self { - if let cell = createCell(from: attr) { - cells.append(cell) - } - } - return cells - } - - private func createCell(from attr: WMTOperationAttribute, templateCell: WMTTemplates.DetailTemplate.Section.Cell? = nil) -> WMTUserOperationVisualCell? { - let value: String - - switch attr.type { - case .amount: - guard let amount = attr as? WMTOperationAttributeAmount else { return nil } - value = amount.valueFormatted ?? "\(amount.amountFormatted) \(amount.currencyFormatted)" - case .amountConversion: - guard let conversion = attr as? WMTOperationAttributeAmountConversion else { return nil } - 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: - guard let keyValue = attr as? WMTOperationAttributeKeyValue else { return nil} - value = keyValue.value - case .note: - guard let note = attr as? WMTOperationAttributeNote else { return nil } - value = note.note - case .image: - guard let image = attr as? WMTOperationAttributeImage else { return nil } - return WMTUserOperationImageVisualCell( - urlThumbnail: URL(string: image.thumbnailUrl) ?? URL(string: "error")!, - urlFull: image.originalUrl != nil ? URL(string: image.originalUrl!) : nil, - style: templateCell?.style, - attribute: image, - cellTemplate: templateCell - ) - case .heading: - value = "" - case .partyInfo, .unknown: - D.warning("Using unsuported Attribute in Templates") - value = "" - } - - return WMTUserOperationValueAttributeVisualCell( - header: attr.label.value, - defaultFormattedStringValue: value, - style: templateCell?.style, - attribute: attr, - cellTemplate: templateCell - ) - } -} - - -/// Simple image URL downloader with a simple cache implementation -public 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 - - 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) - } - } - } - } -} diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift index f1a9760..b1f17f7 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift @@ -57,9 +57,9 @@ open class WMTUserOperation: WMTOperation, Codable { /// Additional UI data to present /// /// Additional UI data such as Pre-Approval Screen or Post-Approval Screen should be presented. - //TODO: remove before merging -// public let ui = prepareTemplates(response: uiTemplates) public let ui: WMTOperationUIData? + // TODO: remove before merging + /// public let ui = prepareTemplates(response: uiTemplates) /// Proximity Check Data to be passed when OTP is handed to the app public var proximityCheck: WMTProximityCheck? From 84658a049575af2b3dcfc2377ff305d19b527d3d Mon Sep 17 00:00:00 2001 From: Jan Kobersky Date: Thu, 18 Jul 2024 15:17:11 +0200 Subject: [PATCH 17/29] Fixed podspec --- WultraMobileTokenSDK.podspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WultraMobileTokenSDK.podspec b/WultraMobileTokenSDK.podspec index 49fdd27..8a8d534 100644 --- a/WultraMobileTokenSDK.podspec +++ b/WultraMobileTokenSDK.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'WultraMobileTokenSDK' - s.version = '1.8.0' + s.version = '1.12.0' # Metadata s.license = { :type => 'Apache License, Version 2.0', :file => 'LICENSE' } s.summary = 'High level PowerAuth based library written in swift' @@ -19,7 +19,7 @@ Pod::Spec.new do |s| s.subspec 'Common' do |sub| sub.source_files = 'WultraMobileTokenSDK/Common/**/*.swift' sub.dependency 'PowerAuth2', '~> 1.8.0' - sub.dependency 'WultraPowerAuthNetworking', '~> 1.3.0' + sub.dependency 'WultraPowerAuthNetworking', '~> 1.4.0' end # 'Operations' subspec From a62bba4d1e94dc8ef8b382740a29db0f9dc3853b Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Thu, 18 Jul 2024 15:38:47 +0200 Subject: [PATCH 18/29] Fix issues after incorrect merge --- Cartfile | 2 +- Cartfile.resolved | 2 +- Deploy/WultraMobileTokenSDK.podspec | 2 +- Package.resolved | 23 ---- WultraMobileTokenSDK/Common/WMTLogger.swift | 115 ++++++++++++++---- .../UserOperation/WMTOperationFormData.swift | 2 +- .../Service/WMTOperationsImpl.swift | 4 +- .../Utils/WMTOperationExpirationWatcher.swift | 12 +- .../IntegrationProxy.swift | 2 +- .../IntegrationTests.swift | 2 +- docs/Changelog.md | 14 ++- docs/Logging.md | 27 ++-- 12 files changed, 125 insertions(+), 82 deletions(-) delete mode 100644 Package.resolved diff --git a/Cartfile b/Cartfile index 40b13dc..f5a715d 100644 --- a/Cartfile +++ b/Cartfile @@ -1 +1 @@ -github "wultra/networking-apple" "1.3.0" +github "wultra/networking-apple" "1.4.0" diff --git a/Cartfile.resolved b/Cartfile.resolved index 72d5757..28d9b28 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,3 +1,3 @@ binary "https://raw.githubusercontent.com/wultra/powerauth-mobile-sdk-spm/1.8.0/PowerAuth2.json" "1.8.0" binary "https://raw.githubusercontent.com/wultra/powerauth-mobile-sdk-spm/1.8.0/PowerAuthCore.json" "1.8.0" -github "wultra/networking-apple" "1.3.0" +github "wultra/networking-apple" "1.4.0" diff --git a/Deploy/WultraMobileTokenSDK.podspec b/Deploy/WultraMobileTokenSDK.podspec index df2ea20..34c1d92 100644 --- a/Deploy/WultraMobileTokenSDK.podspec +++ b/Deploy/WultraMobileTokenSDK.podspec @@ -19,7 +19,7 @@ Pod::Spec.new do |s| s.subspec 'Common' do |sub| sub.source_files = 'WultraMobileTokenSDK/Common/**/*.swift' sub.dependency 'PowerAuth2', '~> 1.8.0' - sub.dependency 'WultraPowerAuthNetworking', '~> 1.3.0' + sub.dependency 'WultraPowerAuthNetworking', '~> 1.4.0' end # 'Operations' subspec diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index 1550cab..0000000 --- a/Package.resolved +++ /dev/null @@ -1,23 +0,0 @@ -{ - "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/Common/WMTLogger.swift b/WultraMobileTokenSDK/Common/WMTLogger.swift index ab36922..ad8bd23 100644 --- a/WultraMobileTokenSDK/Common/WMTLogger.swift +++ b/WultraMobileTokenSDK/Common/WMTLogger.swift @@ -16,49 +16,67 @@ import Foundation -/// WMTLogger provides simple logging facility available for DEBUG build of the library. +/// WMTLogger provides simple logging facility. +/// +/// Note that HTTP logs are managed by the underlying Networking library (via `WPNLogger` class). public class WMTLogger { - /// Defines verbose level for this simple debugging facility. + /// Verbose level of the logger. public enum VerboseLevel: Int { /// Silences all messages. case off = 0 - /// Only errors will be printed to the debug console. + /// Only errors will be logged. case errors = 1 - /// Errors and warnings will be printed to the debug console. + /// Errors and warnings will be logged. case warnings = 2 - /// All messages will be printed to the debug console. - case all = 3 + /// Error, warning and info messages will be logged. + case info = 3 + /// All messages will logged - including debug messages + case debug = 4 } - /// Current verbose level. Note that value is ignored for non-DEBUG builds. + /// Logger delegate + public static weak var delegate: WMTLoggerDelegate? + + /// Current verbose level. `warnings` by default public static var verboseLevel: VerboseLevel = .warnings - /// Prints simple message to the debug console. - static func print(_ message: @autoclosure () -> String) { - #if DEBUG || WMT_ENABLE_LOGGING - if verboseLevel == .all { - Swift.print("[WMT] \(message())") - } - #endif + /// Prints simple message to the system console. + static func debug(_ message: @autoclosure () -> String) { + log(message(), level: .debug) + } + + /// Prints simple message to the system console. + static func info(_ message: @autoclosure () -> String) { + log(message(), level: .info) } - /// Prints warning message to the debug console. + /// Prints warning message to the system console. static func warning(_ message: @autoclosure () -> String) { - #if DEBUG || WMT_ENABLE_LOGGING - if verboseLevel.rawValue >= VerboseLevel.warnings.rawValue { - Swift.print("[WMT] WARNING: \(message())") - } - #endif + log(message(), level: .warning) } - /// Prints error message to the debug console. + /// Prints error message to the system console. static func error(_ message: @autoclosure () -> String) { - #if DEBUG || WMT_ENABLE_LOGGING - if verboseLevel != .off { - Swift.print("[WMT] ERROR: \(message())") + log(message(), level: .error) + } + + private static func log(_ message: @autoclosure () -> String, level: WMTLogLevel) { + let levelAllowed = level.minVerboseLevel.rawValue <= verboseLevel.rawValue + let forceReport = delegate?.wmtFollowVerboseLevel == false + guard levelAllowed || forceReport else { + // not logging + return + } + + let msg = message() + + if levelAllowed { + print("[WMT:\(level.logName)] \(msg)") + } + if levelAllowed || forceReport { + delegate?.wmtLog(message: msg, logLevel: level) } - #endif } #if DEBUG @@ -84,4 +102,51 @@ public class WMTLogger { #endif } +/// Delegate that can further process logs from the library +public protocol WMTLoggerDelegate: AnyObject { + + /// If the delegate should follow selected verbosity level. + /// + /// When set to true, then (for example) if `errors` is selected as a `verboseLevel`, only `error` logLevel will be called. + /// When set to false, all methods might be called no matter the selected `verboseLevel`. + var wmtFollowVerboseLevel: Bool { get } + + /// Log was recorded + /// - Parameters: + /// - message: Message of the log + /// - logLevel: Log level + func wmtLog(message: String, logLevel: WMTLogLevel) +} + +/// Level of the log +public enum WMTLogLevel { + /// Debug logs. Might contain sensitive data like body of the request etc. + /// You should only use this level during development. + case debug + /// Regular library logic logs + case info + /// Non-critical warning + case warning + /// Error happened + case error + + fileprivate var minVerboseLevel: WMTLogger.VerboseLevel { + return switch self { + case .debug: .debug + case .info: .info + case .warning: .warnings + case .error: .errors + } + } + + fileprivate var logName: String { + return switch self { + case .debug: "DEBUG" + case .info: "INFO" + case .warning: "WARNING" + case .error: "ERROR" + } + } +} + internal typealias D = WMTLogger diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationFormData.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationFormData.swift index e4ad53c..b3b84db 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationFormData.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationFormData.swift @@ -59,7 +59,7 @@ public class WMTOperationFormData: Codable { } } } catch { - D.print("No attributes in WMTOperationFormData: \(error)") + D.error("No attributes in WMTOperationFormData: \(error)") } attributes = operationAttributes diff --git a/WultraMobileTokenSDK/Operations/Service/WMTOperationsImpl.swift b/WultraMobileTokenSDK/Operations/Service/WMTOperationsImpl.swift index 7b16cfd..5aed671 100644 --- a/WultraMobileTokenSDK/Operations/Service/WMTOperationsImpl.swift +++ b/WultraMobileTokenSDK/Operations/Service/WMTOperationsImpl.swift @@ -374,7 +374,7 @@ class WMTOperationsImpl: WMTOperations, WMTService { adjustedInterval = interval } - D.print("Operations polling started with \(adjustedInterval) seconds interval") + D.info("Operations polling started with \(adjustedInterval) seconds interval") pollingTimer = Timer.scheduledTimer(withTimeInterval: adjustedInterval, repeats: true) { [weak self] _ in self?.refreshOperations() } @@ -393,7 +393,7 @@ class WMTOperationsImpl: WMTOperations, WMTService { } pollingTimer = nil timer.invalidate() - D.print("Operations polling stopped") + D.info("Operations polling stopped") } // MARK: - private functions diff --git a/WultraMobileTokenSDK/Operations/Utils/WMTOperationExpirationWatcher.swift b/WultraMobileTokenSDK/Operations/Utils/WMTOperationExpirationWatcher.swift index deac2b5..d036410 100644 --- a/WultraMobileTokenSDK/Operations/Utils/WMTOperationExpirationWatcher.swift +++ b/WultraMobileTokenSDK/Operations/Utils/WMTOperationExpirationWatcher.swift @@ -134,7 +134,7 @@ public class WMTOperationExpirationWatcher { if opsToWatch.isEmpty { D.warning("WMTOperationExpirationWatcher: All operations are already watched") } else { - D.print("WMTOperationExpirationWatcher: Adding \(opsToWatch.count) operation to watch.") + D.debug("WMTOperationExpirationWatcher: Adding \(opsToWatch.count) operation to watch.") self.operationsToWatch.append(contentsOf: opsToWatch) self.prepareTimer() } @@ -175,10 +175,10 @@ public class WMTOperationExpirationWatcher { // when nil is provided, we consider it as "stop all" if let operations = operations { self.operationsToWatch.removeAll(where: { current in operations.contains(where: { toRemove in toRemove.equals(other: current) }) }) - D.print("WMTOperationExpirationWatcher: Stoped watching \(operations.count) operations.") + D.debug("WMTOperationExpirationWatcher: Stoped watching \(operations.count) operations.") } else { self.operationsToWatch.removeAll() - D.print("WMTOperationExpirationWatcher: Stoped watching all operations.") + D.debug("WMTOperationExpirationWatcher: Stoped watching all operations.") } self.prepareTimer() } @@ -194,7 +194,7 @@ public class WMTOperationExpirationWatcher { timer = nil guard operationsToWatch.isEmpty == false else { - D.print("WMTOperationExpirationWatcher: No operations to watch.") + D.debug("WMTOperationExpirationWatcher: No operations to watch.") return } @@ -211,7 +211,7 @@ public class WMTOperationExpirationWatcher { // The 0.1 addition is a correction of the Timer class which can fire slightly (in order of 0.000x seconds) earlier than scheduled. let interval = max(5, firstOp.operationExpires.timeIntervalSince1970 - self.currentDateProvider.currentDate.timeIntervalSince1970) + 0.1 - D.print("WMTOperationExpirationWatcher: Scheduling operation expire check in \(Int(interval)) seconds.") + D.debug("WMTOperationExpirationWatcher: Scheduling operation expire check in \(Int(interval)) seconds.") self.timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { [weak self] _ in guard let self = self else { @@ -230,7 +230,7 @@ public class WMTOperationExpirationWatcher { self.operationsToWatch.removeAll(where: { $0.isExpired(currentDate) }) self.prepareTimer() DispatchQueue.main.async { - D.print("WMTOperationExpirationWatcher: Reporting \(expiredOps.count) expired operations.") + D.info("WMTOperationExpirationWatcher: Reporting \(expiredOps.count) expired operations.") self.delegate?.operationsExpired(expiredOps) } } diff --git a/WultraMobileTokenSDKTests/IntegrationProxy.swift b/WultraMobileTokenSDKTests/IntegrationProxy.swift index e8bffc5..b0497df 100644 --- a/WultraMobileTokenSDKTests/IntegrationProxy.swift +++ b/WultraMobileTokenSDKTests/IntegrationProxy.swift @@ -31,7 +31,7 @@ class IntegrationProxy { typealias Callback = (_ error: String?) -> Void func prepareActivation(pin: String, callback: @escaping Callback) { - WPNLogger.verboseLevel = .all + WPNLogger.verboseLevel = .debug guard let configPath = Bundle.init(for: IntegrationProxy.self).path(forResource: "config", ofType: "json", inDirectory: "Configs") else { callback("Config file config.json is not present.") return diff --git a/WultraMobileTokenSDKTests/IntegrationTests.swift b/WultraMobileTokenSDKTests/IntegrationTests.swift index b3af71b..689aa0e 100644 --- a/WultraMobileTokenSDKTests/IntegrationTests.swift +++ b/WultraMobileTokenSDKTests/IntegrationTests.swift @@ -35,7 +35,7 @@ class IntegrationTests: XCTestCase { override func setUp() { super.setUp() - WMTLogger.verboseLevel = .all + WMTLogger.verboseLevel = .debug proxy = IntegrationProxy() let exp = XCTestExpectation(description: "setup expectation") diff --git a/docs/Changelog.md b/docs/Changelog.md index 294970c..3ffb736 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -1,11 +1,15 @@ # Changelog -## 1.11.0 (TBA) +## 1.11.1 (July 2024) -- Added WMTTemplates to WMTOperationUIData [(#162)](https://github.com/wultra/mtoken-sdk-ios/pull/162) -- Added resultTexts to UserOperation [(#160)](https://github.com/wultra/mtoken-sdk-ios/pull/160) -- Extended PushParser to support parsing of inbox notifications [(#158)](https://github.com/wultra/mtoken-sdk-ios/pull/158) -- Added statusReason to UserOperation [(#156)](https://github.com/wultra/mtoken-sdk-ios/pull/156) +- Dependency `networking-apple` is now required in version `1.4.x` + +## 1.11.0 (July 2024) + +- Added `resultTexts` to the `UserOperation` [(#160)](https://github.com/wultra/mtoken-sdk-ios/pull/160) +- Extended `PushParser` to support parsing of inbox notifications [(#158)](https://github.com/wultra/mtoken-sdk-ios/pull/158) +- Added `statusReason` to the `UserOperation` [(#156)](https://github.com/wultra/mtoken-sdk-ios/pull/156) +- Improved logging options [(#164)](https://github.com/wultra/mtoken-sdk-ios/pull/164) ## 1.10.0 (Apr 18, 2024) diff --git a/docs/Logging.md b/docs/Logging.md index 1312278..a0b942e 100644 --- a/docs/Logging.md +++ b/docs/Logging.md @@ -1,26 +1,23 @@ # Logging -For logging purposes, WMT uses `WMTLogger` class that prints to the console. +You can set up logging for the library using the `WMTLogger` class. -Note that logging to the console is available only when the library is compiled in the `Debug` mode or with `WMT_ENABLE_LOGGING` Swift compile condition. +Networking traffic is logged in its own logger described in the [networking library documentation](https://github.com/wultra/networking-apple). ### Verbosity Level -You can limit the amount of logged information via `verboseLevel` property. +You can limit the amount of logged information via the `verboseLevel` property. -| Level | Description | -| --- | --- | -| `off` | Silences all messages. | -| `errors` | Only errors will be printed to the debug console. | -| `warnings` _(default)_ | Errors and warnings will be printed to the debug console. | -| `all` | All messages will be printed to the debug console. | +| Level | Description | +| ---------------------- | ------------------------------------------------- | +| `off` | Silences all messages. | +| `errors` | Only errors will be logged. | +| `warnings` _(default)_ | Errors and warnings will be logged. | +| `info` | Error, warning and info messages will be logged. | +| `debug` | All messages will be logged. | -Example configuration: +### Logger Delegate -```swift -import WultraMobileTokenSDK - -WMTLogger.verboseLevel = .all -``` \ No newline at end of file +In case you want to process logs on your own (for example log into a file or some cloud service), you can set `WMTLogger.delegate`. From 7a227fa78735a5d25aee934caecc75b50f96fdaa Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Tue, 23 Jul 2024 15:34:49 +0200 Subject: [PATCH 19/29] Remove image downloader, rename parsers, add docs --- .../project.pbxproj | 28 ++- .../TemplateParser/ImageDownloader.swift | 106 ---------- ...al.swift => WMTTemplateDetailVisual.swift} | 195 +++++++++++++----- ...sual.swift => WMTTemplateListVisual.swift} | 48 ++--- .../WMTTemplateVisualParser.swift | 38 ++++ .../WMTUserOperationVisualParser.swift | 27 --- 6 files changed, 207 insertions(+), 235 deletions(-) delete mode 100644 WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/ImageDownloader.swift rename WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/{WMTUserOperationVisual.swift => WMTTemplateDetailVisual.swift} (57%) rename WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/{WMTUserOperationListVisual.swift => WMTTemplateListVisual.swift} (87%) create mode 100644 WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTTemplateVisualParser.swift delete mode 100644 WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift diff --git a/WultraMobileTokenSDK.xcodeproj/project.pbxproj b/WultraMobileTokenSDK.xcodeproj/project.pbxproj index 015b270..1ac4621 100644 --- a/WultraMobileTokenSDK.xcodeproj/project.pbxproj +++ b/WultraMobileTokenSDK.xcodeproj/project.pbxproj @@ -73,10 +73,9 @@ EA6DDF1C29F807230011E234 /* OperationUIDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6DDF1B29F807230011E234 /* OperationUIDataTests.swift */; }; EA74F7B32C2561BB004340B9 /* WMTResultTexts.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA74F7B22C2561BB004340B9 /* WMTResultTexts.swift */; }; EA7A6E582B0E639800C1D4F4 /* WMTOperationDetailRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7A6E572B0E639800C1D4F4 /* WMTOperationDetailRequest.swift */; }; - EA7EA22D2C494478000ECA41 /* WMTUserOperationVisualParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA951B4F2C412E43006C76B5 /* WMTUserOperationVisualParser.swift */; }; - EA7EA22E2C49447B000ECA41 /* WMTUserOperationListVisual.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA392BB62C4941CE00B6ADB7 /* WMTUserOperationListVisual.swift */; }; - EA7EA2302C494546000ECA41 /* WMTUserOperationVisual.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7EA22F2C494546000ECA41 /* WMTUserOperationVisual.swift */; }; - EA7EA2322C49480E000ECA41 /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7EA2312C49480E000ECA41 /* ImageDownloader.swift */; }; + EA7EA22D2C494478000ECA41 /* WMTTemplateVisualParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA951B4F2C412E43006C76B5 /* WMTTemplateVisualParser.swift */; }; + EA7EA22E2C49447B000ECA41 /* WMTTemplateListVisual.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA392BB62C4941CE00B6ADB7 /* WMTTemplateListVisual.swift */; }; + EA7EA2302C494546000ECA41 /* WMTTemplateDetailVisual.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7EA22F2C494546000ECA41 /* WMTTemplateDetailVisual.swift */; }; EA9795132C2C18450073E861 /* WMTTemplates.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9795122C2C18450073E861 /* WMTTemplates.swift */; }; EA9CE2BE2AEAA9FD00FE4E35 /* WMTProximityCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9CE2BD2AEAA9FD00FE4E35 /* WMTProximityCheck.swift */; }; EA9CE2C22AEBDB0D00FE4E35 /* WMTPACUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9CE2C12AEBDB0D00FE4E35 /* WMTPACUtils.swift */; }; @@ -158,7 +157,7 @@ DCE660D024CEBECA00870E53 /* IntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationTests.swift; sourceTree = ""; }; DCE660D224CEF56400870E53 /* IntegrationProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrationProxy.swift; sourceTree = ""; }; EA294F3C29F6A07A00A0494E /* WMTOperationUIData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOperationUIData.swift; sourceTree = ""; }; - EA392BB62C4941CE00B6ADB7 /* WMTUserOperationListVisual.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTUserOperationListVisual.swift; sourceTree = ""; }; + EA392BB62C4941CE00B6ADB7 /* WMTTemplateListVisual.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTTemplateListVisual.swift; sourceTree = ""; }; EA44366929F9294600DDEC1C /* WMTPostApprovaScreenReview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPostApprovaScreenReview.swift; sourceTree = ""; }; EA44366B29F9297100DDEC1C /* WMTPostApprovaScreenRedirect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPostApprovaScreenRedirect.swift; sourceTree = ""; }; EA44366D29F9298100DDEC1C /* WMTPostApprovaScreenGeneric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPostApprovaScreenGeneric.swift; sourceTree = ""; }; @@ -167,9 +166,8 @@ EA6DDF1B29F807230011E234 /* OperationUIDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationUIDataTests.swift; sourceTree = ""; }; EA74F7B22C2561BB004340B9 /* WMTResultTexts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTResultTexts.swift; sourceTree = ""; }; EA7A6E572B0E639800C1D4F4 /* WMTOperationDetailRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOperationDetailRequest.swift; sourceTree = ""; }; - EA7EA22F2C494546000ECA41 /* WMTUserOperationVisual.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTUserOperationVisual.swift; sourceTree = ""; }; - EA7EA2312C49480E000ECA41 /* ImageDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDownloader.swift; sourceTree = ""; }; - EA951B4F2C412E43006C76B5 /* WMTUserOperationVisualParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTUserOperationVisualParser.swift; sourceTree = ""; }; + EA7EA22F2C494546000ECA41 /* WMTTemplateDetailVisual.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTTemplateDetailVisual.swift; sourceTree = ""; }; + EA951B4F2C412E43006C76B5 /* WMTTemplateVisualParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTTemplateVisualParser.swift; sourceTree = ""; }; EA9795122C2C18450073E861 /* WMTTemplates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTTemplates.swift; sourceTree = ""; }; EA9CE2BD2AEAA9FD00FE4E35 /* WMTProximityCheck.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WMTProximityCheck.swift; sourceTree = ""; }; EA9CE2C12AEBDB0D00FE4E35 /* WMTPACUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WMTPACUtils.swift; sourceTree = ""; }; @@ -470,10 +468,9 @@ EA951B512C412F84006C76B5 /* TemplateParser */ = { isa = PBXGroup; children = ( - EA951B4F2C412E43006C76B5 /* WMTUserOperationVisualParser.swift */, - EA392BB62C4941CE00B6ADB7 /* WMTUserOperationListVisual.swift */, - EA7EA22F2C494546000ECA41 /* WMTUserOperationVisual.swift */, - EA7EA2312C49480E000ECA41 /* ImageDownloader.swift */, + EA951B4F2C412E43006C76B5 /* WMTTemplateVisualParser.swift */, + EA392BB62C4941CE00B6ADB7 /* WMTTemplateListVisual.swift */, + EA7EA22F2C494546000ECA41 /* WMTTemplateDetailVisual.swift */, ); path = TemplateParser; sourceTree = ""; @@ -631,7 +628,7 @@ DC0268DF29965495000BB9FA /* WMTOperationListResponse.swift in Sources */, DC8CB202244DCBE2009DDAA3 /* WMTOperations.swift in Sources */, DC48803E292282FF00DB844B /* WMTInboxMessage.swift in Sources */, - EA7EA22E2C49447B000ECA41 /* WMTUserOperationListVisual.swift in Sources */, + EA7EA22E2C49447B000ECA41 /* WMTTemplateListVisual.swift in Sources */, DCC5CCB52449F8E9004679AC /* WMTOperationAttributeAmount.swift in Sources */, DCC5CCD6244DBB7F004679AC /* WMTPushRegistrationData.swift in Sources */, DC3D0B392480F886000DC4D9 /* WMTLocalOperation.swift in Sources */, @@ -646,7 +643,7 @@ DC6E52D6259C964600FC25BE /* WMTOperationExpirationWatcher.swift in Sources */, DCC5CCDA244DBBE2004679AC /* WMTRejectionData.swift in Sources */, EA9795132C2C18450073E861 /* WMTTemplates.swift in Sources */, - EA7EA22D2C494478000ECA41 /* WMTUserOperationVisualParser.swift in Sources */, + EA7EA22D2C494478000ECA41 /* WMTTemplateVisualParser.swift in Sources */, DC48803F292282FF00DB844B /* WMTInboxMessageDetail.swift in Sources */, EA6DDF0F29F8036B0011E234 /* WMTPreApprovalScreen.swift in Sources */, DCAB7BC824580B4C0006989D /* WMTQROperationParser.swift in Sources */, @@ -673,10 +670,9 @@ DCC3420424E3DB310045D27D /* WMTPushParser.swift in Sources */, BFEEB20529379C700047941D /* WMTInboxGetMessageDetail.swift in Sources */, DCE5EAB026BD81150061861A /* WMTOperationHistoryEntry.swift in Sources */, - EA7EA2322C49480E000ECA41 /* ImageDownloader.swift in Sources */, EACAF7B02A126B7D0021CA54 /* WMTJsonValue.swift in Sources */, DCAB7BCA24580BAC0006989D /* WMTQROperation.swift in Sources */, - EA7EA2302C494546000ECA41 /* WMTUserOperationVisual.swift in Sources */, + EA7EA2302C494546000ECA41 /* WMTTemplateDetailVisual.swift in Sources */, DCC5CCBF2449F981004679AC /* WMTOperationAttributePartyInfo.swift in Sources */, DC81D1CB244F451E00F80CD6 /* WMTPushImpl.swift in Sources */, EA9CE2BE2AEAA9FD00FE4E35 /* WMTProximityCheck.swift in Sources */, diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/ImageDownloader.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/ImageDownloader.swift deleted file mode 100644 index aa1b533..0000000 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/ImageDownloader.swift +++ /dev/null @@ -1,106 +0,0 @@ -// -// 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 - - 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) - } - } - } - } -} diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisual.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTTemplateDetailVisual.swift similarity index 57% rename from WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisual.swift rename to WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTTemplateDetailVisual.swift index d8be2a1..8b66de3 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisual.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTTemplateDetailVisual.swift @@ -14,15 +14,29 @@ // and limitations under the License. // -import UIKit +import Foundation -public struct WMTUserOperationVisual { +/// This holds the visual data for displaying a detailed view of a user operation. +public struct WMTTemplateDetailVisual { + + /// An array of `WMTUserOperationVisualSection` defining the sections of the detailed view. public let sections: [WMTUserOperationVisualSection] + + public init(sections: [WMTUserOperationVisualSection]) { + self.sections = sections + } } +/// This struct defines one section in the detailed view of a user operation. public struct WMTUserOperationVisualSection { + + /// Predefined style of the section to which the app can react and adjust the operation visual public let style: String? - public let title: String? // not an id, actual value + + /// The title value for the section + public let title: String? + + /// An array of cells with `WMTOperationFormData` header and message or visual cells based on `WMTOperationAttributes` public let cells: [WMTUserOperationVisualCell] public init(style: String? = nil, title: String? = nil, cells: [WMTUserOperationVisualCell]) { @@ -32,56 +46,150 @@ public struct WMTUserOperationVisualSection { } } +/// A protocol for visual cells in a user operation's detailed view. public protocol WMTUserOperationVisualCell { } +/// `WMTUserOperationHeaderVisualCell` contains a header in a user operation's detail header view. +/// +/// This struct is used to distinguish between the default header section and custom `WMTUserOperationAttribute` sections. public struct WMTUserOperationHeaderVisualCell: WMTUserOperationVisualCell { + + /// This value corresponds to `WMTOperationFormData.title` public let value: String + + public init(value: String) { + self.value = value + } } +/// `WMTUserOperationMessageVisualCell` is a message cell in a user operation's header view. +/// +/// This struct is used within default header section and is used to distinguished from custom `WMTUserOperationAttribute` sections. public struct WMTUserOperationMessageVisualCell: WMTUserOperationVisualCell { + + /// This value corresponds to `WMTOperationFormData.message` public let value: String + + public init(value: String) { + self.value = value + } +} + +/// `WMTUserOperationHeadingVisualCell` defines a heading cell in a user operation's detailed view. +public struct WMTUserOperationHeadingVisualCell: WMTUserOperationVisualCell { + + /// Single highlighted text used as a section heading + public let header: String + + /// Predefined style of the section cell, app shall react to it and should change the visual of the cell + public let style: String? + + /// The source user operation attribute. + public let attribute: WMTOperationAttribute + + /// The template the cell was made from. + public let cellTemplate: WMTTemplates.DetailTemplate.Section.Cell? + + public init( + header: String, + style: String? = nil, + attribute: WMTOperationAttribute, + cellTemplate: WMTTemplates.DetailTemplate.Section.Cell? = nil + ) { + self.header = header + self.style = style + self.attribute = attribute + self.cellTemplate = cellTemplate + } } public struct WMTUserOperationValueAttributeVisualCell: WMTUserOperationVisualCell { + /// The header text value public let header: String + + /// The text value preformatted for the cell (if preformatted value isn't suficcient value from attribute can be used) public let defaultFormattedStringValue: String + + /// Predefined style of the section cell, app shall react to it and should change the visual of the cell public let style: String? + + /// The source user operation attribute. public let attribute: WMTOperationAttribute + + /// The template the cell was made from. public let cellTemplate: WMTTemplates.DetailTemplate.Section.Cell? + + public init( + header: String, + defaultFormattedStringValue: String, + style: String?, + attribute: WMTOperationAttribute, + cellTemplate: WMTTemplates.DetailTemplate.Section.Cell? + ) { + self.header = header + self.defaultFormattedStringValue = defaultFormattedStringValue + self.style = style + self.attribute = attribute + self.cellTemplate = cellTemplate + } } +/// `WMTUserOperationImageVisualCell` defines an image cell in a user operation's detailed view. public struct WMTUserOperationImageVisualCell: WMTUserOperationVisualCell { + /// The URL of the thumbnail image public let urlThumbnail: URL + + /// The URL of the full size image public let urlFull: URL? + + /// Predefined style of the section cell, app shall react to it and should change the visual of the cell public let style: String? + + /// The source user operation attribute. public let attribute: WMTOperationAttributeImage + + /// The template the cell was made from. public let cellTemplate: WMTTemplates.DetailTemplate.Section.Cell? - private let downloader = ImageDownloader.shared + public init( + urlThumbnail: URL, + urlFull: URL?, + style: String?, + attribute: WMTOperationAttributeImage, + cellTemplate: WMTTemplates.DetailTemplate.Section.Cell? + ) { + self.urlThumbnail = urlThumbnail + self.urlFull = urlFull + self.style = style + self.attribute = attribute + self.cellTemplate = cellTemplate + } + } // MARK: WMTUserOperation Detail Visual preparation extension extension WMTUserOperation { - internal func prepareVisualDetail() -> WMTUserOperationVisual { + internal func prepareVisualDetail() -> WMTTemplateDetailVisual { + // If templates don't contain detail return default header from `WMTOperationFormData` guard let detailTemplate = self.ui?.templates?.detail else { let attrs = self.formData.attributes if attrs.isEmpty { - return WMTUserOperationVisual(sections: [createHeaderVisual()]) + return WMTTemplateDetailVisual(sections: [createDefaultHeaderVisual()]) } else { - let headerSection = createHeaderVisual() + let headerSection = createDefaultHeaderVisual() let dataSections: WMTUserOperationVisualSection = .init(cells: attrs.getRemainingCells()) - return WMTUserOperationVisual(sections: [headerSection, dataSections]) + return WMTTemplateDetailVisual(sections: [headerSection, dataSections]) } } return createTemplateRichData(from: detailTemplate) } - // Default header - private func createHeaderVisual(style: String? = nil) -> WMTUserOperationVisualSection { + // Default header visual + private func createDefaultHeaderVisual(style: String? = nil) -> WMTUserOperationVisualSection { let defaultHeaderCell = WMTUserOperationHeaderVisualCell(value: self.formData.title) let defaultMessageCell = WMTUserOperationMessageVisualCell(value: self.formData.message) @@ -92,26 +200,30 @@ extension WMTUserOperation { ) } - private func createTemplateRichData(from detailTemplate: WMTTemplates.DetailTemplate) -> WMTUserOperationVisual { + // Creates WMTTemplateDetailVisual which contains cells divided in sections + private func createTemplateRichData(from detailTemplate: WMTTemplates.DetailTemplate) -> WMTTemplateDetailVisual { 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 headerSection = createDefaultHeaderVisual(style: detailTemplate.style) let dataSections: WMTUserOperationVisualSection = .init(cells: attrs.getRemainingCells()) - return WMTUserOperationVisual(sections: [headerSection, dataSections]) + return WMTTemplateDetailVisual(sections: [headerSection, dataSections]) } var sections = [WMTUserOperationVisualSection]() + // If showTitleAndMessage is explicitly false don't create default header + // this means that the whole operation is defined by templates + // AND `WMTOperationFormData` title and message will be ignored in visual object!!! if detailTemplate.showTitleAndMessage == false { let dataSections = attrs.popCells(from: sectionsTemplate) sections.append(contentsOf: dataSections) sections.append(.init(cells: attrs.getRemainingCells())) return .init(sections: sections) } else { - let headerSection = createHeaderVisual(style: detailTemplate.style) + let headerSection = createDefaultHeaderVisual(style: detailTemplate.style) let dataSection = attrs.popCells(from: sectionsTemplate) sections.append(headerSection) sections.append(contentsOf: dataSection) @@ -121,48 +233,12 @@ extension WMTUserOperation { } } -extension WMTUserOperationImageVisualCell { - func downloadFull(callback: @escaping (UIImage?) -> Void) { - guard let url = urlFull else { - callback(nil) - return - } - - // Use ImageDownloader to download the image - downloader.downloadImage( - at: url, - ImageDownloader.Callback { img in - if let img { - callback(img) - } else { - callAgain(callback: callback) - } - } - ) - } - - func downloadThumbnail(callback: @escaping (UIImage?) -> Void) { - downloader.downloadImage( - at: urlThumbnail, - ImageDownloader.Callback { img in - if let img { - callback(img) - } else { - callAgain(callback: callback) - } - } - ) - } - - private func callAgain(callback: @escaping (UIImage?) -> Void) { - DispatchQueue.global().asyncAfter(deadline: .now() + 1) { - self.downloadFull(callback: callback) - } - } -} - +// MARK: - Array Extension for WMTOperationAttribute private extension Array where Element: WMTOperationAttribute { + /// Pops an attribute of the specified type by its ID. + /// - Parameter id: The ID of the attribute. + /// - Returns: The attribute if found, otherwise nil. mutating func pop(id: String?) -> T? { guard let id = id else { return nil @@ -181,10 +257,14 @@ private extension Array where Element: WMTOperationAttribute { return attr } + /// Pops cells from the sections of the detail template. + /// - Parameter sections: The sections of the detail template. + /// - Returns: An array of `WMTUserOperationVisualSection` objects. mutating func popCells(from sections: [WMTTemplates.DetailTemplate.Section]) -> [WMTUserOperationVisualSection] { return sections.map { popCells(from: $0) } } + // Note that section title is already a string value mutating func popCells(from section: WMTTemplates.DetailTemplate.Section) -> WMTUserOperationVisualSection { let sectionFilled = WMTUserOperationVisualSection( style: section.style, @@ -248,7 +328,12 @@ private extension Array where Element: WMTOperationAttribute { cellTemplate: templateCell ) case .heading: - value = "" + return WMTUserOperationHeadingVisualCell( + header: attr.label.value, + style: templateCell?.style, + attribute: attr, + cellTemplate: templateCell + ) case .partyInfo, .unknown: D.warning("Using unsuported Attribute in Templates") value = "" diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationListVisual.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTTemplateListVisual.swift similarity index 87% rename from WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationListVisual.swift rename to WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTTemplateListVisual.swift index 2506c8a..5ba8f01 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationListVisual.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTTemplateListVisual.swift @@ -14,17 +14,28 @@ // and limitations under the License. // -import UIKit +import Foundation -public struct WMTUserOperationListVisual { +/// `WMTTemplateListVisual` holds the visual data for displaying a user operation in a list view (table view/collection view) +public struct WMTTemplateListVisual { + + /// The header of the cell public let header: String? + + /// The title of the cell public let title: String? + + /// The message (subtitle) of the cell public let message: String? + + /// Predefined style of the cell on which the implementation can react public let style: String? + + /// URL of the cell thumbnail public let thumbnailImageURL: URL? - public let template: WMTTemplates.ListTemplate? - private let downloader = ImageDownloader.shared + /// Complete template from which the WMTTemplateListVisual was created + public let template: WMTTemplates.ListTemplate? public init( header: String? = nil, @@ -41,37 +52,12 @@ public struct WMTUserOperationListVisual { 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 { + internal func prepareVisualListDetail() -> WMTTemplateListVisual { let listTemplate = self.ui?.templates?.list let attributes = self.formData.attributes let headerAtrr = listTemplate?.header?.replacePlaceholders(from: attributes) @@ -117,7 +103,7 @@ extension WMTUserOperation { return nil } - return WMTUserOperationListVisual( + return WMTTemplateListVisual( header: headerAtrr, title: title, message: message, diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTTemplateVisualParser.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTTemplateVisualParser.swift new file mode 100644 index 0000000..2aab1da --- /dev/null +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTTemplateVisualParser.swift @@ -0,0 +1,38 @@ +// +// 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 Foundation + +/// This is a utility class responsible for preparing visual representations of `WMTUserOperation`. +/// +/// It generates visual data for both list and detailed views of the operations from `WMTOperationFormData` and its `WMTOperationAttribute`. +/// The visual data are created based on the sctructure of the `WMTTemplates`. +public class WMTTemplateVisualParser { + + /// Prepares the visual representation for the given `WMTUserOperation` in a list view. + /// - Parameter operation: The user operation to prepare the visual data for. + /// - Returns: A `WMTTemplateListVisual` instance containing the visual data. + public static func prepareForList(operation: WMTUserOperation) -> WMTTemplateListVisual { + return operation.prepareVisualListDetail() + } + + /// Prepares the visual representation for a detail view of the given user operation. + /// - Parameter operation: The user operation to prepare the visual data for. + /// - Returns: A `WMTTemplateDetailVisual` instance containing the visual data. + public static func prepareForDetail(operation: WMTUserOperation) -> WMTTemplateDetailVisual { + return operation.prepareVisualDetail() + } +} diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift deleted file mode 100644 index 577c6f4..0000000 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTUserOperationVisualParser.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// 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 class WMTUserOperationVisualParser { - public static func prepareForList(operation: WMTUserOperation) -> WMTUserOperationListVisual { - return operation.prepareVisualListDetail() - } - - public static func prepareForDetail(operation: WMTUserOperation) -> WMTUserOperationVisual { - return operation.prepareVisualDetail() - } -} From abf9b321111827d927cb4af0e898ff0f7d01ecac Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Fri, 26 Jul 2024 11:50:26 +0200 Subject: [PATCH 20/29] Add logging when decoding of optionals throws --- .../Responses/WMTOperationListResponse.swift | 12 +- .../WMTOperationAttributeImage.swift | 12 +- .../Screens/WMTPostApprovaScreenReview.swift | 21 +- .../Screens/WMTPreApprovalScreen.swift | 24 +- .../UserOperation/WMTOperationFormData.swift | 12 +- .../UserOperation/WMTOperationUIData.swift | 60 ++++- .../Model/UserOperation/WMTResultTexts.swift | 36 ++- .../Model/UserOperation/WMTTemplates.swift | 210 ++++++++++++++++-- .../OperationUIDataTests.swift | 5 + 9 files changed, 359 insertions(+), 33 deletions(-) diff --git a/WultraMobileTokenSDK/Operations/Model/Responses/WMTOperationListResponse.swift b/WultraMobileTokenSDK/Operations/Model/Responses/WMTOperationListResponse.swift index 5bfc2cc..41dd8c5 100644 --- a/WultraMobileTokenSDK/Operations/Model/Responses/WMTOperationListResponse.swift +++ b/WultraMobileTokenSDK/Operations/Model/Responses/WMTOperationListResponse.swift @@ -29,7 +29,17 @@ public class WMTOperationListResponse: WPNResponseArray public required init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: Keys.self) - currentTimestamp = try? c.decode(Date.self, forKey: Keys.currentTimestamp) + + if c.contains(.currentTimestamp) { + do { + currentTimestamp = try c.decode(Date.self, forKey: .currentTimestamp) + } catch { + D.error("Failed to decode \(Keys.currentTimestamp) - \(error), setting to null") + currentTimestamp = nil + } + } else { + currentTimestamp = nil + } try super.init(from: decoder) } diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/Attributes/WMTOperationAttributeImage.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/Attributes/WMTOperationAttributeImage.swift index b8c83d3..b5d51c7 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/Attributes/WMTOperationAttributeImage.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/Attributes/WMTOperationAttributeImage.swift @@ -42,7 +42,17 @@ public class WMTOperationAttributeImage: WMTOperationAttribute { let c = try decoder.container(keyedBy: Keys.self) self.thumbnailUrl = try c.decode(String.self, forKey: .thumbnailUrl) - self.originalUrl = try? c.decode(String.self, forKey: .originalUrl) + + if c.contains(.originalUrl) { + do { + originalUrl = try c.decode(String.self, forKey: .originalUrl) + } catch { + D.error("Failed to decode \(Keys.originalUrl) - \(error), setting to null") + originalUrl = nil + } + } else { + originalUrl = nil + } try super.init(from: decoder) } diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPostApprovaScreenReview.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPostApprovaScreenReview.swift index 35d6318..08fa608 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPostApprovaScreenReview.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPostApprovaScreenReview.swift @@ -58,10 +58,25 @@ public class WMTReviewPostApprovalScreenPayload: WMTPostApprovalScreenPayload { // MARK: Internals public required init(from decoder: Decoder) throws { - let c = try decoder.container(keyedBy: Keys.self) - attributes = (try? c.decode([WMTOperationAttributeDecodable].self, forKey: .attributes).map { - $0.attrObject }) ?? [] + + var operationAttributes: [WMTOperationAttribute] = [] + do { + var container = try c.nestedUnkeyedContainer(forKey: .attributes) + // If decoding fails log it and continue decoding until the end of container + while container.isAtEnd == false { + do { + let attribute = try WMTOperationAttributeDecodable(from: container.superDecoder()) + operationAttributes.append(attribute.attrObject) + } catch { + D.error("Error decoding WMTOperationFormData attribute: \(error)") + } + } + } catch { + D.error("No attributes in WMTOperationFormData: \(error)") + } + attributes = operationAttributes + super.init() } diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPreApprovalScreen.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPreApprovalScreen.swift index 5d0caf8..fdf136b 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPreApprovalScreen.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPreApprovalScreen.swift @@ -56,8 +56,28 @@ public class WMTPreApprovalScreen: Codable { type = ScreenType(rawValue: t) ?? .unknown heading = try c.decode(String.self, forKey: .heading) message = try c.decode(String.self, forKey: .message) - items = try? c.decode([String].self, forKey: .items) - approvalType = try? c.decode(WMTPreApprovalScreenConfirmAction.self, forKey: .approvalType) + + if c.contains(.items) { + do { + items = try c.decode([String].self, forKey: .items) + } catch { + D.error("Failed to decode \(Keys.items) - \(error), setting to null") + items = nil + } + } else { + items = nil + } + + if c.contains(.approvalType) { + do { + approvalType = try c.decode(WMTPreApprovalScreenConfirmAction.self, forKey: .approvalType) + } catch { + D.error("Failed to decode \(Keys.approvalType) - \(error), setting to null") + approvalType = nil + } + } else { + approvalType = nil + } } public init(type: ScreenType, heading: String, message: String, items: [String]? = nil, approvalType: WMTPreApprovalScreenConfirmAction?) { diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationFormData.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationFormData.swift index b3b84db..c51323e 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationFormData.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationFormData.swift @@ -44,7 +44,17 @@ public class WMTOperationFormData: Codable { let c = try decoder.container(keyedBy: Keys.self) title = try c.decode(String.self, forKey: .title) message = try c.decode(String.self, forKey: .message) - resultTexts = try? c.decode(WMTResultTexts.self, forKey: .resultTexts) + + if c.contains(.resultTexts) { + do { + resultTexts = try c.decode(WMTResultTexts.self, forKey: .resultTexts) + } catch { + D.error("Failed to decode \(Keys.resultTexts) - \(error), setting to null") + resultTexts = nil + } + } else { + resultTexts = nil + } var operationAttributes: [WMTOperationAttribute] = [] do { diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationUIData.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationUIData.swift index 2f16e02..d7680a6 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationUIData.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationUIData.swift @@ -46,11 +46,61 @@ open class WMTOperationUIData: Codable { public required init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: Keys.self) - flipButtons = try? c.decode(Bool.self, forKey: .flipButtons) - blockApprovalOnCall = try? c.decode(Bool.self, forKey: .blockApprovalOnCall) - preApprovalScreen = try? c.decode(WMTPreApprovalScreen.self, forKey: .preApprovalScreen) - postApprovalScreen = try? c.decode(WMTPostApprovalScreenDecodable.self, forKey: .postApprovalScreen).postApprovalObject - templates = try? c.decode(WMTTemplates.self, forKey: .templates) + + if c.contains(.flipButtons) { + do { + flipButtons = try c.decode(Bool.self, forKey: .flipButtons) + } catch { + D.error("Failed to decode \(Keys.flipButtons) - \(error), setting to null") + flipButtons = nil + } + } else { + flipButtons = nil + } + + if c.contains(.blockApprovalOnCall) { + do { + blockApprovalOnCall = try c.decode(Bool.self, forKey: .blockApprovalOnCall) + } catch { + D.error("Failed to decode \(Keys.blockApprovalOnCall) - \(error), setting to null") + blockApprovalOnCall = nil + } + } else { + blockApprovalOnCall = nil + } + + if c.contains(.preApprovalScreen) { + do { + preApprovalScreen = try c.decode(WMTPreApprovalScreen.self, forKey: .preApprovalScreen) + } catch { + D.error("Failed to decode \(Keys.preApprovalScreen) - \(error), setting to null") + preApprovalScreen = nil + } + } else { + preApprovalScreen = nil + } + + if c.contains(.postApprovalScreen) { + do { + postApprovalScreen = try c.decode(WMTPostApprovalScreenDecodable.self, forKey: .postApprovalScreen).postApprovalObject + } catch { + D.error("Failed to decode \(Keys.postApprovalScreen) - \(error), setting to null") + postApprovalScreen = nil + } + } else { + postApprovalScreen = nil + } + + if c.contains(.templates) { + do { + templates = try c.decode(WMTTemplates.self, forKey: .templates) + } catch { + D.error("Failed to decode \(Keys.templates) - \(error), setting to null") + templates = nil + } + } else { + templates = nil + } } public init(flipButtons: Bool?, blockApprovalOnCall: Bool?, preApprovalScreen: WMTPreApprovalScreen?, postApprovalScreen: WMTPostApprovalScreen?, templates: WMTTemplates? = nil) { diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTResultTexts.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTResultTexts.swift index 205c97d..0e1399b 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTResultTexts.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTResultTexts.swift @@ -38,9 +38,39 @@ public class WMTResultTexts: Codable { public required init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: Keys.self) - success = try? c.decode(String.self, forKey: .success) - failure = try? c.decode(String.self, forKey: .failure) - reject = try? c.decode(String.self, forKey: .reject) + + if c.contains(.success) { + do { + success = try c.decode(String.self, forKey: .success) + } catch { + D.error("Failed to decode \(Keys.success) - \(error), setting to null") + success = nil + } + } else { + success = nil + } + + if c.contains(.failure) { + do { + failure = try c.decode(String.self, forKey: .failure) + } catch { + D.error("Failed to decode \(Keys.failure) - \(error), setting to null") + failure = nil + } + } else { + failure = nil + } + + if c.contains(.reject) { + do { + reject = try c.decode(String.self, forKey: .reject) + } catch { + D.error("Failed to decode \(Keys.reject) - \(error), setting to null") + reject = nil + } + } else { + reject = nil + } } public init(success: String?, failure: String?, reject: String?) { diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift index 861df4e..bdeebe6 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift @@ -36,8 +36,27 @@ public class WMTTemplates: Codable { public required init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: Keys.self) - list = try? c.decode(ListTemplate.self, forKey: .list) - detail = try? c.decode(DetailTemplate.self, forKey: .detail) + if c.contains(.list) { + do { + list = try c.decode(ListTemplate.self, forKey: .list) + } catch { + D.error("Failed to decode \(Keys.list) - \(error), setting to null") + list = nil + } + } else { + list = nil + } + + if c.contains(.detail) { + do { + detail = try c.decode(DetailTemplate.self, forKey: .detail) + } catch { + D.error("Failed to decode \(Keys.detail) - \(error), setting to null") + detail = nil + } + } else { + detail = nil + } } public init(list: ListTemplate?, detail: DetailTemplate?) { @@ -83,11 +102,61 @@ public class WMTTemplates: Codable { public required init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: Keys.self) - self.style = try? c.decode(String.self, forKey: .style) - self.header = try? c.decode(AttributeFormatted.self, forKey: .header) - self.title = try? c.decode(AttributeFormatted.self, forKey: .title) - self.message = try? c.decode(AttributeFormatted.self, forKey: .message) - self.image = try? c.decode(AttributeId.self, forKey: .image) + + if c.contains(.style) { + do { + style = try c.decode(String.self, forKey: .style) + } catch { + D.error("Failed to decode \(Keys.style) - \(error), setting to null") + style = nil + } + } else { + style = nil + } + + if c.contains(.header) { + do { + header = try c.decode(AttributeFormatted.self, forKey: .header) + } catch { + D.error("Failed to decode \(Keys.header) - \(error), setting to null") + header = nil + } + } else { + header = nil + } + + if c.contains(.title) { + do { + title = try c.decode(AttributeFormatted.self, forKey: .title) + } catch { + D.error("Failed to decode \(Keys.title) - \(error), setting to null") + title = nil + } + } else { + title = nil + } + + if c.contains(.message) { + do { + message = try c.decode(AttributeFormatted.self, forKey: .message) + } catch { + D.error("Failed to decode \(Keys.message) - \(error), setting to null") + message = nil + } + } else { + message = nil + } + + if c.contains(.image) { + do { + image = try c.decode(AttributeId.self, forKey: .image) + } catch { + D.error("Failed to decode \(Keys.image) - \(error), setting to null") + image = nil + } + } else { + image = nil + } } public init(style: String?, header: AttributeFormatted?, title: AttributeFormatted?, message: AttributeFormatted?, image: AttributeId?) { @@ -122,9 +191,39 @@ public class WMTTemplates: Codable { public required init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: Keys.self) - style = try? c.decode(String.self, forKey: .style) - showTitleAndMessage = try? c.decode(Bool.self, forKey: .showTitleAndMessage) - sections = try? c.decode([Section].self, forKey: .sections) + + if c.contains(.style) { + do { + style = try c.decode(String.self, forKey: .style) + } catch { + D.error("Failed to decode \(Keys.style) - \(error), setting to null") + style = nil + } + } else { + style = nil + } + + if c.contains(.showTitleAndMessage) { + do { + showTitleAndMessage = try c.decode(Bool.self, forKey: .showTitleAndMessage) + } catch { + D.error("Failed to decode \(Keys.showTitleAndMessage) - \(error), setting to null") + showTitleAndMessage = nil + } + } else { + showTitleAndMessage = nil + } + + if c.contains(.sections) { + do { + sections = try c.decode([Section].self, forKey: .sections) + } catch { + D.error("Failed to decode \(Keys.sections) - \(error), setting to null") + sections = nil + } + } else { + sections = nil + } } public init(style: String?, automaticHeaderSection: Bool?, sections: [Section]?) { @@ -153,9 +252,46 @@ public class WMTTemplates: Codable { public required init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: Keys.self) - style = try? c.decode(String.self, forKey: .style) - title = try? c.decode(AttributeId.self, forKey: .title) - cells = try? c.decode([Cell].self, forKey: .cells) + + if c.contains(.style) { + do { + style = try c.decode(String.self, forKey: .style) + } catch { + D.error("Failed to decode \(Keys.style) - \(error), setting to null") + style = nil + } + } else { + style = nil + } + + if c.contains(.title) { + do { + title = try c.decode(AttributeId.self, forKey: .title) + } catch { + D.error("Failed to decode \(Keys.title) - \(error), setting to null") + title = nil + } + } else { + title = nil + } + + if c.contains(.cells) { + var decodedCells: [Cell] = [] + + var nestedContainer = try c.nestedUnkeyedContainer(forKey: .cells) + while nestedContainer.isAtEnd == false { + do { + let cell = try Cell(from: nestedContainer.superDecoder()) + decodedCells.append(cell) + } catch { + D.error("Failed to decode \(Keys.cells) - \(error), setting to null") + } + } + + cells = decodedCells + } else { + cells = nil + } } public init(style: String?, title: AttributeId?, cells: [Cell]?) { @@ -201,11 +337,51 @@ public class WMTTemplates: Codable { public required init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: Keys.self) - style = try? c.decode(String.self, forKey: .style) name = try c.decode(AttributeId.self, forKey: .name) - visibleTitle = try? c.decode(Bool.self, forKey: .visibleTitle) - canCopy = try? c.decode(Bool.self, forKey: .canCopy) - collapsable = try? c.decode(Collapsable.self, forKey: .collapsable) + + if c.contains(.style) { + do { + style = try c.decode(String.self, forKey: .style) + } catch { + D.error("Failed to decode \(Keys.style) - \(error), setting to null") + style = nil + } + } else { + style = nil + } + + if c.contains(.visibleTitle) { + do { + visibleTitle = try c.decode(Bool.self, forKey: .visibleTitle) + } catch { + D.error("Failed to decode \(Keys.visibleTitle) - \(error), setting to null") + visibleTitle = nil + } + } else { + visibleTitle = nil + } + + if c.contains(.canCopy) { + do { + canCopy = try c.decode(Bool.self, forKey: .canCopy) + } catch { + D.error("Failed to decode \(Keys.canCopy) - \(error), setting to null") + canCopy = nil + } + } else { + canCopy = nil + } + + if c.contains(.collapsable) { + do { + collapsable = try c.decode(Collapsable.self, forKey: .collapsable) + } catch { + D.error("Failed to decode \(Keys.collapsable) - \(error), setting to null") + collapsable = nil + } + } else { + collapsable = nil + } } public init(style: String?, name: AttributeId, visibleTitle: Bool?, canCopy: Bool?, collapsable: Collapsable?) { diff --git a/WultraMobileTokenSDKTests/OperationUIDataTests.swift b/WultraMobileTokenSDKTests/OperationUIDataTests.swift index 6957592..953b051 100644 --- a/WultraMobileTokenSDKTests/OperationUIDataTests.swift +++ b/WultraMobileTokenSDKTests/OperationUIDataTests.swift @@ -227,6 +227,8 @@ class OperationUIDataTests: XCTestCase { XCTAssertEqual(uiResult.templates?.detail?.sections?[0].cells?[2].visibleTitle, true) XCTAssertEqual(uiResult.templates?.detail?.sections?[0].cells?[2].canCopy, false) XCTAssertEqual(uiResult.templates?.detail?.sections?[0].cells?[2].collapsable, .collapsed) + + XCTAssertEqual(uiResult.templates?.detail?.sections?[0].cells?.count, 3) } @@ -501,6 +503,9 @@ class OperationUIDataTests: XCTestCase { "style": null, "canCopy": false, "collapsable": "COLLAPSED" + }, + { + "visibleTitle": true } ] } From a5d8c4a2bd3af27ceb9095fd369d193e68232818 Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Fri, 26 Jul 2024 15:09:43 +0200 Subject: [PATCH 21/29] Add centered Cell property --- .../Model/UserOperation/WMTTemplates.swift | 19 +++++++++++++++++-- .../IntegrationProxy.swift | 2 +- .../OperationUIDataTests.swift | 4 +++- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift index bdeebe6..e5b72a0 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift @@ -318,6 +318,9 @@ public class WMTTemplates: Codable { /// Define if the cell should be collapsable public let collapsable: Collapsable? + /// If value should be centered + public let centered: Bool? + public enum Collapsable: String, Codable { /// The cell should not be collapsable case no = "NO" @@ -332,7 +335,7 @@ public class WMTTemplates: Codable { // MARK: - Internals private enum Keys: String, CodingKey { - case style, name, visibleTitle, canCopy, collapsable + case style, name, visibleTitle, canCopy, collapsable, centered } public required init(from decoder: Decoder) throws { @@ -382,14 +385,26 @@ public class WMTTemplates: Codable { } else { collapsable = nil } + + if c.contains(.centered) { + do { + centered = try c.decode(Bool.self, forKey: .centered) + } catch { + D.error("Failed to decode \(Keys.centered) - \(error), setting to null") + centered = nil + } + } else { + centered = nil + } } - public init(style: String?, name: AttributeId, visibleTitle: Bool?, canCopy: Bool?, collapsable: Collapsable?) { + public init(style: String?, name: AttributeId, visibleTitle: Bool?, canCopy: Bool?, collapsable: Collapsable?, centered: Bool?) { self.name = name self.style = style self.visibleTitle = visibleTitle self.canCopy = canCopy self.collapsable = collapsable + self.centered = centered } } } diff --git a/WultraMobileTokenSDKTests/IntegrationProxy.swift b/WultraMobileTokenSDKTests/IntegrationProxy.swift index b0497df..e8bffc5 100644 --- a/WultraMobileTokenSDKTests/IntegrationProxy.swift +++ b/WultraMobileTokenSDKTests/IntegrationProxy.swift @@ -31,7 +31,7 @@ class IntegrationProxy { typealias Callback = (_ error: String?) -> Void func prepareActivation(pin: String, callback: @escaping Callback) { - WPNLogger.verboseLevel = .debug + WPNLogger.verboseLevel = .all guard let configPath = Bundle.init(for: IntegrationProxy.self).path(forResource: "config", ofType: "json", inDirectory: "Configs") else { callback("Config file config.json is not present.") return diff --git a/WultraMobileTokenSDKTests/OperationUIDataTests.swift b/WultraMobileTokenSDKTests/OperationUIDataTests.swift index 953b051..0a404c6 100644 --- a/WultraMobileTokenSDKTests/OperationUIDataTests.swift +++ b/WultraMobileTokenSDKTests/OperationUIDataTests.swift @@ -212,6 +212,7 @@ class OperationUIDataTests: XCTestCase { XCTAssertEqual(uiResult.templates?.detail?.sections?[0].title, "operation.money.header") XCTAssertEqual(uiResult.templates?.detail?.sections?[0].cells?[0].style, nil) XCTAssertEqual(uiResult.templates?.detail?.sections?[0].cells?[0].name, "operation.amount") + XCTAssertEqual(uiResult.templates?.detail?.sections?[0].cells?[0].centered, true) XCTAssertEqual(uiResult.templates?.detail?.sections?[0].cells?[0].visibleTitle, false) XCTAssertEqual(uiResult.templates?.detail?.sections?[0].cells?[0].canCopy, true) XCTAssertEqual(uiResult.templates?.detail?.sections?[0].cells?[0].collapsable, .no) @@ -489,7 +490,8 @@ class OperationUIDataTests: XCTestCase { "visibleTitle": false, "style": null, "canCopy": true, - "collapsable": "NO" + "collapsable": "NO", + "centered": true }, { "style": "CONVERSION", From 4bee9c39162207bb25cb21cd211568cd15524e1c Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Mon, 29 Jul 2024 13:57:11 +0200 Subject: [PATCH 22/29] Fix `WPNLogger.verboseLevel` after incorrect merge --- WultraMobileTokenSDKTests/IntegrationProxy.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WultraMobileTokenSDKTests/IntegrationProxy.swift b/WultraMobileTokenSDKTests/IntegrationProxy.swift index e8bffc5..b0497df 100644 --- a/WultraMobileTokenSDKTests/IntegrationProxy.swift +++ b/WultraMobileTokenSDKTests/IntegrationProxy.swift @@ -31,7 +31,7 @@ class IntegrationProxy { typealias Callback = (_ error: String?) -> Void func prepareActivation(pin: String, callback: @escaping Callback) { - WPNLogger.verboseLevel = .all + WPNLogger.verboseLevel = .debug guard let configPath = Bundle.init(for: IntegrationProxy.self).path(forResource: "config", ofType: "json", inDirectory: "Configs") else { callback("Config file config.json is not present.") return From 033148e3a5da2b039f4bf75c2e9de4ac28368abb Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Wed, 31 Jul 2024 10:36:50 +0200 Subject: [PATCH 23/29] Remove generated .swiftpm contents file --- .../xcode/package.xcworkspace/contents.xcworkspacedata | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a..0000000 --- a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - From bab7f7585b17aa4db53bf2942fc06e56ae1cbe26 Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Wed, 31 Jul 2024 12:44:23 +0200 Subject: [PATCH 24/29] Simplify decoding with decodeIfPresent & improve decoding of the Section Cells --- .../Responses/WMTOperationListResponse.swift | 13 +- .../WMTOperationAttributeImage.swift | 12 +- .../Screens/WMTPreApprovalScreen.swift | 24 +- .../UserOperation/WMTOperationFormData.swift | 12 +- .../UserOperation/WMTOperationUIData.swift | 60 ++--- .../Model/UserOperation/WMTResultTexts.swift | 36 +-- .../Model/UserOperation/WMTTemplates.swift | 245 +++++++----------- .../OperationUIDataTests.swift | 20 ++ 8 files changed, 162 insertions(+), 260 deletions(-) diff --git a/WultraMobileTokenSDK/Operations/Model/Responses/WMTOperationListResponse.swift b/WultraMobileTokenSDK/Operations/Model/Responses/WMTOperationListResponse.swift index 41dd8c5..fbac0a0 100644 --- a/WultraMobileTokenSDK/Operations/Model/Responses/WMTOperationListResponse.swift +++ b/WultraMobileTokenSDK/Operations/Model/Responses/WMTOperationListResponse.swift @@ -30,17 +30,12 @@ public class WMTOperationListResponse: WPNResponseArray let c = try decoder.container(keyedBy: Keys.self) - if c.contains(.currentTimestamp) { - do { - currentTimestamp = try c.decode(Date.self, forKey: .currentTimestamp) - } catch { - D.error("Failed to decode \(Keys.currentTimestamp) - \(error), setting to null") - currentTimestamp = nil - } - } else { + do { + currentTimestamp = try c.decodeIfPresent(Date.self, forKey: .currentTimestamp) + } catch { + D.error("Failed to decode \(Keys.currentTimestamp) - \(error), setting to null") currentTimestamp = nil } - try super.init(from: decoder) } diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/Attributes/WMTOperationAttributeImage.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/Attributes/WMTOperationAttributeImage.swift index b5d51c7..4bb0fa5 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/Attributes/WMTOperationAttributeImage.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/Attributes/WMTOperationAttributeImage.swift @@ -43,14 +43,10 @@ public class WMTOperationAttributeImage: WMTOperationAttribute { self.thumbnailUrl = try c.decode(String.self, forKey: .thumbnailUrl) - if c.contains(.originalUrl) { - do { - originalUrl = try c.decode(String.self, forKey: .originalUrl) - } catch { - D.error("Failed to decode \(Keys.originalUrl) - \(error), setting to null") - originalUrl = nil - } - } else { + do { + originalUrl = try c.decodeIfPresent(String.self, forKey: .originalUrl) + } catch { + D.error("Failed to decode \(Keys.originalUrl) - \(error), setting to null") originalUrl = nil } diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPreApprovalScreen.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPreApprovalScreen.swift index fdf136b..9459e88 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPreApprovalScreen.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPreApprovalScreen.swift @@ -57,25 +57,17 @@ public class WMTPreApprovalScreen: Codable { heading = try c.decode(String.self, forKey: .heading) message = try c.decode(String.self, forKey: .message) - if c.contains(.items) { - do { - items = try c.decode([String].self, forKey: .items) - } catch { - D.error("Failed to decode \(Keys.items) - \(error), setting to null") - items = nil - } - } else { + do { + items = try c.decodeIfPresent([String].self, forKey: .items) + } catch { + D.error("Failed to decode \(Keys.items) - \(error), setting to null") items = nil } - if c.contains(.approvalType) { - do { - approvalType = try c.decode(WMTPreApprovalScreenConfirmAction.self, forKey: .approvalType) - } catch { - D.error("Failed to decode \(Keys.approvalType) - \(error), setting to null") - approvalType = nil - } - } else { + do { + approvalType = try c.decodeIfPresent(WMTPreApprovalScreenConfirmAction.self, forKey: .approvalType) + } catch { + D.error("Failed to decode \(Keys.approvalType) - \(error), setting to null") approvalType = nil } } diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationFormData.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationFormData.swift index c51323e..99ded81 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationFormData.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationFormData.swift @@ -45,14 +45,10 @@ public class WMTOperationFormData: Codable { title = try c.decode(String.self, forKey: .title) message = try c.decode(String.self, forKey: .message) - if c.contains(.resultTexts) { - do { - resultTexts = try c.decode(WMTResultTexts.self, forKey: .resultTexts) - } catch { - D.error("Failed to decode \(Keys.resultTexts) - \(error), setting to null") - resultTexts = nil - } - } else { + do { + resultTexts = try c.decodeIfPresent(WMTResultTexts.self, forKey: .resultTexts) + } catch { + D.error("Failed to decode \(Keys.resultTexts) - \(error), setting to null") resultTexts = nil } diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationUIData.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationUIData.swift index d7680a6..d59e77b 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationUIData.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationUIData.swift @@ -47,58 +47,38 @@ open class WMTOperationUIData: Codable { public required init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: Keys.self) - if c.contains(.flipButtons) { - do { - flipButtons = try c.decode(Bool.self, forKey: .flipButtons) - } catch { - D.error("Failed to decode \(Keys.flipButtons) - \(error), setting to null") - flipButtons = nil - } - } else { + do { + flipButtons = try c.decodeIfPresent(Bool.self, forKey: .flipButtons) + } catch { + D.error("Failed to decode \(Keys.flipButtons) - \(error), setting to null") flipButtons = nil } - if c.contains(.blockApprovalOnCall) { - do { - blockApprovalOnCall = try c.decode(Bool.self, forKey: .blockApprovalOnCall) - } catch { - D.error("Failed to decode \(Keys.blockApprovalOnCall) - \(error), setting to null") - blockApprovalOnCall = nil - } - } else { + do { + blockApprovalOnCall = try c.decodeIfPresent(Bool.self, forKey: .blockApprovalOnCall) + } catch { + D.error("Failed to decode \(Keys.blockApprovalOnCall) - \(error), setting to null") blockApprovalOnCall = nil } - if c.contains(.preApprovalScreen) { - do { - preApprovalScreen = try c.decode(WMTPreApprovalScreen.self, forKey: .preApprovalScreen) - } catch { - D.error("Failed to decode \(Keys.preApprovalScreen) - \(error), setting to null") - preApprovalScreen = nil - } - } else { + do { + preApprovalScreen = try c.decodeIfPresent(WMTPreApprovalScreen.self, forKey: .preApprovalScreen) + } catch { + D.error("Failed to decode \(Keys.preApprovalScreen) - \(error), setting to null") preApprovalScreen = nil } - if c.contains(.postApprovalScreen) { - do { - postApprovalScreen = try c.decode(WMTPostApprovalScreenDecodable.self, forKey: .postApprovalScreen).postApprovalObject - } catch { - D.error("Failed to decode \(Keys.postApprovalScreen) - \(error), setting to null") - postApprovalScreen = nil - } - } else { + do { + postApprovalScreen = try c.decodeIfPresent(WMTPostApprovalScreenDecodable.self, forKey: .postApprovalScreen)?.postApprovalObject + } catch { + D.error("Failed to decode \(Keys.postApprovalScreen) - \(error), setting to null") postApprovalScreen = nil } - if c.contains(.templates) { - do { - templates = try c.decode(WMTTemplates.self, forKey: .templates) - } catch { - D.error("Failed to decode \(Keys.templates) - \(error), setting to null") - templates = nil - } - } else { + do { + templates = try c.decodeIfPresent(WMTTemplates.self, forKey: .templates) + } catch { + D.error("Failed to decode \(Keys.templates) - \(error), setting to null") templates = nil } } diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTResultTexts.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTResultTexts.swift index 0e1399b..42abd8b 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTResultTexts.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTResultTexts.swift @@ -39,36 +39,24 @@ public class WMTResultTexts: Codable { public required init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: Keys.self) - if c.contains(.success) { - do { - success = try c.decode(String.self, forKey: .success) - } catch { - D.error("Failed to decode \(Keys.success) - \(error), setting to null") - success = nil - } - } else { + do { + success = try c.decodeIfPresent(String.self, forKey: .success) + } catch { + D.error("Failed to decode \(Keys.success) - \(error), setting to null") success = nil } - if c.contains(.failure) { - do { - failure = try c.decode(String.self, forKey: .failure) - } catch { - D.error("Failed to decode \(Keys.failure) - \(error), setting to null") - failure = nil - } - } else { + do { + failure = try c.decodeIfPresent(String.self, forKey: .failure) + } catch { + D.error("Failed to decode \(Keys.failure) - \(error), setting to null") failure = nil } - if c.contains(.reject) { - do { - reject = try c.decode(String.self, forKey: .reject) - } catch { - D.error("Failed to decode \(Keys.reject) - \(error), setting to null") - reject = nil - } - } else { + do { + reject = try c.decodeIfPresent(String.self, forKey: .reject) + } catch { + D.error("Failed to decode \(Keys.reject) - \(error), setting to null") reject = nil } } diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift index e5b72a0..4c87661 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift @@ -36,25 +36,17 @@ public class WMTTemplates: Codable { public required init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: Keys.self) - if c.contains(.list) { - do { - list = try c.decode(ListTemplate.self, forKey: .list) - } catch { - D.error("Failed to decode \(Keys.list) - \(error), setting to null") - list = nil - } - } else { + do { + list = try c.decodeIfPresent(ListTemplate.self, forKey: .list) + } catch { + D.error("Failed to decode \(Keys.list) - \(error), setting to null") list = nil } - if c.contains(.detail) { - do { - detail = try c.decode(DetailTemplate.self, forKey: .detail) - } catch { - D.error("Failed to decode \(Keys.detail) - \(error), setting to null") - detail = nil - } - } else { + do { + detail = try c.decodeIfPresent(DetailTemplate.self, forKey: .detail) + } catch { + D.error("Failed to decode \(Keys.detail) - \(error), setting to null") detail = nil } } @@ -103,58 +95,38 @@ public class WMTTemplates: Codable { public required init(from decoder: any Decoder) throws { let c = try decoder.container(keyedBy: Keys.self) - if c.contains(.style) { - do { - style = try c.decode(String.self, forKey: .style) - } catch { - D.error("Failed to decode \(Keys.style) - \(error), setting to null") - style = nil - } - } else { + do { + style = try c.decodeIfPresent(String.self, forKey: .style) + } catch { + D.error("Failed to decode \(Keys.style) - \(error), setting to null") style = nil } - - if c.contains(.header) { - do { - header = try c.decode(AttributeFormatted.self, forKey: .header) - } catch { - D.error("Failed to decode \(Keys.header) - \(error), setting to null") - header = nil - } - } else { + + do { + header = try c.decodeIfPresent(AttributeFormatted.self, forKey: .header) + } catch { + D.error("Failed to decode \(Keys.header) - \(error), setting to null") header = nil } - - if c.contains(.title) { - do { - title = try c.decode(AttributeFormatted.self, forKey: .title) - } catch { - D.error("Failed to decode \(Keys.title) - \(error), setting to null") - title = nil - } - } else { + + do { + title = try c.decodeIfPresent(AttributeFormatted.self, forKey: .title) + } catch { + D.error("Failed to decode \(Keys.title) - \(error), setting to null") title = nil } - - if c.contains(.message) { - do { - message = try c.decode(AttributeFormatted.self, forKey: .message) - } catch { - D.error("Failed to decode \(Keys.message) - \(error), setting to null") - message = nil - } - } else { + + do { + message = try c.decodeIfPresent(AttributeFormatted.self, forKey: .message) + } catch { + D.error("Failed to decode \(Keys.message) - \(error), setting to null") message = nil } - - if c.contains(.image) { - do { - image = try c.decode(AttributeId.self, forKey: .image) - } catch { - D.error("Failed to decode \(Keys.image) - \(error), setting to null") - image = nil - } - } else { + + do { + image = try c.decodeIfPresent(AttributeId.self, forKey: .image) + } catch { + D.error("Failed to decode \(Keys.image) - \(error), setting to null") image = nil } } @@ -192,36 +164,24 @@ public class WMTTemplates: Codable { public required init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: Keys.self) - if c.contains(.style) { - do { - style = try c.decode(String.self, forKey: .style) - } catch { - D.error("Failed to decode \(Keys.style) - \(error), setting to null") - style = nil - } - } else { + do { + style = try c.decodeIfPresent(String.self, forKey: .style) + } catch { + D.error("Failed to decode \(Keys.style) - \(error), setting to null") style = nil } - if c.contains(.showTitleAndMessage) { - do { - showTitleAndMessage = try c.decode(Bool.self, forKey: .showTitleAndMessage) - } catch { - D.error("Failed to decode \(Keys.showTitleAndMessage) - \(error), setting to null") - showTitleAndMessage = nil - } - } else { + do { + showTitleAndMessage = try c.decodeIfPresent(Bool.self, forKey: .showTitleAndMessage) + } catch { + D.error("Failed to decode \(Keys.showTitleAndMessage) - \(error), setting to null") showTitleAndMessage = nil } - if c.contains(.sections) { - do { - sections = try c.decode([Section].self, forKey: .sections) - } catch { - D.error("Failed to decode \(Keys.sections) - \(error), setting to null") - sections = nil - } - } else { + do { + sections = try c.decodeIfPresent([Section].self, forKey: .sections) + } catch { + D.error("Failed to decode \(Keys.sections) - \(error), setting to null") sections = nil } } @@ -253,42 +213,37 @@ public class WMTTemplates: Codable { public required init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: Keys.self) - if c.contains(.style) { - do { - style = try c.decode(String.self, forKey: .style) - } catch { - D.error("Failed to decode \(Keys.style) - \(error), setting to null") - style = nil - } - } else { + do { + style = try c.decodeIfPresent(String.self, forKey: .style) + } catch { + D.error("Failed to decode \(Keys.style) - \(error), setting to null") style = nil } - - if c.contains(.title) { - do { - title = try c.decode(AttributeId.self, forKey: .title) - } catch { - D.error("Failed to decode \(Keys.title) - \(error), setting to null") - title = nil - } - } else { + + do { + title = try c.decodeIfPresent(AttributeId.self, forKey: .title) + } catch { + D.error("Failed to decode \(Keys.title) - \(error), setting to null") title = nil } if c.contains(.cells) { var decodedCells: [Cell] = [] - - var nestedContainer = try c.nestedUnkeyedContainer(forKey: .cells) - while nestedContainer.isAtEnd == false { - do { - let cell = try Cell(from: nestedContainer.superDecoder()) - decodedCells.append(cell) - } catch { - D.error("Failed to decode \(Keys.cells) - \(error), setting to null") + do { + var nestedContainer = try c.nestedUnkeyedContainer(forKey: .cells) + while nestedContainer.isAtEnd == false { + do { + let cell = try Cell(from: nestedContainer.superDecoder()) + decodedCells.append(cell) + } catch { + D.error("Failed to decode \(Keys.cells) - \(error), setting to null") + } } + cells = decodedCells + } catch { + D.error("Failed to decode nested container for \(Keys.cells) - \(error), setting to null") + cells = nil } - - cells = decodedCells } else { cells = nil } @@ -342,58 +297,38 @@ public class WMTTemplates: Codable { let c = try decoder.container(keyedBy: Keys.self) name = try c.decode(AttributeId.self, forKey: .name) - if c.contains(.style) { - do { - style = try c.decode(String.self, forKey: .style) - } catch { - D.error("Failed to decode \(Keys.style) - \(error), setting to null") - style = nil - } - } else { + do { + style = try c.decodeIfPresent(String.self, forKey: .style) + } catch { + D.error("Failed to decode \(Keys.style) - \(error), setting to null") style = nil } - - if c.contains(.visibleTitle) { - do { - visibleTitle = try c.decode(Bool.self, forKey: .visibleTitle) - } catch { - D.error("Failed to decode \(Keys.visibleTitle) - \(error), setting to null") - visibleTitle = nil - } - } else { + + do { + visibleTitle = try c.decodeIfPresent(Bool.self, forKey: .visibleTitle) + } catch { + D.error("Failed to decode \(Keys.visibleTitle) - \(error), setting to null") visibleTitle = nil } - - if c.contains(.canCopy) { - do { - canCopy = try c.decode(Bool.self, forKey: .canCopy) - } catch { - D.error("Failed to decode \(Keys.canCopy) - \(error), setting to null") - canCopy = nil - } - } else { + + do { + canCopy = try c.decodeIfPresent(Bool.self, forKey: .canCopy) + } catch { + D.error("Failed to decode \(Keys.canCopy) - \(error), setting to null") canCopy = nil } - - if c.contains(.collapsable) { - do { - collapsable = try c.decode(Collapsable.self, forKey: .collapsable) - } catch { - D.error("Failed to decode \(Keys.collapsable) - \(error), setting to null") - collapsable = nil - } - } else { + + do { + collapsable = try c.decodeIfPresent(Collapsable.self, forKey: .collapsable) + } catch { + D.error("Failed to decode \(Keys.collapsable) - \(error), setting to null") collapsable = nil } - - if c.contains(.centered) { - do { - centered = try c.decode(Bool.self, forKey: .centered) - } catch { - D.error("Failed to decode \(Keys.centered) - \(error), setting to null") - centered = nil - } - } else { + + do { + centered = try c.decodeIfPresent(Bool.self, forKey: .centered) + } catch { + D.error("Failed to decode \(Keys.centered) - \(error), setting to null") centered = nil } } diff --git a/WultraMobileTokenSDKTests/OperationUIDataTests.swift b/WultraMobileTokenSDKTests/OperationUIDataTests.swift index 0a404c6..ea89a2f 100644 --- a/WultraMobileTokenSDKTests/OperationUIDataTests.swift +++ b/WultraMobileTokenSDKTests/OperationUIDataTests.swift @@ -230,6 +230,9 @@ class OperationUIDataTests: XCTestCase { XCTAssertEqual(uiResult.templates?.detail?.sections?[0].cells?[2].collapsable, .collapsed) XCTAssertEqual(uiResult.templates?.detail?.sections?[0].cells?.count, 3) + + XCTAssertNil(uiResult.templates?.detail?.sections?[1].cells) + XCTAssertNil(uiResult.templates?.detail?.sections?[2].cells) } @@ -510,6 +513,23 @@ class OperationUIDataTests: XCTestCase { "visibleTitle": true } ] + }, + { + "style": "FOOTER", + "title": "operation.footer" + }, + { + "style": "FOOTER", + "title": "operation.footer", + "cells": + { + "name": "operation.amount", + "visibleTitle": false, + "style": null, + "canCopy": true, + "collapsable": "NO", + "centered": true + } } ] } From 12900c0d59c216834d1d65ab4f3cc2de2a99e6cb Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Wed, 31 Jul 2024 13:00:06 +0200 Subject: [PATCH 25/29] Implement remarks --- .../WMTTemplateDetailVisual.swift | 19 +++++++------------ .../WMTTemplateListVisual.swift | 6 ------ 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTTemplateDetailVisual.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTTemplateDetailVisual.swift index 8b66de3..9354514 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTTemplateDetailVisual.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTTemplateDetailVisual.swift @@ -107,7 +107,7 @@ public struct WMTUserOperationValueAttributeVisualCell: WMTUserOperationVisualCe /// The header text value public let header: String - /// The text value preformatted for the cell (if preformatted value isn't suficcient value from attribute can be used) + /// The text value preformatted for the cell (if the preformatted value isn't sufficient, the value from the attribute can be used) public let defaultFormattedStringValue: String /// Predefined style of the section cell, app shall react to it and should change the visual of the cell @@ -164,7 +164,6 @@ public struct WMTUserOperationImageVisualCell: WMTUserOperationVisualCell { self.attribute = attribute self.cellTemplate = cellTemplate } - } // MARK: WMTUserOperation Detail Visual preparation extension @@ -174,18 +173,14 @@ extension WMTUserOperation { // If templates don't contain detail return default header from `WMTOperationFormData` guard let detailTemplate = self.ui?.templates?.detail else { - let attrs = self.formData.attributes - if attrs.isEmpty { - return WMTTemplateDetailVisual(sections: [createDefaultHeaderVisual()]) - } else { - let headerSection = createDefaultHeaderVisual() - let dataSections: WMTUserOperationVisualSection = .init(cells: attrs.getRemainingCells()) - - return WMTTemplateDetailVisual(sections: [headerSection, dataSections]) + var sections = [createDefaultHeaderVisual()] + if formData.attributes.isEmpty == false { + sections.append(.init(cells: formData.attributes.getRemainingCells())) } + return WMTTemplateDetailVisual(sections: sections) } - return createTemplateRichData(from: detailTemplate) + return createDetailVisual(from: detailTemplate) } // Default header visual @@ -201,7 +196,7 @@ extension WMTUserOperation { } // Creates WMTTemplateDetailVisual which contains cells divided in sections - private func createTemplateRichData(from detailTemplate: WMTTemplates.DetailTemplate) -> WMTTemplateDetailVisual { + private func createDetailVisual(from detailTemplate: WMTTemplates.DetailTemplate) -> WMTTemplateDetailVisual { var attrs = self.formData.attributes guard let sectionsTemplate = detailTemplate.sections else { diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTTemplateListVisual.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTTemplateListVisual.swift index 5ba8f01..5eca1f8 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTTemplateListVisual.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTTemplateListVisual.swift @@ -94,12 +94,6 @@ extension WMTUserOperation { return URL(string: imgAttrCell.thumbnailUrl) } - if let imgAttrCell = self.formData.attributes - .compactMap({ $0 as? WMTOperationAttributeImage }) - .first { - return URL(string: imgAttrCell.thumbnailUrl) - } - return nil } From b2327e2aa9cb3e7b5415fe16f57c13608b3c1233 Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Thu, 1 Aug 2024 12:09:48 +0200 Subject: [PATCH 26/29] Update docs --- .../Operations/Model/UserOperation/WMTTemplates.swift | 4 ++-- docs/Using-Operations-Service.md | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift index 4c87661..ad1ee42 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift @@ -24,7 +24,7 @@ public class WMTTemplates: Codable { /// How the operation should look like in the list of operations public let list: ListTemplate? - /// How the operation detail should look like + /// How the operation detail should look like when viewed individually. public let detail: DetailTemplate? // MARK: - Internals @@ -149,7 +149,7 @@ public class WMTTemplates: Codable { /// Predefined style name that can be processed by the app to customize the overall look of the operation. public let style: String? - /// Indicates if the header should be created from form data (title, message, image) or customized for a specific operation + /// Indicates if the header should be created from form data (title, message) or customized for a specific operation public let showTitleAndMessage: Bool? /// Sections of the operation data. diff --git a/docs/Using-Operations-Service.md b/docs/Using-Operations-Service.md index 8a0d91b..c81e70e 100644 --- a/docs/Using-Operations-Service.md +++ b/docs/Using-Operations-Service.md @@ -771,7 +771,7 @@ public class WMTTemplates: Codable { /// The template how the operation should look like in the list of operations let list: ListTemplate? - /// The template for how the operation data should look like + /// How the operation detail should look like when viewed individually. let detail: DetailTemplate? } ``` @@ -794,7 +794,7 @@ public class ListTemplate: Codable { let message: AttributeName? /// Attribute which will be used for the image - let image: AttributeName? + let image: AttributeId? } public class DetailTemplate: Codable { @@ -802,8 +802,8 @@ public class DetailTemplate: Codable { /// Predefined style name that can be processed by the app to customize the overall look of the operation. let style: String? - /// Indicates if the header should be created from form data (title, message, image) or customized for a specific operation - let automaticHeaderSection: Bool? + /// Indicates if the header should be created from form data (title, message) or customized for a specific operation + let showTitleAndMessage: Bool? /// Sections of the operation data. let sections: [Section]? @@ -838,6 +838,9 @@ public class DetailTemplate: Codable { /// Define if the cell should be collapsable let collapsable: Collapsable? + /// If value should be centered + let centered: Bool? + public enum Collapsable: String, Codable { /// The cell should not be collapsable case no = "NO" From 46f639cf18ad6ab49908b049007845beb68662ec Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Mon, 5 Aug 2024 16:06:57 +0200 Subject: [PATCH 27/29] Add style for the whole detail in visual parser --- .../WMTTemplateDetailVisual.swift | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTTemplateDetailVisual.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTTemplateDetailVisual.swift index 9354514..707d443 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTTemplateDetailVisual.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/TemplateParser/WMTTemplateDetailVisual.swift @@ -19,10 +19,14 @@ import Foundation /// This holds the visual data for displaying a detailed view of a user operation. public struct WMTTemplateDetailVisual { + /// Predefined style of the whole operation detail to which the app can react and adjust the operation visual + public let style: String? + /// An array of `WMTUserOperationVisualSection` defining the sections of the detailed view. public let sections: [WMTUserOperationVisualSection] - public init(sections: [WMTUserOperationVisualSection]) { + public init(style: String?, sections: [WMTUserOperationVisualSection]) { + self.style = style self.sections = sections } } @@ -36,7 +40,7 @@ public struct WMTUserOperationVisualSection { /// The title value for the section public let title: String? - /// An array of cells with `WMTOperationFormData` header and message or visual cells based on `WMTOperationAttributes` + /// An array of cells with `WMTOperationFormData` header and message or visual cells based on `WMTOperationAttributes` public let cells: [WMTUserOperationVisualCell] public init(style: String? = nil, title: String? = nil, cells: [WMTUserOperationVisualCell]) { @@ -103,6 +107,7 @@ public struct WMTUserOperationHeadingVisualCell: WMTUserOperationVisualCell { } } +/// `WMTUserOperationValueAttributeVisualCell` defines a key-value cell in a user operation's detailed view. public struct WMTUserOperationValueAttributeVisualCell: WMTUserOperationVisualCell { /// The header text value public let header: String @@ -177,19 +182,19 @@ extension WMTUserOperation { if formData.attributes.isEmpty == false { sections.append(.init(cells: formData.attributes.getRemainingCells())) } - return WMTTemplateDetailVisual(sections: sections) + return WMTTemplateDetailVisual(style: nil, sections: sections) } return createDetailVisual(from: detailTemplate) } // Default header visual - private func createDefaultHeaderVisual(style: String? = nil) -> WMTUserOperationVisualSection { + private func createDefaultHeaderVisual() -> WMTUserOperationVisualSection { let defaultHeaderCell = WMTUserOperationHeaderVisualCell(value: self.formData.title) let defaultMessageCell = WMTUserOperationMessageVisualCell(value: self.formData.message) return WMTUserOperationVisualSection( - style: style, + style: nil, title: nil, cells: [defaultHeaderCell, defaultMessageCell] ) @@ -201,10 +206,10 @@ extension WMTUserOperation { guard let sectionsTemplate = detailTemplate.sections else { // Sections not specified, but style might be - let headerSection = createDefaultHeaderVisual(style: detailTemplate.style) + let headerSection = createDefaultHeaderVisual() let dataSections: WMTUserOperationVisualSection = .init(cells: attrs.getRemainingCells()) - return WMTTemplateDetailVisual(sections: [headerSection, dataSections]) + return WMTTemplateDetailVisual(style: detailTemplate.style, sections: [headerSection, dataSections]) } var sections = [WMTUserOperationVisualSection]() @@ -216,14 +221,14 @@ extension WMTUserOperation { let dataSections = attrs.popCells(from: sectionsTemplate) sections.append(contentsOf: dataSections) sections.append(.init(cells: attrs.getRemainingCells())) - return .init(sections: sections) + return .init(style: detailTemplate.style, sections: sections) } else { - let headerSection = createDefaultHeaderVisual(style: detailTemplate.style) + let headerSection = createDefaultHeaderVisual() let dataSection = attrs.popCells(from: sectionsTemplate) sections.append(headerSection) sections.append(contentsOf: dataSection) sections.append(.init(cells: attrs.getRemainingCells())) - return .init(sections: sections) + return .init(style: detailTemplate.style, sections: sections) } } } From 593ba896c3d2f767f4fb5560b0b6f00c9b1751f9 Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Wed, 14 Aug 2024 16:30:05 +0200 Subject: [PATCH 28/29] Add visual parser to docs --- docs/Using-Operations-Service.md | 99 ++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/docs/Using-Operations-Service.md b/docs/Using-Operations-Service.md index c81e70e..03bc5a0 100644 --- a/docs/Using-Operations-Service.md +++ b/docs/Using-Operations-Service.md @@ -16,6 +16,8 @@ - [WMTUserOperation](#wmtuseroperation) - [Creating a Custom Operation](#creating-a-custom-operation) - [WMTProximityCheck](#wmtproximitycheck) +- [WMTTemplates](#wmttemplates) +- [WMTTemplateVisualParser](#wmttemplatevisualparser) - [Error handling](#error-handling) ## Introduction @@ -858,6 +860,103 @@ public class DetailTemplate: Codable { ``` +###WMTTemplateVisualParser + +For convenience we provide a utility class responsible for preparing visual representations of `WMTUserOperation` from received `WMTTemplates`. The parser translates `AttributeNames` from templates and returnes usable Strings values instead. Parser also always returns the source template from which the data was created. + +```swift +public class WMTTemplateVisualParser { + + /// Prepares the visual representation for the given `WMTUserOperation` in a list. + public static func prepareForList(operation: WMTUserOperation) -> WMTTemplateListVisual { + return operation.prepareVisualListDetail() + } + + /// Prepares the visual representation for a detail view of the given user operation. + public static func prepareForDetail(operation: WMTUserOperation) -> WMTTemplateDetailVisual { + return operation.prepareVisualDetail() + } +} + +``` + + +#### WMTTemplateListVisual + +`WMTTemplateListVisual` holds the visual data for displaying a `WMTUserOperation` in a list. + +```swift +public struct TemplateListVisual( + + /// The header of the cell + public let header: String? + + /// The title of the cell + public let title: String? + + /// The message (subtitle) of the cell + public let message: String? + + /// Predefined style of the cell on which the implementation can react + public let style: String? + + /// URL of the cell thumbnail + public let thumbnailImageURL: URL? + + /// Complete template from which the WMTTemplateListVisual was created + public let template: WMTTemplates.ListTemplate? + +) +``` + +#### WMTTemplateDetailVisual + +`WMTTemplateDetailVisual` holds the visual data for displaying a detailed view of a `WMTUserOperation`. It contains style to which the app can react and adjust the operation style. It also contains list of `WMTUserOperationVisualSection `. + +```kotlin +data class TemplateDetailVisual( + + /** Predefined style of the whole operation detail to which the app can react and adjust the operation visual */ + val style: String?, + + /** An array of `UserOperationVisualSection` defining the sections of the detailed view. */ + val sections: List +) +``` + +Sections contain style, title and cells properties. + +```swift +public struct WMTUserOperationVisualSection { + + /// Predefined style of the section to which the app can react and adjust the operation visual + public let style: String? + + /// The title value for the section + public let title: String? + + /// An array of cells with `WMTOperationFormData` header and message or visual cells based on `WMTOperationAttributes` + public let cells: [WMTUserOperationVisualCell] +) +``` + +`WMTUserOperationVisualCell ` is the basic building block of the UserOperation. We differentiate between 5 different cell types: +
    +
  1. `WMTUserOperationHeaderVisualCell` - is a header in a user operation's detail header view.
  2. + - it is created from UserOperation FormData title +
  3. `WMTUserOperationMessageVisualCell` - is a message cell in a user operation's header view.
  4. + - it is created from UserOperation FormData message +
  5. `WMTUserOperationHeadingVisualCell` - is a heading ("section separator") cell in a user operation's detailed view.
  6. + - it is created from `HEADING` FormData attribute +
  7. `WMTUserOperationImageVisualCell` - is an image cell in a user operation's detailed view.
  8. + - it is created from `IMAGE` FormData attribute +
  9. `WMTUserOperationValueAttributeVisualCell` - is value attribute cell in a user operation's detailed view.
  10. + - it is created from the remaining (`AMOUNT`, `AMOUNT_CONVERSION `, `KEY_VALUE`, `NOTE`) FormData attribute +
+ +> [!WARNING] +> At this moment `PARTY_INFO` & `UNKNOWN` attributes are not supported + ## Error handling From 0e88fef961300b051e8393d529d2778af9bdc73b Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Thu, 15 Aug 2024 09:58:20 +0200 Subject: [PATCH 29/29] Remove hardcoded template which was used for development --- .../UserOperation/WMTUserOperation.swift | 118 ------------------ 1 file changed, 118 deletions(-) diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift index b1f17f7..19679a6 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift @@ -58,8 +58,6 @@ open class WMTUserOperation: WMTOperation, Codable { /// /// Additional UI data such as Pre-Approval Screen or Post-Approval Screen should be presented. public let ui: WMTOperationUIData? - // TODO: remove before merging - /// public let ui = prepareTemplates(response: uiTemplates) /// Proximity Check Data to be passed when OTP is handed to the app public var proximityCheck: WMTProximityCheck? @@ -69,119 +67,3 @@ open class WMTUserOperation: WMTOperation, Codable { /// Max 32 characters are expected. Possible values depend on the backend implementation and configuration. public let statusReason: String? } - -private let jsonDecoder: JSONDecoder = { - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .iso8601 - return decoder -}() - -private func prepareTemplates(response: String) -> WMTOperationUIData? { - let result = try? jsonDecoder.decode(WMTOperationUIData.self, from: response.data(using: .utf8)!) - return result -} - -private let uiTemplates: String = { -""" -{ - "templates": { - "list": { - "header": "${operation.request_no} Withdrawal Initiation", - "message": "${operation.tx_amount}", - "title": "${operation.account} · ${operation.enterprise}", - "image": "operation.image" - }, - "detail": { - "style": null, - "showTitleAndMessage": false, - "sections": [ - { - "style": "HEADER", - "cells": [ - { - "style": "ROUND", - "name": "operation.image" - }, - { - "style": "HEADER_TITLE", - "name": "operation.dapp_originUrl" - } - ] - }, - { - "style": "HEADER_WARNING", - "title": null, - "cells": [ - { - "style": "WARNING_NOTE", - "name": "operation.blind_note" - } - ] - }, - { - "style": "MONEY", - "title": null, - "cells": [ - { - "name": "operation.request_no" - }, - { - "name": "operation.tx_amount", - "canCopy": true - }, - { - "name": "operation.account" - }, - { - "name": "operation.network" - }, - { - "name": "operation.address" - }, - { - "name": "operation.fee_amount" - }, - { - "name": "operation.scheme" - } - ] - }, - { - "style": "DESCRIPTION", - "cells": [ - { - "name": "operation.initiated_by", - "visibleTitle": true - }, - { - "style": "CONVERSION", - "name": "operation.location", - "canCopy": true, - "collapsable": "NO" - }, - { - "name": "operation.initiated_at", - "visibleTitle": false, - "style": null, - "canCopy": false - }, - { - "name": "operation.enterprise" - } - ] - }, - { - "style": "DATA", - "cells": [ - { - "name": "operation.tx_data", - "collapsable": "COLLAPSED" - } - ] - } - ] - } - } -} -""" -}()