Skip to content

Commit

Permalink
Implement WMTTemplates
Browse files Browse the repository at this point in the history
  • Loading branch information
Hopsaheysa committed Jun 27, 2024
1 parent cca38ed commit 21375c4
Show file tree
Hide file tree
Showing 6 changed files with 429 additions and 2 deletions.
4 changes: 4 additions & 0 deletions WultraMobileTokenSDK.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -159,6 +160,7 @@
EA6DDF1929F804D60011E234 /* WMTPostApprovalScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTPostApprovalScreen.swift; sourceTree = "<group>"; };
EA6DDF1B29F807230011E234 /* OperationUIDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationUIDataTests.swift; sourceTree = "<group>"; };
EA7A6E572B0E639800C1D4F4 /* WMTOperationDetailRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTOperationDetailRequest.swift; sourceTree = "<group>"; };
EA9795122C2C18450073E861 /* WMTTemplates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WMTTemplates.swift; sourceTree = "<group>"; };
EA9CE2BD2AEAA9FD00FE4E35 /* WMTProximityCheck.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WMTProximityCheck.swift; sourceTree = "<group>"; };
EA9CE2C12AEBDB0D00FE4E35 /* WMTPACUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WMTPACUtils.swift; sourceTree = "<group>"; };
EAB705492AF1161500756AC2 /* PACParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PACParserTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -232,6 +234,7 @@
DCE5EAAF26BD81150061861A /* WMTOperationHistoryEntry.swift */,
EA294F3C29F6A07A00A0494E /* WMTOperationUIData.swift */,
EA9CE2BD2AEAA9FD00FE4E35 /* WMTProximityCheck.swift */,
EA9795122C2C18450073E861 /* WMTTemplates.swift */,
);
path = UserOperation;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
}

Expand Down
217 changes: 217 additions & 0 deletions WultraMobileTokenSDK/Operations/Model/UserOperation/WMTTemplates.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
}

92 changes: 92 additions & 0 deletions WultraMobileTokenSDKTests/OperationUIDataTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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? {
Expand All @@ -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()
Expand Down Expand Up @@ -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"
}
]
}
]
}
}
}
"""
}()
}
Loading

0 comments on commit 21375c4

Please sign in to comment.