From 6876fa0f426b73439175620b748242bc194bf35f Mon Sep 17 00:00:00 2001 From: Jan Kobersky <5406945+kober32@users.noreply.github.com> Date: Fri, 23 Jun 2023 11:47:01 +0200 Subject: [PATCH] Moved ui object from the mtoken to SDK (#118) --- .../project.pbxproj | 40 ++ .../Screens/WMTPostApprovaScreenGeneric.swift | 51 +++ .../WMTPostApprovaScreenRedirect.swift | 85 ++++ .../Screens/WMTPostApprovaScreenReview.swift | 76 ++++ .../Screens/WMTPostApprovalScreen.swift | 63 +++ .../Screens/WMTPreApprovalScreen.swift | 76 ++++ .../UserOperation/WMTOperationFormData.swift | 4 +- .../UserOperation/WMTOperationUIData.swift | 67 +++ .../UserOperation/WMTUserOperation.swift | 5 + .../Operations/Utils/WMTJsonValue.swift | 103 +++++ .../OperationUIDataTests.swift | 422 ++++++++++++++++++ docs/Using-Operations-Service.md | 39 ++ 12 files changed, 1029 insertions(+), 2 deletions(-) create mode 100644 WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPostApprovaScreenGeneric.swift create mode 100644 WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPostApprovaScreenRedirect.swift create mode 100644 WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPostApprovaScreenReview.swift create mode 100644 WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPostApprovalScreen.swift create mode 100644 WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPreApprovalScreen.swift create mode 100644 WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationUIData.swift create mode 100644 WultraMobileTokenSDK/Operations/Utils/WMTJsonValue.swift create mode 100644 WultraMobileTokenSDKTests/OperationUIDataTests.swift diff --git a/WultraMobileTokenSDK.xcodeproj/project.pbxproj b/WultraMobileTokenSDK.xcodeproj/project.pbxproj index 869e1f6..2bd5988 100644 --- a/WultraMobileTokenSDK.xcodeproj/project.pbxproj +++ b/WultraMobileTokenSDK.xcodeproj/project.pbxproj @@ -64,6 +64,14 @@ DCE5EAB026BD81150061861A /* WMTOperationHistoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE5EAAF26BD81150061861A /* WMTOperationHistoryEntry.swift */; }; DCE660D124CEBECA00870E53 /* IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE660D024CEBECA00870E53 /* IntegrationTests.swift */; }; DCE660D324CEF56400870E53 /* IntegrationProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE660D224CEF56400870E53 /* IntegrationProxy.swift */; }; + EA294F3D29F6A07A00A0494E /* WMTOperationUIData.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA294F3C29F6A07A00A0494E /* WMTOperationUIData.swift */; }; + EA44366A29F9294600DDEC1C /* WMTPostApprovaScreenReview.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA44366929F9294600DDEC1C /* WMTPostApprovaScreenReview.swift */; }; + EA44366C29F9297100DDEC1C /* WMTPostApprovaScreenRedirect.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA44366B29F9297100DDEC1C /* WMTPostApprovaScreenRedirect.swift */; }; + EA44366E29F9298100DDEC1C /* WMTPostApprovaScreenGeneric.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA44366D29F9298100DDEC1C /* WMTPostApprovaScreenGeneric.swift */; }; + EA6DDF0F29F8036B0011E234 /* WMTPreApprovalScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6DDF0E29F8036B0011E234 /* WMTPreApprovalScreen.swift */; }; + EA6DDF1A29F804D60011E234 /* WMTPostApprovalScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6DDF1929F804D60011E234 /* WMTPostApprovalScreen.swift */; }; + EA6DDF1C29F807230011E234 /* OperationUIDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6DDF1B29F807230011E234 /* OperationUIDataTests.swift */; }; + EACAF7B02A126B7D0021CA54 /* WMTJsonValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = EACAF7AF2A126B7D0021CA54 /* WMTJsonValue.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -139,6 +147,14 @@ DCE5EAAF26BD81150061861A /* WMTOperationHistoryEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOperationHistoryEntry.swift; sourceTree = ""; }; 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 = ""; }; + 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 = ""; }; + EA6DDF0E29F8036B0011E234 /* WMTPreApprovalScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPreApprovalScreen.swift; sourceTree = ""; }; + EA6DDF1929F804D60011E234 /* WMTPostApprovalScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPostApprovalScreen.swift; sourceTree = ""; }; + EA6DDF1B29F807230011E234 /* OperationUIDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationUIDataTests.swift; sourceTree = ""; }; + EACAF7AF2A126B7D0021CA54 /* WMTJsonValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTJsonValue.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -200,11 +216,13 @@ DC3D0B352480F368000DC4D9 /* UserOperation */ = { isa = PBXGroup; children = ( + EA6DDF0D29F8031F0011E234 /* Screens */, DC059A3C244DD30900B24878 /* Attributes */, DCC5CCAD2449F7AC004679AC /* WMTUserOperation.swift */, DCC5CCB02449F81C004679AC /* WMTOperationFormData.swift */, DC8CB205244DD007009DDAA3 /* WMTAllowedOperationSignature.swift */, DCE5EAAF26BD81150061861A /* WMTOperationHistoryEntry.swift */, + EA294F3C29F6A07A00A0494E /* WMTOperationUIData.swift */, ); path = UserOperation; sourceTree = ""; @@ -246,6 +264,7 @@ DC5F6DE724E14FA100D351D3 /* Configs */, DC616237248508F8000DED17 /* Info.plist */, DC61624124852B6D000DED17 /* NetworkingObjectsTests.swift */, + EA6DDF1B29F807230011E234 /* OperationUIDataTests.swift */, DCE660D024CEBECA00870E53 /* IntegrationTests.swift */, DCE660D224CEF56400870E53 /* IntegrationProxy.swift */, DC395C0924E55B9B0007C36E /* PushParserTests.swift */, @@ -269,6 +288,7 @@ isa = PBXGroup; children = ( DC6E52D5259C964600FC25BE /* WMTOperationExpirationWatcher.swift */, + EACAF7AF2A126B7D0021CA54 /* WMTJsonValue.swift */, ); path = Utils; sourceTree = ""; @@ -408,6 +428,18 @@ path = Requests; sourceTree = ""; }; + EA6DDF0D29F8031F0011E234 /* Screens */ = { + isa = PBXGroup; + children = ( + EA6DDF0E29F8036B0011E234 /* WMTPreApprovalScreen.swift */, + EA6DDF1929F804D60011E234 /* WMTPostApprovalScreen.swift */, + EA44366B29F9297100DDEC1C /* WMTPostApprovaScreenRedirect.swift */, + EA44366929F9294600DDEC1C /* WMTPostApprovaScreenReview.swift */, + EA44366D29F9298100DDEC1C /* WMTPostApprovaScreenGeneric.swift */, + ); + path = Screens; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -545,6 +577,7 @@ DC395C0A24E55B9B0007C36E /* PushParserTests.swift in Sources */, DC6EDB7925A49ED900A229E4 /* OperationExpirationTests.swift in Sources */, DC616236248508F8000DED17 /* QROperationParserTests.swift in Sources */, + EA6DDF1C29F807230011E234 /* OperationUIDataTests.swift in Sources */, DCE660D124CEBECA00870E53 /* IntegrationTests.swift in Sources */, DCE660D324CEF56400870E53 /* IntegrationProxy.swift in Sources */, ); @@ -571,14 +604,19 @@ DC6E52D6259C964600FC25BE /* WMTOperationExpirationWatcher.swift in Sources */, DCC5CCDA244DBBE2004679AC /* WMTRejectionData.swift in Sources */, DC48803F292282FF00DB844B /* WMTInboxMessageDetail.swift in Sources */, + EA6DDF0F29F8036B0011E234 /* WMTPreApprovalScreen.swift in Sources */, DCAB7BC824580B4C0006989D /* WMTQROperationParser.swift in Sources */, + EA44366C29F9297100DDEC1C /* WMTPostApprovaScreenRedirect.swift in Sources */, DCC5CCB12449F81C004679AC /* WMTOperationFormData.swift in Sources */, DCC5CCD4244DBA1C004679AC /* WMTOperationEndpoints.swift in Sources */, DC488031292282C900DB844B /* WMTService.swift in Sources */, + EA6DDF1A29F804D60011E234 /* WMTPostApprovalScreen.swift in Sources */, DCC5CCB92449F93C004679AC /* WMTOperationAttributeKeyValue.swift in Sources */, DCC5CCBB2449F952004679AC /* WMTOperationAttributeNote.swift in Sources */, BFEEB2092937A2680047941D /* WMTInboxGetList.swift in Sources */, BFEEB20729379F960047941D /* WMTInboxSetMessageRead.swift in Sources */, + EA44366A29F9294600DDEC1C /* WMTPostApprovaScreenReview.swift in Sources */, + EA294F3D29F6A07A00A0494E /* WMTOperationUIData.swift in Sources */, DCC5CCB32449F8CD004679AC /* WMTOperationAttribute.swift in Sources */, DCC5CCAC2449F765004679AC /* WMTOperationsImpl.swift in Sources */, DC06D01F25AC74E400F2EA69 /* WMTLock.swift in Sources */, @@ -590,12 +628,14 @@ DCC3420424E3DB310045D27D /* WMTPushParser.swift in Sources */, BFEEB20529379C700047941D /* WMTInboxGetMessageDetail.swift in Sources */, DCE5EAB026BD81150061861A /* WMTOperationHistoryEntry.swift in Sources */, + EACAF7B02A126B7D0021CA54 /* WMTJsonValue.swift in Sources */, DCAB7BCA24580BAC0006989D /* WMTQROperation.swift in Sources */, DCC5CCBF2449F981004679AC /* WMTOperationAttributePartyInfo.swift in Sources */, DC81D1CB244F451E00F80CD6 /* WMTPushImpl.swift in Sources */, DC488041292282FF00DB844B /* WMTInboxEndpoints.swift in Sources */, BF53DFC82971905600829814 /* WMTInboxContentType.swift in Sources */, DCA43C6D2993F63E0059A163 /* WMTOperationAttributeImage.swift in Sources */, + EA44366E29F9298100DDEC1C /* WMTPostApprovaScreenGeneric.swift in Sources */, DC48803D292282FF00DB844B /* WMTInbox.swift in Sources */, DC488042292282FF00DB844B /* WMTInboxImpl.swift in Sources */, ); diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPostApprovaScreenGeneric.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPostApprovaScreenGeneric.swift new file mode 100644 index 0000000..3ab3603 --- /dev/null +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPostApprovaScreenGeneric.swift @@ -0,0 +1,51 @@ +// +// Copyright 2023 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 + +/// Generic screen may contain any object +public class WMTPostApprovalScreenGeneric: WMTPostApprovalScreen { + + /// Heading of the post-approval screen + public let heading: String + + /// Message to the user + public let message: String + + /// Generic data defined on server + public let payload: WMTJSONValue + + // MARK: Internals + + private enum Keys: String, CodingKey { + case heading, message, payload + } + + public required init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: Keys.self) + heading = try c.decode(String.self, forKey: .heading) + message = try c.decode(String.self, forKey: .message) + payload = try c.decode(WMTJSONValue.self, forKey: .payload) + try super.init(from: decoder) + } + + public init(heading: String, message: String, payload: WMTJSONValue) { + self.heading = heading + self.message = message + self.payload = payload + super.init(type: .generic) + } +} diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPostApprovaScreenRedirect.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPostApprovaScreenRedirect.swift new file mode 100644 index 0000000..6bf4d70 --- /dev/null +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPostApprovaScreenRedirect.swift @@ -0,0 +1,85 @@ +// +// Copyright 2023 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 + +/// Redirect screen prepares for merchant redirect +public class WMTPostApprovalScreenRedirect: WMTPostApprovalScreen { + + /// Heading of the post-approval screen + public let heading: String + + /// Message to the user + public let message: String + + /// Payload with data about action after the operation + public let payload: WMTRedirectPostApprovalScreenPayload + + private enum Keys: String, CodingKey { + case heading, message, payload + } + + public init(heading: String, message: String, payload: WMTRedirectPostApprovalScreenPayload, type: ScreenType) { + self.heading = heading + self.message = message + self.payload = payload + super.init(type: type) + } + + public required init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: Keys.self) + heading = try c.decode(String.self, forKey: .heading) + message = try c.decode(String.self, forKey: .message) + payload = try c.decode(WMTRedirectPostApprovalScreenPayload.self, forKey: .payload) + try super.init(from: decoder) + } +} + +/// Payload with data about redirecting after the operation +public class WMTRedirectPostApprovalScreenPayload: WMTPostApprovalScreenPayload { + + /// Text for the button title + public let text: String + + /// URL where to redirect + public let url: String + + /// Countdown after which the redirect should happen in seconds + public let countdown: Int + + // MARK: Internals + + private enum Keys: String, CodingKey { + case text = "redirectText" + case url = "redirectUrl" + case countdown = "countdown" + } + + public init(text: String, url: String, countdown: Int) { + self.text = text + self.url = url + self.countdown = countdown + super.init() + } + + required init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: Keys.self) + text = try c.decode(String.self, forKey: .text) + url = try c.decode(String.self, forKey: .url) + countdown = try c.decode(Int.self, forKey: .countdown) + super.init() + } +} diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPostApprovaScreenReview.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPostApprovaScreenReview.swift new file mode 100644 index 0000000..35d6318 --- /dev/null +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPostApprovaScreenReview.swift @@ -0,0 +1,76 @@ +// +// Copyright 2023 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 + +/// Review screen shows the operation attributes +public class WMTPostApprovalScreenReview: WMTPostApprovalScreen { + + /// Heading of the post-approval screen + public let heading: String + + /// Message to the user + public let message: String + + /// Payload with data about action after the operation + public let payload: WMTReviewPostApprovalScreenPayload + // MARK: Internals + + private enum Keys: String, CodingKey { + case heading, message, payload + } + + public init(heading: String, message: String, payload: WMTReviewPostApprovalScreenPayload, type: ScreenType) { + self.heading = heading + self.message = message + self.payload = payload + super.init(type: type) + } + + public required init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: Keys.self) + heading = try c.decode(String.self, forKey: .heading) + message = try c.decode(String.self, forKey: .message) + payload = try c.decode(WMTReviewPostApprovalScreenPayload.self, forKey: .payload) + try super.init(from: decoder) + } +} + +/// Payload of the review post-approval screen shows the operation attributes. +public class WMTReviewPostApprovalScreenPayload: WMTPostApprovalScreenPayload { + + /// Attributes as in FormData but its data might be only a subset + public let attributes: [WMTOperationAttribute] + + // 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 }) ?? [] + super.init() + } + + public init(attributes: [WMTOperationAttribute]) { + self.attributes = attributes + super.init() + } + + private enum Keys: CodingKey { + case attributes + } +} diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPostApprovalScreen.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPostApprovalScreen.swift new file mode 100644 index 0000000..88680ba --- /dev/null +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPostApprovalScreen.swift @@ -0,0 +1,63 @@ +// +// Copyright 2023 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 + +/// WMTPostApprovalScreen is the base class for Post Approval screens classes +/// +/// `type` define different kind of data which can be passed with operation +/// and shall be displayed before operation is confirmed +public class WMTPostApprovalScreen: Codable { + + /// type of PostApprovalScreen is presented with different classes (Starting with `WMTPostApprovalScreen*`) + public let type: ScreenType + + public enum ScreenType: String, Codable { + case review = "REVIEW" + case redirect = "MERCHANT_REDIRECT" + case generic = "GENERIC" + } + + private enum Keys: String, CodingKey { + case type + } + + public required init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: Keys.self) + type = try c.decode(ScreenType.self, forKey: .type) + } + + public init(type: ScreenType) { + self.type = type + } + + /// This is convenience function to implement Polymorphic behavior with different types of screens + class func decode(decoder: Decoder) throws -> WMTPostApprovalScreen? { + let c = try decoder.container(keyedBy: Keys.self) + let t = try c.decode(String.self, forKey: .type) + let preType = ScreenType(rawValue: t) + + switch preType { + case .review: return try WMTPostApprovalScreenReview(from: decoder) + case .redirect: return try WMTPostApprovalScreenRedirect(from: decoder) + default: + return try WMTPostApprovalScreenGeneric(from: decoder) + } + } +} + +/// PostApprovalScreenPayload is base class for all payload classes +public class WMTPostApprovalScreenPayload: Codable {} diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPreApprovalScreen.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPreApprovalScreen.swift new file mode 100644 index 0000000..5d0caf8 --- /dev/null +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/Screens/WMTPreApprovalScreen.swift @@ -0,0 +1,76 @@ +// +// Copyright 2023 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 + +/// WMTPreApprovalScreen contains data to be presented before approving operation +/// +/// `type` define different kind of data which can be passed with operation +/// and shall be displayed before operation is confirmed +public class WMTPreApprovalScreen: Codable { + + /// Type of PreApprovalScreen (`WARNING`, `INFO`, `QR_SCAN` or unknown for future compatibility ) + public let type: ScreenType + + /// Heading of the pre-approval screen + public let heading: String + + /// Message to the user + public let message: String + + /// Array of items to be displayed as list of choices + public let items: [String]? + + /// Type of the approval button + public let approvalType: WMTPreApprovalScreenConfirmAction? + + // MARK: - INTERNALS + + public enum ScreenType: String, Codable { + case info = "INFO" + case warning = "WARNING" + case qr = "QR_SCAN" + case unknown = "UNKNOWN" + } + + private enum Keys: String, CodingKey { + case type, heading, message, items, approvalType + } + + public required init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: Keys.self) + let t = try c.decode(String.self, forKey: .type) + 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) + } + + public init(type: ScreenType, heading: String, message: String, items: [String]? = nil, approvalType: WMTPreApprovalScreenConfirmAction?) { + self.type = type + self.heading = heading + self.message = message + self.items = items + self.approvalType = approvalType + } +} + +/// Type of action which is used within Derived PreApproval classes to define +/// how the confirm action shall be performed +public enum WMTPreApprovalScreenConfirmAction: String, Codable { + case slider = "SLIDER" +} diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationFormData.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationFormData.swift index d22d479..90854e8 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationFormData.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationFormData.swift @@ -56,9 +56,9 @@ public class WMTOperationFormData: Codable { // This class acts as "translation layer" for decoding polymorphic property of "attributes" // property inside OperationFormData class that can have multiple types of Attribute inside -private class WMTOperationAttributeDecodable: Decodable { +class WMTOperationAttributeDecodable: Decodable { - fileprivate let attrObject: WMTOperationAttribute + let attrObject: WMTOperationAttribute required init(from decoder: Decoder) throws { attrObject = try WMTOperationAttribute.decode(decoder: decoder) diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationUIData.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationUIData.swift new file mode 100644 index 0000000..c70546a --- /dev/null +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTOperationUIData.swift @@ -0,0 +1,67 @@ +// +// Copyright 2023 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 + +/// Operation UI contains data for screens pre and/or post approved operation +open class WMTOperationUIData: Codable { + + /// Confirm and Reject buttons should be flipped both in position and style + public let flipButtons: Bool? + + /// Block approval when on call (for example when on phone or skype call) + public let blockApprovalOnCall: Bool? + + /// UI for pre-approval operation screen + public let preApprovalScreen: WMTPreApprovalScreen? + + /// UI for post-approval opration screen + /// + /// Type of PostApprovalScrren is presented with different classes (Starting with `WMTPostApprovalScreen*`) + public let postApprovalScreen: WMTPostApprovalScreen? + + // MARK: - INTERNALS + + private enum Keys: String, CodingKey { + case flipButtons, blockApprovalOnCall, preApprovalScreen, postApprovalScreen + } + + 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 + } + + public init(flipButtons: Bool?, blockApprovalOnCall: Bool?, preApprovalScreen: WMTPreApprovalScreen?, postApprovalScreen: WMTPostApprovalScreen?) { + self.flipButtons = flipButtons + self.blockApprovalOnCall = blockApprovalOnCall + self.preApprovalScreen = preApprovalScreen + self.postApprovalScreen = postApprovalScreen + } +} + +// This class acts as "translation layer" for decoding polymorphic property of PostApprovalScreen +// property inside OperationFormData class that can have multiple types of PreApprovalScreen inside +private class WMTPostApprovalScreenDecodable: Decodable { + + fileprivate let postApprovalObject: WMTPostApprovalScreen? + + required init(from decoder: Decoder) throws { + postApprovalObject = try WMTPostApprovalScreen.decode(decoder: decoder) + } +} diff --git a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift index cfd9b89..b9bf8d0 100644 --- a/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift +++ b/WultraMobileTokenSDK/Operations/Model/UserOperation/WMTUserOperation.swift @@ -53,4 +53,9 @@ open class WMTUserOperation: WMTOperation, Codable { /// tapping an approve button. If the operation requires 2FA, this value also hints if /// the user may use the biometry, or if a password is required. public let allowedSignatureType: WMTAllowedOperationSignature + + /// Additional UI data to present + /// + /// Additional UI data such as Pre-Approval Screen or Post-Approval Screen should be presented. + public let ui: WMTOperationUIData? } diff --git a/WultraMobileTokenSDK/Operations/Utils/WMTJsonValue.swift b/WultraMobileTokenSDK/Operations/Utils/WMTJsonValue.swift new file mode 100644 index 0000000..11ef096 --- /dev/null +++ b/WultraMobileTokenSDK/Operations/Utils/WMTJsonValue.swift @@ -0,0 +1,103 @@ +// +// Copyright 2023 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 + +// JSONValue is helper enum to decode generic response +public enum WMTJSONValue: Codable, Equatable { + case string(String) + case int(Int) + case double(Double) + case bool(Bool) + case object([String: WMTJSONValue]) + case array([WMTJSONValue]) + case null + + public subscript(key: String) -> WMTJSONValue? { + if case .object(let object) = self { + return object[key] + } + return nil + } + + public init(from decoder: Decoder) throws { + if let c = try? decoder.singleValueContainer(), + let string = try? c.decode(String.self) { + self = .string(string) + } else if let c = try? decoder.container(keyedBy: AnyCodingKey.self) { + var object = [String: WMTJSONValue]() + for key in c.allKeys { + object[key.stringValue] = try c.decode(WMTJSONValue.self, forKey: key) + } + self = .object(object) + } else if var c = try? decoder.unkeyedContainer() { + var array = [WMTJSONValue]() + while !c.isAtEnd { + array.append(try c.decode(WMTJSONValue.self)) + } + self = .array(array) + } else if let c = try? decoder.singleValueContainer() { + if c.decodeNil() { + self = .null + } else if let bool = try? c.decode(Bool.self) { + self = .bool(bool) + } else if let int = try? c.decode(Int.self) { + self = .int(int) + } else if let double = try? c.decode(Double.self) { + self = .double(double) + } else if let string = try? c.decode(String.self) { + self = .string(string) + } else { + let data = try JSONSerialization.data(withJSONObject: decoder) + if let string = String(data: data, encoding: .utf8) { + self = .object(["error": .string("Unknown JSON pattern"), "json": .string(string)]) + } else { + self = .object(["error": .string("Unknown JSON pattern"), "json": .null]) + } + } + } else { + let data = try JSONSerialization.data(withJSONObject: decoder) + if let string = String(data: data, encoding: .utf8) { + self = .object(["error": .string("Unknown JSON pattern"), "json": .string(string)]) + } else { + self = .object(["error": .string("Unknown JSON pattern"), "json": .null]) + } + } + } + public init(jsonData: Data) throws { + let decoder = JSONDecoder() + self = try decoder.decode(WMTJSONValue.self, from: jsonData) + } +} + +/// Helper struct for key: value decoding +struct AnyCodingKey: CodingKey { + + /// property to hold the key's name as a string + var stringValue: String + + /// optional property to hold the key's index if it's an array index. + var intValue: Int? + + init?(stringValue: String) { + self.stringValue = stringValue + } + + init?(intValue: Int) { + self.stringValue = String(intValue) + self.intValue = intValue + } +} diff --git a/WultraMobileTokenSDKTests/OperationUIDataTests.swift b/WultraMobileTokenSDKTests/OperationUIDataTests.swift new file mode 100644 index 0000000..7cbd21a --- /dev/null +++ b/WultraMobileTokenSDKTests/OperationUIDataTests.swift @@ -0,0 +1,422 @@ +// +// Copyright 2023 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 XCTest +@testable import WultraMobileTokenSDK + +class OperationUIDataTests: XCTestCase { + + + func testPreApprovalWarningResponse() { + guard let result = prepareResult(response: preApprovalResponse) else { + XCTFail("Failed to parse JSON data") + return + } + + let ui = WMTOperationUIData( + flipButtons: true, + blockApprovalOnCall: false, + preApprovalScreen: + .init( + type: .warning, + heading: "Watch out!", + message: "You may become a victim of an attack.", + items: [ + "You activate a new app and allow access to your accounts", + "Make sure the activation takes place on your device", + "If you have been prompted for this operation in connection with a payment, decline it" + ], + approvalType: WMTPreApprovalScreenConfirmAction(rawValue: "SLIDER")!), + postApprovalScreen: nil) + + + XCTAssertEqual(result.ui?.flipButtons, ui.flipButtons) + XCTAssertEqual(result.ui?.blockApprovalOnCall, ui.blockApprovalOnCall) + XCTAssertEqual(result.ui?.preApprovalScreen?.type,ui.preApprovalScreen?.type) + XCTAssertEqual(result.ui?.preApprovalScreen?.heading, ui.preApprovalScreen?.heading) + XCTAssertEqual((result.ui?.preApprovalScreen)?.items, ui.preApprovalScreen?.items) + XCTAssertEqual((result.ui?.preApprovalScreen)?.message, ui.preApprovalScreen?.message) + XCTAssertEqual((result.ui?.preApprovalScreen)?.approvalType, ui.preApprovalScreen?.approvalType) + } + + func testPreApprovalUnknownResponse() { + guard let result = prepareResult(response: preApprovalFutureResponse) else { + XCTFail("Failed to parse JSON data") + return + } + + let ui = WMTOperationUIData( + flipButtons: true, + blockApprovalOnCall: false, + preApprovalScreen: + .init( + type: .unknown, + heading: "Future", + message: "Future is now, old man", + items: [], + approvalType: nil), + postApprovalScreen: nil) + + XCTAssertEqual(result.ui?.preApprovalScreen?.type,ui.preApprovalScreen?.type) + XCTAssertEqual((result.ui?.preApprovalScreen)?.heading, ui.preApprovalScreen?.heading) + } + + func testPostApprovalGenericResponse() { + guard let result = prepareGenericPostApproval(response: genericPostApproval) else { + XCTFail("Failed to parse JSON data") + return + } + + let generic = WMTPostApprovalScreenGeneric( + heading: "Thank you for your order", + message: "You may close the application now.", + payload: try! WMTJSONValue(jsonData: + """ + { + "nestedMessage": "See you next time.", + "integer": 1, + "boolean": true, + "array": ["firstElement", "secondElement"], + "object": { + "nestedObject": "stringValue" + } + } + """.data(using: .utf8)! + ) + ) + + XCTAssertEqual(result.heading, generic.heading) + XCTAssertEqual(result.message, generic.message) + XCTAssertEqual(result.payload, generic.payload) + XCTAssertEqual(result.payload["nestedMessage"], .string("See you next time.")) + XCTAssertEqual(result.payload["integer"], .int(1)) + XCTAssertEqual(result.payload["boolean"], .bool(true)) + XCTAssertEqual(result.payload["array"], .array([.string("firstElement"), .string("secondElement")])) + XCTAssertEqual(result.payload["object"], .object(["nestedObject" : .string("stringValue")]) ) + } + + + func testPostApprovalResponseRedirect() { + guard let result = prepareResult(response: postApprovalResponseRedirect) else { + XCTFail("Failed to parse JSON data") + return + } + + let ui = WMTOperationUIData( + flipButtons: nil, + blockApprovalOnCall: nil, + preApprovalScreen: nil, + postApprovalScreen: + WMTPostApprovalScreenRedirect( + heading: "Thank you for your order", + message: "You will be redirected to the merchant application.", + payload: + WMTRedirectPostApprovalScreenPayload( + text: "Go to the application", + url:"https://www.alza.cz/ubiquiti-unifi-ap-6-pro-d7212937.htm", + countdown: 5), + type: WMTPostApprovalScreen.ScreenType(rawValue: "MERCHANT_REDIRECT")!)) + + XCTAssertEqual(result.ui?.flipButtons, ui.flipButtons) + XCTAssertEqual(result.ui?.blockApprovalOnCall, ui.blockApprovalOnCall) + XCTAssertEqual(result.ui?.preApprovalScreen?.type,ui.preApprovalScreen?.type) + XCTAssertEqual((result.ui?.postApprovalScreen as? WMTPostApprovalScreenRedirect)?.heading, (ui.postApprovalScreen as? WMTPostApprovalScreenRedirect)?.heading) + XCTAssertEqual((result.ui?.postApprovalScreen as? WMTPostApprovalScreenRedirect)?.message, (ui.postApprovalScreen as? WMTPostApprovalScreenRedirect)?.message) + XCTAssertEqual(((result.ui?.postApprovalScreen as? WMTPostApprovalScreenRedirect)?.payload as? WMTRedirectPostApprovalScreenPayload)?.text, ((ui.postApprovalScreen as? WMTPostApprovalScreenRedirect)?.payload as? WMTRedirectPostApprovalScreenPayload)?.text) + XCTAssertEqual( + ((result.ui?.postApprovalScreen as? WMTPostApprovalScreenRedirect)?.payload as? WMTRedirectPostApprovalScreenPayload)?.url, + ((ui.postApprovalScreen as? WMTPostApprovalScreenRedirect)?.payload as? WMTRedirectPostApprovalScreenPayload)?.url + ) + XCTAssertEqual( + ((result.ui?.postApprovalScreen as? WMTPostApprovalScreenRedirect)?.payload as? WMTRedirectPostApprovalScreenPayload)?.countdown, + ((ui.postApprovalScreen as? WMTPostApprovalScreenRedirect)?.payload as? WMTRedirectPostApprovalScreenPayload)?.countdown + ) + } + + func testPostApprovalResponseReview() { + guard let result = prepareResult(response: postApprovalResponseReview) else { + XCTFail("Failed to parse JSON data") + return + } + + let ui = WMTOperationUIData( + flipButtons: nil, + blockApprovalOnCall: nil, + preApprovalScreen: nil, + postApprovalScreen: + WMTPostApprovalScreenReview( + heading: "Successful", + message: "The operation was approved.", + payload: + WMTReviewPostApprovalScreenPayload( + attributes: [ + WMTOperationAttributeNote( + label: WMTOperationAttribute.AttributeLabel( + id: "1", + value: "test label" + ), + note: "myNote" + ) + ] + ), + type: WMTPostApprovalScreen.ScreenType(rawValue: "REVIEW")! + ) + ) + let resultPostApproval = result.ui?.postApprovalScreen as? WMTPostApprovalScreenReview + let uiPostApproval = ui.postApprovalScreen as? WMTPostApprovalScreenReview + + XCTAssertEqual(resultPostApproval?.heading, uiPostApproval?.heading) + XCTAssertEqual(resultPostApproval?.message, uiPostApproval?.message) + + let resultNoteAttribute = (resultPostApproval?.payload as? WMTReviewPostApprovalScreenPayload)?.attributes[0] as? WMTOperationAttributeNote + let uiNoteAttribute = (uiPostApproval?.payload as? WMTReviewPostApprovalScreenPayload)?.attributes[0] as? WMTOperationAttributeNote + + XCTAssertEqual(resultNoteAttribute?.note, uiNoteAttribute?.note) + + let resultAttributeLabel = (((result.ui?.postApprovalScreen as? WMTPostApprovalScreenReview)?.payload as? WMTReviewPostApprovalScreenPayload)?.attributes[0] as? WMTOperationAttributeNote)?.label as? WMTOperationAttribute.AttributeLabel + let uiAttributeLabel = (((ui.postApprovalScreen as? WMTPostApprovalScreenReview)?.payload as? WMTReviewPostApprovalScreenPayload)?.attributes[0] as? WMTOperationAttributeNote)?.label as? WMTOperationAttribute.AttributeLabel + + XCTAssertEqual(resultAttributeLabel?.id, uiAttributeLabel?.id) + XCTAssertEqual(resultAttributeLabel?.value, uiAttributeLabel?.value) + } + + + // MARK: Helpers + private func prepareResult(response: String) -> WMTUserOperation? { + let result = try? jsonDecoder.decode(WMTUserOperation.self, from: response.data(using: .utf8)!) + return result + } + + private func prepareGenericPostApproval(response: String) -> WMTPostApprovalScreenGeneric? { + let result = try? jsonDecoder.decode(WMTPostApprovalScreenGeneric.self, from: response.data(using: .utf8)!) + return result + } + + private let jsonDecoder: JSONDecoder = { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + return decoder + }() + + private let preApprovalResponse: String = { + """ + { + "id": "74654880-6db9-4b84-9174-386fc5e7d8ab", + "name": "authorize_payment_preApproval", + "data": "A1*A100.00EUR*ICZ3855000000003643174999", + "status": "PENDING", + "operationCreated": "2023-04-25T13:09:52+0000", + "operationExpires": "2023-04-25T13:14:52+0000", + "ui": { + "flipButtons": true, + "blockApprovalOnCall": false, + "preApprovalScreen": { + "type": "WARNING", + "heading": "Watch out!", + "message": "You may become a victim of an attack.", + "items": ["You activate a new app and allow access to your accounts", "Make sure the activation takes place on your device", "If you have been prompted for this operation in connection with a payment, decline it"], + "approvalType": "SLIDER" + } + }, + "allowedSignatureType": { + "type": "2FA", + "variants": ["possession_knowledge", "possession_biometry"] + }, + "formData": { + "title": "Payment Approval", + "message": "Please confirm the payment", + "attributes": [{ + "type": "AMOUNT", + "id": "operation.amount", + "label": "Amount", + "amount": 100.00, + "currency": "EUR", + "amountFormatted": "100,00", + "currencyFormatted": "€" + }, { + "type": "KEY_VALUE", + "id": "operation.account", + "label": "To Account", + "value": "CZ3855000000003643174999" + }] + } + } + """ + }() + + private let preApprovalFutureResponse: String = { + """ + { + "id": "74654880-6db9-4b84-9174-386fc5e7d8ab", + "name": "authorize_payment_preApproval", + "data": "A1*A100.00EUR*ICZ3855000000003643174999", + "status": "PENDING", + "operationCreated": "2023-04-25T13:09:52+0000", + "operationExpires": "2023-04-25T13:14:52+0000", + "ui": { + "flipButtons": true, + "blockApprovalOnCall": false, + "preApprovalScreen": { + "type": "FUTURE", + "heading": "Future", + "message": "Future is now, old man.", + "items": [] + } + }, + "allowedSignatureType": { + "type": "2FA", + "variants": ["possession_knowledge", "possession_biometry"] + }, + "formData": { + "title": "Payment Approval", + "message": "Please confirm the payment", + "attributes": [{ + "type": "AMOUNT", + "id": "operation.amount", + "label": "Amount", + "amount": 100.00, + "currency": "EUR", + "amountFormatted": "100,00", + "currencyFormatted": "€" + }, { + "type": "KEY_VALUE", + "id": "operation.account", + "label": "To Account", + "value": "CZ3855000000003643174999" + }] + } + } + """ + }() + + private let postApprovalResponseRedirect: String = { + """ + { + "id": "f68f6e70-a3d8-4616-b138-358e1799599d", + "name": "authorize_payment_postApproval", + "data": "A1*A100.00EUR*ICZ3855000000003643174999", + "status": "PENDING", + "operationCreated": "2023-04-25T12:29:23+0000", + "operationExpires": "2023-04-25T12:34:23+0000", + "ui": { + "postApprovalScreen": { + "type": "MERCHANT_REDIRECT", + "heading": "Thank you for your order", + "message": "You will be redirected to the merchant application.", + "payload": { + "redirectText": "Go to the application", + "redirectUrl": "https://www.alza.cz/ubiquiti-unifi-ap-6-pro-d7212937.htm", + "countdown": 5 + } + } + }, + "allowedSignatureType": { + "type": "2FA", + "variants": ["possession_knowledge", "possession_biometry"] + }, + "formData": { + "title": "Payment Approval", + "message": "Please confirm the payment", + "attributes": [{ + "type": "AMOUNT", + "id": "operation.amount", + "label": "Amount", + "amount": 100.00, + "currency": "EUR", + "amountFormatted": "100,00", + "currencyFormatted": "€" + }, { + "type": "KEY_VALUE", + "id": "operation.account", + "label": "To Account", + "value": "CZ3855000000003643174999" + }] + } + } + """ + }() + + private let postApprovalResponseReview: String = { + """ + { + "id": "f68f6e70-a3d8-4616-b138-358e1799599d", + "name": "authorize_payment_postApproval", + "data": "A1*A100.00EUR*ICZ3855000000003643174999", + "status": "PENDING", + "operationCreated": "2023-04-25T12:29:23+0000", + "operationExpires": "2023-04-25T12:34:23+0000", + "ui": { + "postApprovalScreen": { + "type": "REVIEW", + "heading": "Successful", + "message": "The operation was approved.", + "payload": { + "attributes": [ + { + "type": "NOTE", + "id": "1", + "label": "test label", + "note": "myNote" + } + ] + } + } + }, + "allowedSignatureType": { + "type": "2FA", + "variants": ["possession_knowledge", "possession_biometry"] + }, + "formData": { + "title": "Payment Approval", + "message": "Please confirm the payment", + "attributes": [{ + "type": "AMOUNT", + "id": "operation.amount", + "label": "Amount", + "amount": 100.00, + "currency": "EUR", + "amountFormatted": "100,00", + "currencyFormatted": "€" + }, { + "type": "KEY_VALUE", + "id": "operation.account", + "label": "To Account", + "value": "CZ3855000000003643174999" + }] + } + } + """ + }() + + private let genericPostApproval: String = { + """ + { + "type": "GENERIC", + "heading": "Thank you for your order", + "message": "You may close the application now.", + "payload": { + "nestedMessage": "See you next time.", + "integer": 1, + "boolean": true, + "array": ["firstElement", "secondElement"], + "object": { + "nestedObject": "stringValue" + } + } + } + """ + }() +} diff --git a/docs/Using-Operations-Service.md b/docs/Using-Operations-Service.md index eb189d1..e29ae2f 100644 --- a/docs/Using-Operations-Service.md +++ b/docs/Using-Operations-Service.md @@ -431,6 +431,11 @@ class WMTUserOperation: WMTOperation { /// tapping an approve button. If the operation requires 2FA, this value also hints if /// the user may use the biometry, or if a password is required. public let allowedSignatureType: WMTAllowedOperationSignature + + /// Additional UI data to present + /// + /// Additional UI data such as Pre-Approval Screen or Post-Approval Screen should be presented. + public let ui: WMTOperationUIData? } ``` @@ -463,6 +468,40 @@ Attributes types: - `IMAGE` image row - `UNKNOWN` fallback option when unknown attribute type is passed. Such attribute only contains the label. +Definition of `WMTOperationUIData`: + +```swift +open class WMTOperationUIData: Codable { + /// Confirm and Reject buttons should be flipped both in position and style + public let flipButtons: Bool? + + /// Block approval when on call (for example when on a phone or Skype call) + public let blockApprovalOnCall: Bool? + + /// UI for pre-approval operation screen + public let preApprovalScreen: WMTPreApprovalScreen? + + /// UI for post-approval opration screen + /// + /// Type of PostApprovalScrren is presented with different classes (Starting with `WMTPostApprovalScreen*`) + public let postApprovalScreen: WMTPostApprovalScreen? +} +``` + +PreApprovalScreen types: + +- `WARNING` +- `INFO` +- `QR_SCAN` +- `UNKNOWN` + +PostApprovalScreen types: +`WMTPostApprovalScreen*` classes commonly contain `heading` and `message` and different payload data + +- `REVIEW` provides an array of operations attributes with data: type, id, label, and note +- `REDIRECT` providing text for button, countdown, and redirection URL +- `GENERIC` may contain any object + ### Subclassing WMTUserOperation `WMTUserOperation` class is `open` and can be subclassed. This is useful when your backend adds additional properties to operations retrieved via the `getOperations` API.