From b3df8aa209785f98f433fe259935e5c6a71908c2 Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Wed, 10 Jul 2024 15:57:47 +0200 Subject: [PATCH 1/6] Implement Templates --- .../androidTest/java/OperationUIDataTests.kt | 94 ++++++++++++++++++ .../api/operation/model/Templates.kt | 98 +++++++++++++++++++ .../api/operation/model/UserOperation.kt | 10 +- 3 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 library/src/main/java/com/wultra/android/mtokensdk/api/operation/model/Templates.kt diff --git a/library/src/androidTest/java/OperationUIDataTests.kt b/library/src/androidTest/java/OperationUIDataTests.kt index 438f832..6f76746 100644 --- a/library/src/androidTest/java/OperationUIDataTests.kt +++ b/library/src/androidTest/java/OperationUIDataTests.kt @@ -208,6 +208,44 @@ class OperationUIDataTests { assertEquals(postApprovalGenericResult.payload["object"], JSONValue.JSONObject(mapOf("nestedObject" to JSONValue.JSONString("stringValue")))) } + @Test + fun testTemplates() { + val uiResult = prepareUIData(uiDataWithTemplates) + if (uiResult == null) { + assert(false) { "Failed to parse JSON data" } + return + } + + assertEquals("POSITIVE", uiResult.templates?.list?.style) + assertEquals("\${operation.request_no} Withdrawal Initiation", uiResult.templates?.list?.header) + assertEquals("\${operation.account} · \${operation.enterprise}", uiResult.templates?.list?.title) + assertEquals("\${operation.tx_amount}", uiResult.templates?.list?.message) + assertEquals("operation.image", uiResult.templates?.list?.image) + + assertEquals(null, uiResult.templates?.detail?.style) + assertEquals(false, uiResult.templates?.detail?.showTitleAndMessage) + + assertEquals("MONEY", uiResult.templates?.detail?.sections?.get(0)?.style) + assertEquals("operation.money.header", uiResult.templates?.detail?.sections?.get(0)?.title) + assertEquals(null, uiResult.templates?.detail?.sections?.get(0)?.cells?.get(0)?.style) + assertEquals("operation.amount", uiResult.templates?.detail?.sections?.get(0)?.cells?.get(0)?.name) + assertEquals(false, uiResult.templates?.detail?.sections?.get(0)?.cells?.get(0)?.visibleTitle) + assertEquals(true, uiResult.templates?.detail?.sections?.get(0)?.cells?.get(0)?.canCopy) + assertEquals(Templates.DetailTemplate.Section.Cell.Collapsable.NO, uiResult.templates?.detail?.sections?.get(0)?.cells?.get(0)?.collapsable) + + assertEquals("CONVERSION", uiResult.templates?.detail?.sections?.get(0)?.cells?.get(1)?.style) + assertEquals("operation.conversion", uiResult.templates?.detail?.sections?.get(0)?.cells?.get(1)?.name) + assertEquals(null, uiResult.templates?.detail?.sections?.get(0)?.cells?.get(1)?.visibleTitle) + assertEquals(true, uiResult.templates?.detail?.sections?.get(0)?.cells?.get(1)?.canCopy) + assertEquals(Templates.DetailTemplate.Section.Cell.Collapsable.NO, uiResult.templates?.detail?.sections?.get(0)?.cells?.get(1)?.collapsable) + + assertEquals(null, uiResult.templates?.detail?.sections?.get(0)?.cells?.get(2)?.style) + assertEquals("operation.conversion2", uiResult.templates?.detail?.sections?.get(0)?.cells?.get(2)?.name) + assertEquals(true, uiResult.templates?.detail?.sections?.get(0)?.cells?.get(2)?.visibleTitle) + assertEquals(false, uiResult.templates?.detail?.sections?.get(0)?.cells?.get(2)?.canCopy) + assertEquals(Templates.DetailTemplate.Section.Cell.Collapsable.COLLAPSED, uiResult.templates?.detail?.sections?.get(0)?.cells?.get(2)?.collapsable) + } + /** Helpers */ private val jsonDecoder: Gson = OperationsUtils.defaultGsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").create() @@ -220,6 +258,14 @@ class OperationUIDataTests { return result } + private fun prepareUIData(response: String): OperationUIData? { + return try { + jsonDecoder.fromJson(response, OperationUIData::class.java) + } catch (e: Exception) { + null + } + } + private val preApprovalResponse: String = """ { "id": "74654880-6db9-4b84-9174-386fc5e7d8ab", @@ -451,4 +497,52 @@ class OperationUIDataTests { } } """ + + private val uiDataWithTemplates: String = """ + { + "flipButtons": false, + "blockApprovalOnCall": true, + "templates": { + "list": { + "style": "POSITIVE", + "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": "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/library/src/main/java/com/wultra/android/mtokensdk/api/operation/model/Templates.kt b/library/src/main/java/com/wultra/android/mtokensdk/api/operation/model/Templates.kt new file mode 100644 index 0000000..9b15f14 --- /dev/null +++ b/library/src/main/java/com/wultra/android/mtokensdk/api/operation/model/Templates.kt @@ -0,0 +1,98 @@ +/* + * 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. + */ + +package com.wultra.android.mtokensdk.api.operation.model + + +/** + * This typealias specifies that attributes using it should refer to `OperationAttributes`. + * + * AttributeId is supposed to be `OperationAttribute.Label.id` + */ +typealias AttributeId = String + +/** + * This typealias specifies that attributes using might refer to `OperationAttributes` + * and additional characters and might require additional parsing . + * + * Example might be `"${operation.date} - ${operation.place}"` + */ +typealias AttributeFormatted = String + +/** + * Detailed information about displaying operation data + * + * Contains prearranged styles for the operation attributes for the app to display + */ +data class Templates( + val list: ListTemplate?, + val detail: DetailTemplate? +) { + + /** + * ListTemplate defines how the operation should look in the list (active operations, history) + * + * List cell usually contains header, title, message(subtitle) and image + */ + data class ListTemplate( + val style: String?, + val header: AttributeFormatted?, + val title: AttributeFormatted?, + val message: AttributeFormatted?, + val image: AttributeId? + ) + + /** + * 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. + */ + data class DetailTemplate( + val style: String?, + val showTitleAndMessage: Boolean?, + val sections: List
? + ) { + + /** + * Operation data can be divided into sections + */ + data class Section( + val style: String?, + val title: AttributeId?, + val cells: List? + ) { + + /** + * Each section can have multiple cells of data + */ + data class Cell( + val name: AttributeId, + val style: String?, + val visibleTitle: Boolean?, + val canCopy: Boolean?, + val collapsable: Collapsable? + ) { + + enum class Collapsable { + NO, + COLLAPSED, + YES + } + } + } + } +} diff --git a/library/src/main/java/com/wultra/android/mtokensdk/api/operation/model/UserOperation.kt b/library/src/main/java/com/wultra/android/mtokensdk/api/operation/model/UserOperation.kt index b85837f..b7e8d0e 100644 --- a/library/src/main/java/com/wultra/android/mtokensdk/api/operation/model/UserOperation.kt +++ b/library/src/main/java/com/wultra/android/mtokensdk/api/operation/model/UserOperation.kt @@ -192,7 +192,15 @@ data class OperationUIData( * Type of PostApprovalScreen is presented with different classes (Starting with `PostApprovalScreen*`) */ @SerializedName("postApprovalScreen") - val postApprovalScreen: PostApprovalScreen? + val postApprovalScreen: PostApprovalScreen?, + + /** + * Detailed information about displaying the operation data + * + * Contains prearranged styles for the operation attributes for the app to display + */ + @SerializedName("templates") + val templates: Templates? = null ) /** From 391c3af7c400691467f8498271f123af9ce85fa1 Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Mon, 5 Aug 2024 14:30:50 +0200 Subject: [PATCH 2/6] Implement Templates deserializer --- .../androidTest/java/OperationUIDataTests.kt | 63 +++++- .../api/operation/TemplatesDeserializer.kt | 183 ++++++++++++++++++ .../api/operation/model/Templates.kt | 61 +++++- .../mtokensdk/operation/OperationsUtils.kt | 6 + 4 files changed, 303 insertions(+), 10 deletions(-) create mode 100644 library/src/main/java/com/wultra/android/mtokensdk/api/operation/TemplatesDeserializer.kt diff --git a/library/src/androidTest/java/OperationUIDataTests.kt b/library/src/androidTest/java/OperationUIDataTests.kt index 6f76746..bfa1187 100644 --- a/library/src/androidTest/java/OperationUIDataTests.kt +++ b/library/src/androidTest/java/OperationUIDataTests.kt @@ -21,6 +21,7 @@ import com.wultra.android.mtokensdk.api.operation.model.* import com.wultra.android.mtokensdk.operation.JSONValue import com.wultra.android.mtokensdk.operation.OperationsUtils import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNull import junit.framework.TestCase.fail import org.junit.Test @@ -208,11 +209,26 @@ class OperationUIDataTests { assertEquals(postApprovalGenericResult.payload["object"], JSONValue.JSONObject(mapOf("nestedObject" to JSONValue.JSONString("stringValue")))) } + @Test + fun testListTemplates() { + val uiResult = prepareUIData(templatesList) + if (uiResult == null) { + fail("Failed to parse JSON data") + return + } + + assertEquals("POSITIVE", uiResult.templates?.list?.style) + assertEquals("$\\{operation.request_no} Withdrawal Initiation", uiResult.templates?.list?.header) + assertNull(uiResult.templates?.list?.title) + assertNull(uiResult.templates?.list?.message) + assertNull(uiResult.templates?.list?.image) + } + @Test fun testTemplates() { val uiResult = prepareUIData(uiDataWithTemplates) if (uiResult == null) { - assert(false) { "Failed to parse JSON data" } + fail("Failed to parse JSON data") return } @@ -244,6 +260,11 @@ class OperationUIDataTests { assertEquals(true, uiResult.templates?.detail?.sections?.get(0)?.cells?.get(2)?.visibleTitle) assertEquals(false, uiResult.templates?.detail?.sections?.get(0)?.cells?.get(2)?.canCopy) assertEquals(Templates.DetailTemplate.Section.Cell.Collapsable.COLLAPSED, uiResult.templates?.detail?.sections?.get(0)?.cells?.get(2)?.collapsable) + + assertEquals(3, uiResult.templates?.detail?.sections?.get(0)?.cells?.size) + + assertNull(uiResult.templates?.detail?.sections?.get(1)?.cells) + assertNull(uiResult.templates?.detail?.sections?.get(2)?.cells) } /** Helpers */ @@ -498,8 +519,23 @@ class OperationUIDataTests { } """ - private val uiDataWithTemplates: String = """ + private val templatesList: String = """ { + "flipButtons": false, + "blockApprovalOnCall": true, + "templates": { + "list": { + "style": "POSITIVE", + "header": "${'$'}\\{operation.request_no} Withdrawal Initiation", + "message": null, + "image": true + } + } + } + """ + + private val uiDataWithTemplates: String = """ +{ "flipButtons": false, "blockApprovalOnCall": true, "templates": { @@ -523,7 +559,8 @@ class OperationUIDataTests { "visibleTitle": false, "style": null, "canCopy": true, - "collapsable": "NO" + "collapsable": "NO", + "centered": true }, { "style": "CONVERSION", @@ -537,8 +574,28 @@ class OperationUIDataTests { "style": null, "canCopy": false, "collapsable": "COLLAPSED" + }, + { + "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 + } } ] } diff --git a/library/src/main/java/com/wultra/android/mtokensdk/api/operation/TemplatesDeserializer.kt b/library/src/main/java/com/wultra/android/mtokensdk/api/operation/TemplatesDeserializer.kt new file mode 100644 index 0000000..aa43bad --- /dev/null +++ b/library/src/main/java/com/wultra/android/mtokensdk/api/operation/TemplatesDeserializer.kt @@ -0,0 +1,183 @@ +/* + * Copyright 2022 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. + */ + +package com.wultra.android.mtokensdk.api.operation + +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.reflect.TypeToken +import com.wultra.android.mtokensdk.api.operation.model.Templates +import com.wultra.android.mtokensdk.log.WMTLogger +import java.lang.reflect.Type + +/** + * Custom Templates deserializer + */ + +internal class TemplatesDeserializer : JsonDeserializer { + + override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Templates { + val jsonObject = json.asJsonObject + + val listTemplate = jsonObject.get("list")?.let { listElement -> + try { + context.deserialize(listElement, Templates.ListTemplate::class.java) + } catch (e: Exception) { + WMTLogger.e("Failed to deserialize ListTemplate - ${e.message}") + null + } + } + + val detailTemplate = jsonObject.get("detail")?.let { detailElement -> + try { + context.deserialize(detailElement, Templates.DetailTemplate::class.java) + } catch (e: Exception) { + WMTLogger.e("Failed to deserialize DetailTemplate - ${e.message}") + null + } + } + + return Templates(listTemplate, detailTemplate) + } +} + +internal class ListTemplateDeserializer : JsonDeserializer { + override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Templates.ListTemplate { + val jsonObject = json.asJsonObject + + val style: String? = jsonObject.get("style")?.asStringWithLogging("ListTemplate.style") + val header: String? = jsonObject.get("header")?.asStringWithLogging("ListTemplate.header") + val title: String? = jsonObject.get("title")?.asStringWithLogging("ListTemplate.title") + val message: String? = jsonObject.get("message")?.asStringWithLogging("ListTemplate.message") + val image: String? = jsonObject.get("image")?.asStringWithLogging("ListTemplate.image") + + return Templates.ListTemplate(style, header, title, message, image) + } +} + +internal class DetailTemplateDeserializer : JsonDeserializer { + override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Templates.DetailTemplate { + val jsonObject = json.asJsonObject + + val style: String? = jsonObject.get("style")?.asStringWithLogging("DetailTemplate.style") + val showTitleAndMessage: Boolean? = jsonObject.get("showTitleAndMessage")?.asBooleanWithLogging("DetailTemplate.showTitleAndMessage") + + val sections: List? = try { + val sectionsElement = jsonObject.get("sections") + val sectionType = object : TypeToken>() {}.type + context.deserialize>(sectionsElement, sectionType) + } catch (e: Exception) { + WMTLogger.e("Failed to decode 'DetailTemplate.sections' - ${e.message}, setting to null") + null + } + + return Templates.DetailTemplate(style, showTitleAndMessage, sections) + } +} + +internal class SectionDeserializer : JsonDeserializer { + override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Templates.DetailTemplate.Section { + val jsonObject = json.asJsonObject + + val style: String? = jsonObject.get("style")?.asStringWithLogging("ListTemplate.Section.style") + val title: String? = jsonObject.get("title")?.asStringWithLogging("ListTemplate.Section.title") + + + val cells: List? = jsonObject.get("cells")?.let { cellsElement -> + try { + if (cellsElement.isJsonArray) { + val cellList = mutableListOf() + val jsonArray = cellsElement.asJsonArray + jsonArray.forEach { cellElement -> + try { + val cell = context.deserialize(cellElement, Templates.DetailTemplate.Section.Cell::class.java) + cellList.add(cell) + } catch (e: Exception) { + WMTLogger.e("Failed to decode cell in DetailTemplate.Section.cells - ${e.message}") + } + } + cellList + } else { + WMTLogger.e("Failed to decode 'DetailTemplate.Sections.cells' - Expected a JSON array, setting to null") + null + } + } catch (e: Exception) { + WMTLogger.e("Failed to decode DetailTemplate.Section.cells - ${e.message}, setting to null") + null + } + } + + + return Templates.DetailTemplate.Section(style, title, cells) + } +} + +internal class CellDeserializer : JsonDeserializer { + override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Templates.DetailTemplate.Section.Cell { + val jsonObject = json.asJsonObject + + val name = jsonObject.get("name").asString + + val style: String? = jsonObject.get("style")?.asStringWithLogging("DetailTemplate.Section.Cell.style") + val visibleTitle: Boolean? = jsonObject.get("visibleTitle")?.asBooleanWithLogging("DetailTemplate.Section.Cell.visibleTitle") + val canCopy: Boolean? = jsonObject.get("canCopy")?.asBooleanWithLogging("DetailTemplate.Section.Cell.canCopy") + + val collapsable = jsonObject.get("collapsable")?.asStringWithLogging("DetailTemplate.Section.Cell.collapsable")?.let { + Templates.DetailTemplate.Section.Cell.Collapsable.valueOf(it) + } + + val centered: Boolean? = jsonObject.get("centered")?.asBooleanWithLogging("DetailTemplate.Section.Cell.centered") + + return Templates.DetailTemplate.Section.Cell(name, style, visibleTitle, canCopy, collapsable, centered) + } +} + +private fun JsonElement.asStringWithLogging(fieldName: String): String? { + try { + if (this.isJsonNull) { + return null + } + + if (this.isJsonPrimitive && !this.asJsonPrimitive.isString) { + WMTLogger.e("Failed to decode '$fieldName' - $this is not a String, setting to null") + return null + } + + return this.asString + } catch (e: Exception) { + WMTLogger.e("Failed to decode '$fieldName' - ${e.message}, setting to null") + return null + } +} + +private fun JsonElement.asBooleanWithLogging(fieldName: String): Boolean? { + try { + if (this.isJsonNull) { + return null + } + + if (this.isJsonPrimitive && !this.asJsonPrimitive.isBoolean) { + WMTLogger.e("Failed to decode '$fieldName' - $this is not a Boolean, setting to null") + return null + } + + return this.asBoolean + } catch (e: Exception) { + WMTLogger.e("Failed to decode '$fieldName' - ${e.message}, setting to null") + return null + } +} diff --git a/library/src/main/java/com/wultra/android/mtokensdk/api/operation/model/Templates.kt b/library/src/main/java/com/wultra/android/mtokensdk/api/operation/model/Templates.kt index 9b15f14..c2ee444 100644 --- a/library/src/main/java/com/wultra/android/mtokensdk/api/operation/model/Templates.kt +++ b/library/src/main/java/com/wultra/android/mtokensdk/api/operation/model/Templates.kt @@ -16,19 +16,17 @@ package com.wultra.android.mtokensdk.api.operation.model - /** - * This typealias specifies that attributes using it should refer to `OperationAttributes`. - * - * AttributeId is supposed to be `OperationAttribute.Label.id` + * Value of the `AttributeId` is referencing an existing `WMTOperationAttribute` by `WMTOperationAttribute.AttributeLabel.id` */ typealias AttributeId = String /** - * This typealias specifies that attributes using might refer to `OperationAttributes` - * and additional characters and might 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. */ typealias AttributeFormatted = String @@ -38,7 +36,15 @@ typealias AttributeFormatted = String * Contains prearranged styles for the operation attributes for the app to display */ data class Templates( + + /** + * How the operation should look like in the list of operations + */ val list: ListTemplate?, + + /** + * How the operation detail should look like when viewed individually. + */ val detail: DetailTemplate? ) { @@ -48,10 +54,20 @@ data class Templates( * List cell usually contains header, title, message(subtitle) and image */ data class ListTemplate( + + /** Prearranged name which can be processed by the app */ val style: String?, + + /** Attribute which will be used for the header */ val header: AttributeFormatted?, + + /** Attribute which will be used for the title */ val title: AttributeFormatted?, + + /** Attribute which will be used for the message */ val message: AttributeFormatted?, + + /** Attribute which will be used for the image */ val image: AttributeId? ) @@ -62,8 +78,14 @@ data class Templates( * Attributes not mentioned in the `DetailTemplate` should be displayed without custom styling. */ data class DetailTemplate( + + /** Predefined style name that can be processed by the app to customize the overall look of the operation. */ val style: String?, + + /** Indicates if the header should be created from form data (title, message, image) or customized for a specific operation */ val showTitleAndMessage: Boolean?, + + /** Sections of the operation data. */ val sections: List
? ) { @@ -71,8 +93,14 @@ data class Templates( * Operation data can be divided into sections */ data class Section( + + /** Prearranged name which can be processed by the app to customize the section */ val style: String?, + + /** Attribute for section title */ val title: AttributeId?, + + /** Each section can have multiple cells of data */ val cells: List? ) { @@ -80,16 +108,35 @@ data class Templates( * Each section can have multiple cells of data */ data class Cell( + + /** Which attribute shall be used */ val name: AttributeId, + + /** Prearranged name which can be processed by the app to customize the cell */ val style: String?, + + /** Should be the title visible or hidden */ val visibleTitle: Boolean?, + + /** Should be the content copyable */ val canCopy: Boolean?, - val collapsable: Collapsable? + + /** Define if the cell should be collapsable */ + val collapsable: Collapsable?, + + /** If value should be centered */ + val centered: Boolean? ) { enum class Collapsable { + + /** The cell should not be collapsable */ NO, + + /** The cell should be collapsable and in collapsed state */ COLLAPSED, + + /** The cell should be collapsable and in expanded state */ YES } } diff --git a/library/src/main/java/com/wultra/android/mtokensdk/operation/OperationsUtils.kt b/library/src/main/java/com/wultra/android/mtokensdk/operation/OperationsUtils.kt index a7700a3..e5ee365 100644 --- a/library/src/main/java/com/wultra/android/mtokensdk/operation/OperationsUtils.kt +++ b/library/src/main/java/com/wultra/android/mtokensdk/operation/OperationsUtils.kt @@ -25,6 +25,7 @@ import com.wultra.android.mtokensdk.api.operation.model.Attribute import com.wultra.android.mtokensdk.api.operation.model.OperationHistoryEntry import com.wultra.android.mtokensdk.api.operation.model.PostApprovalScreen import com.wultra.android.mtokensdk.api.operation.model.PreApprovalScreen +import com.wultra.android.mtokensdk.api.operation.model.Templates import org.threeten.bp.ZonedDateTime class OperationsUtils { @@ -49,6 +50,11 @@ class OperationsUtils { builder.registerTypeAdapter(OperationHistoryEntry::class.java, OperationHistoryEntryDeserializer()) builder.registerTypeAdapter(PreApprovalScreen::class.java, PreApprovalScreenDeserializer()) builder.registerTypeAdapter(PostApprovalScreen::class.java, PostApprovalScreenDeserializer()) + builder.registerTypeAdapter(Templates::class.java, TemplatesDeserializer()) + builder.registerTypeAdapter(Templates.ListTemplate::class.java, ListTemplateDeserializer()) + builder.registerTypeAdapter(Templates.DetailTemplate::class.java, DetailTemplateDeserializer()) + builder.registerTypeAdapter(Templates.DetailTemplate.Section::class.java, SectionDeserializer()) + builder.registerTypeAdapter(Templates.DetailTemplate.Section.Cell::class.java, CellDeserializer()) return builder } } From 95b4ecca274bffaae56ac01afc5783b50fe45e1d Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Mon, 5 Aug 2024 15:50:58 +0200 Subject: [PATCH 3/6] Add Visual Parser --- .../api/operation/TemplatesDeserializer.kt | 2 - .../templateparser/TemplateDetailVisual.kt | 299 ++++++++++++++++++ .../templateparser/TemplateListVisual.kt | 132 ++++++++ .../templateparser/TemplateVisualParser.kt | 51 +++ 4 files changed, 482 insertions(+), 2 deletions(-) create mode 100644 library/src/main/java/com/wultra/android/mtokensdk/api/operation/templateparser/TemplateDetailVisual.kt create mode 100644 library/src/main/java/com/wultra/android/mtokensdk/api/operation/templateparser/TemplateListVisual.kt create mode 100644 library/src/main/java/com/wultra/android/mtokensdk/api/operation/templateparser/TemplateVisualParser.kt diff --git a/library/src/main/java/com/wultra/android/mtokensdk/api/operation/TemplatesDeserializer.kt b/library/src/main/java/com/wultra/android/mtokensdk/api/operation/TemplatesDeserializer.kt index aa43bad..5d4b82c 100644 --- a/library/src/main/java/com/wultra/android/mtokensdk/api/operation/TemplatesDeserializer.kt +++ b/library/src/main/java/com/wultra/android/mtokensdk/api/operation/TemplatesDeserializer.kt @@ -96,7 +96,6 @@ internal class SectionDeserializer : JsonDeserializer? = jsonObject.get("cells")?.let { cellsElement -> try { if (cellsElement.isJsonArray) { @@ -121,7 +120,6 @@ internal class SectionDeserializer : JsonDeserializer +) + +/** + * This class defines one section in the detailed view of a user operation. + */ +data class UserOperationVisualSection( + + /** Predefined style of the section to which the app can react and adjust the operation visual */ + val style: String? = null, + + /** The title value for the section */ + val title: String? = null, + + /** An array of cells with `FormData` header and message or visual cells based on `OperationAttributes` */ + val cells: List +) + +/** + * An interface for visual cells in a user operation's detailed view. + */ +interface UserOperationVisualCell + +/** + * `UserOperationHeaderVisualCell` contains a header in a user operation's detail header view. + * + * This data class is used to distinguish between the default header section and custom `OperationAttribute` sections. + */ +data class UserOperationHeaderVisualCell( + + /** This value corresponds to `FormData.title` */ + val value: String +) : UserOperationVisualCell + +/** + * `UserOperationMessageVisualCell` is a message cell in a user operation's header view. + * + * This data class is used within default header section and is used to distinguished from custom `OperationAttribute` sections. + */ +data class UserOperationMessageVisualCell( + + /** This value corresponds to `FormData.message` */ + val value: String +) : UserOperationVisualCell + +/** + * `UserOperationHeadingVisualCell` defines a heading cell in a user operation's detailed view. + */ +data class UserOperationHeadingVisualCell( + + /** Single highlighted text used as a section heading */ + val header: String, + + /** Predefined style of the section cell, app shall react to it and should change the visual of the cell */ + val style: String? = null, + + /** The source user operation attribute. */ + val attribute: Attribute, + + /** The template the cell was made from. */ + val cellTemplate: Templates.DetailTemplate.Section.Cell? = null +) : UserOperationVisualCell + +/** + * `UserOperationValueAttributeVisualCell` defines a value attribute cell in a user operation's detailed view. + */ +data class UserOperationValueAttributeVisualCell( + + /** The header text value */ + val header: String, + + /** The text value preformatted for the cell (if the preformatted value isn't sufficient, the value from the attribute can be used) */ + val defaultFormattedStringValue: String, + + /** Predefined style of the section cell, app shall react to it and should change the visual of the cell */ + val style: String? = null, + + /** /// The source user operation attribute. */ + val attribute: Attribute, + + /** The template the cell was made from. */ + val cellTemplate: Templates.DetailTemplate.Section.Cell? = null +) : UserOperationVisualCell + +/** + * `UserOperationImageVisualCell` defines an image cell in a user operation's detailed view. + */ +data class UserOperationImageVisualCell( + + /** The URL of the thumbnail image */ + val urlThumbnail: URL, + + /** The URL of the full size image */ + val urlFull: URL? = null, + + /** Predefined style of the section cell, app shall react to it and should change the visual of the cell */ + val style: String? = null, + + /** The source user operation attribute. */ + val attribute: ImageAttribute, + + /** The template the cell was made from. */ + val cellTemplate: Templates.DetailTemplate.Section.Cell? = null +) : UserOperationVisualCell + +/** + * Extension function to prepare the visual representation for the given `UserOperation` in a detailed view. + */ +fun UserOperation.prepareVisualDetail(): TemplateDetailVisual { + val detailTemplate = this.ui?.templates?.detail + + return if (detailTemplate == null) { + val sections = mutableListOf(createDefaultHeaderVisual()) + if (formData.attributes.isNotEmpty()) { + sections.add(UserOperationVisualSection(cells = formData.attributes.getRemainingCells())) + } + TemplateDetailVisual(style = null, sections = sections) + } else { + createDetailVisual(detailTemplate) + } +} + +private fun UserOperation.createDefaultHeaderVisual(): UserOperationVisualSection { + val defaultHeaderCell = UserOperationHeaderVisualCell(value = this.formData.title) + val defaultMessageCell = UserOperationMessageVisualCell(value = this.formData.message) + + return UserOperationVisualSection( + style = null, + title = null, + cells = listOf(defaultHeaderCell, defaultMessageCell) + ) +} + +private fun UserOperation.createDetailVisual(detailTemplate: Templates.DetailTemplate): TemplateDetailVisual { + val attributes = this.formData.attributes.toMutableList() + + val sections = mutableListOf() + + if (detailTemplate.showTitleAndMessage == false) { + sections.addAll(attributes.popCellsFromSections(detailTemplate.sections)) + sections.add(UserOperationVisualSection(cells = attributes.getRemainingCells())) + } else { + sections.add(createDefaultHeaderVisual()) + sections.addAll(attributes.popCellsFromSections(detailTemplate.sections)) + sections.add(UserOperationVisualSection(cells = attributes.getRemainingCells())) + } + + return TemplateDetailVisual(style = detailTemplate.style, sections = sections) +} + +private fun MutableList.popCellsFromSections( + sections: List? +): List { + return sections?.map { popCellsFromSection(it) } ?: emptyList() +} + +private fun MutableList.popCellsFromSection( + section: Templates.DetailTemplate.Section +): UserOperationVisualSection { + return UserOperationVisualSection( + style = section.style, + title = popAttribute(section.title)?.label?.value, + cells = section.cells?.mapNotNull { createCellFromTemplateCell(it) } ?: emptyList() + ) +} + +private fun MutableList.popAttribute(id: String?): Attribute? { + id?.let { + val index = indexOfFirst { it.label.id == id } + return if (index != -1) removeAt(index) else null + } + return null +} + +private fun MutableList.createCellFromTemplateCell( + templateCell: Templates.DetailTemplate.Section.Cell +): UserOperationVisualCell? { + val attr = popAttribute(templateCell.name) ?: return null.also { + WMTLogger.w("Template Attribute '${templateCell.name}', not found in FormData Attributes") + } + return createCell(attr, templateCell) +} + +private fun List.getRemainingCells(): List { + return mapNotNull { createCell(it) } +} + +private fun createCell( + attr: Attribute, + templateCell: Templates.DetailTemplate.Section.Cell? = null +): UserOperationVisualCell? { + return when (attr.type) { + Attribute.Type.AMOUNT -> { + val amount = attr as? AmountAttribute ?: return null + UserOperationValueAttributeVisualCell( + header = attr.label.value, + defaultFormattedStringValue = amount.valueFormatted + ?: "${amount.amountFormatted} ${amount.currencyFormatted}", + style = templateCell?.style, + attribute = attr, + cellTemplate = templateCell + ) + } + Attribute.Type.AMOUNT_CONVERSION -> { + val conversion = attr as? ConversionAttribute ?: return null + val sourceValue = conversion.source.valueFormatted + ?: "${conversion.source.amountFormatted} ${conversion.source.currencyFormatted}" + val targetValue = conversion.target.valueFormatted + ?: "${conversion.target.amountFormatted} ${conversion.target.currencyFormatted}" + UserOperationValueAttributeVisualCell( + header = attr.label.value, + defaultFormattedStringValue = "$sourceValue → $targetValue", + style = templateCell?.style, + attribute = attr, + cellTemplate = templateCell + ) + } + Attribute.Type.KEY_VALUE -> { + val keyValue = attr as? KeyValueAttribute ?: return null + UserOperationValueAttributeVisualCell( + header = attr.label.value, + defaultFormattedStringValue = keyValue.value, + style = templateCell?.style, + attribute = attr, + cellTemplate = templateCell + ) + } + Attribute.Type.NOTE -> { + val note = attr as? NoteAttribute ?: return null + UserOperationValueAttributeVisualCell( + header = attr.label.value, + defaultFormattedStringValue = note.note, + style = templateCell?.style, + attribute = attr, + cellTemplate = templateCell + ) + } + Attribute.Type.IMAGE -> { + val image = attr as? ImageAttribute ?: return null + UserOperationImageVisualCell( + urlThumbnail = URL(image.thumbnailUrl), + urlFull = image.originalUrl?.let { URL(it) }, + style = templateCell?.style, + attribute = image, + cellTemplate = templateCell + ) + } + Attribute.Type.HEADING -> { + UserOperationHeadingVisualCell( + header = attr.label.value, + style = templateCell?.style, + attribute = attr, + cellTemplate = templateCell + ) + } + else -> { + WMTLogger.w("Using unsupported Attribute in Templates") + null + } + } +} diff --git a/library/src/main/java/com/wultra/android/mtokensdk/api/operation/templateparser/TemplateListVisual.kt b/library/src/main/java/com/wultra/android/mtokensdk/api/operation/templateparser/TemplateListVisual.kt new file mode 100644 index 0000000..aba5723 --- /dev/null +++ b/library/src/main/java/com/wultra/android/mtokensdk/api/operation/templateparser/TemplateListVisual.kt @@ -0,0 +1,132 @@ +/* + * 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. + */ + +package com.wultra.android.mtokensdk.api.operation.templateparser + +import com.wultra.android.mtokensdk.api.operation.model.AmountAttribute +import com.wultra.android.mtokensdk.api.operation.model.Attribute +import com.wultra.android.mtokensdk.api.operation.model.ConversionAttribute +import com.wultra.android.mtokensdk.api.operation.model.HeadingAttribute +import com.wultra.android.mtokensdk.api.operation.model.ImageAttribute +import com.wultra.android.mtokensdk.api.operation.model.KeyValueAttribute +import com.wultra.android.mtokensdk.api.operation.model.NoteAttribute +import com.wultra.android.mtokensdk.api.operation.model.Templates +import com.wultra.android.mtokensdk.api.operation.model.UserOperation +import com.wultra.android.mtokensdk.log.WMTLogger +import java.net.URL + +/** + * `TemplateListVisual` holds the visual data for displaying a user operation in a list view (RecyclerView/ListView). + */ +data class TemplateListVisual( + val header: String? = null, + val title: String? = null, + val message: String? = null, + val style: String? = null, + val thumbnailImageURL: URL? = null, + val template: Templates.ListTemplate? = null +) + +/** + * Extension function to prepare the visual representation for the given `UserOperation` in a list view. + */ +fun UserOperation.prepareVisualListDetail(): TemplateListVisual { + val listTemplate = this.ui?.templates?.list + val attributes = this.formData.attributes + val headerAttr = listTemplate?.header?.replacePlaceholders(attributes) + + val title: String? = listTemplate?.title?.replacePlaceholders(attributes) + ?: if (this.formData.message.isNotEmpty()) this.formData.title else null + + val message: String? = listTemplate?.message?.replacePlaceholders(attributes) + ?: if (this.formData.message.isNotEmpty()) this.formData.message else null + + val imageUrl: URL? = listTemplate?.image?.let { imgAttr -> + this.formData.attributes + .filterIsInstance() + .firstOrNull { it.label.id == imgAttr } + ?.let { URL(it.thumbnailUrl) } + } + + return TemplateListVisual( + header = headerAttr, + title = title, + message = message, + style = this.ui?.templates?.list?.style, + thumbnailImageURL = imageUrl, + template = listTemplate + ) +} + +/** + * Extension function to replace placeholders in the template with actual values. + */ +fun String.replacePlaceholders(attributes: List): String? { + var result = this + + val placeholders = extractPlaceholders() + placeholders?.forEach { placeholder -> + val value = findAttributeValue(placeholder, attributes) + if (value != null) { + result = result.replace("\${$placeholder}", value) + } else { + WMTLogger.d("Placeholder Attribute: $placeholder not found.") + return null + } + } + return result +} + +/** + * Extracts placeholders from the string. + */ +private fun String.extractPlaceholders(): List? { + return try { + val regex = Regex("""\$\{(.*?)\}""") + regex.findAll(this).map { it.groupValues[1] }.toList() + } catch (e: Exception) { + WMTLogger.w("Error creating regex: $e in TemplatesListParser.") + null + } +} + +/** + * Finds the attribute value for a given attribute ID from the attributes list. + */ +private fun findAttributeValue(attributeId: String, attributes: List): String? { + return attributes.firstOrNull { it.label.id == attributeId }?.let { attribute -> + when (attribute.type) { + Attribute.Type.AMOUNT -> { + val attr = attribute as? AmountAttribute + attr?.valueFormatted ?: "${attr?.amountFormatted} ${attr?.currencyFormatted}" + } + Attribute.Type.AMOUNT_CONVERSION -> { + val attr = attribute as? ConversionAttribute + if (attr != null) { + val sourceValue = attr.source.valueFormatted ?: "${attr.source.amountFormatted} ${attr.source.currencyFormatted}" + val targetValue = attr.target.valueFormatted ?: "${attr.target.amountFormatted} ${attr.target.currencyFormatted}" + "$sourceValue → $targetValue" + } else { + null + } + } + Attribute.Type.KEY_VALUE -> (attribute as? KeyValueAttribute)?.value + Attribute.Type.NOTE -> (attribute as? NoteAttribute)?.note + Attribute.Type.HEADING -> (attribute as? HeadingAttribute)?.label?.value + else -> null + } + } +} diff --git a/library/src/main/java/com/wultra/android/mtokensdk/api/operation/templateparser/TemplateVisualParser.kt b/library/src/main/java/com/wultra/android/mtokensdk/api/operation/templateparser/TemplateVisualParser.kt new file mode 100644 index 0000000..f3f556b --- /dev/null +++ b/library/src/main/java/com/wultra/android/mtokensdk/api/operation/templateparser/TemplateVisualParser.kt @@ -0,0 +1,51 @@ +/* + * 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. + */ + +package com.wultra.android.mtokensdk.api.operation.templateparser + +import com.wultra.android.mtokensdk.api.operation.model.UserOperation + +/** + * This is a utility class responsible for preparing visual representations of `UserOperation`. + * + * It generates visual data for both list and detailed views of the operations from `OperationFormData` and its `OperationAttribute`. + * The visual data are created based on the structure of the `Templates`. + */ +class TemplateVisualParser { + + companion object { + + /** + * Prepares the visual representation for the given `UserOperation` in a list view. + * @param operation The user operation to prepare the visual data for. + * @return A `TemplateListVisual` instance containing the visual data. + */ + @JvmStatic + fun prepareForList(operation: UserOperation): TemplateListVisual { + return operation.prepareVisualListDetail() + } + + /** + * Prepares the visual representation for a detail view of the given `UserOperation`. + * @param operation The user operation to prepare the visual data for. + * @return A `TemplateDetailVisual` instance containing the visual data. + */ + @JvmStatic + fun prepareForDetail(operation: UserOperation): TemplateDetailVisual { + return operation.prepareVisualDetail() + } + } +} From ffe6f17401d51e65c614fb95053511914458a00e Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Wed, 7 Aug 2024 16:36:34 +0200 Subject: [PATCH 4/6] Update docs --- docs/Using-Operations-Service.md | 202 +++++++++++++++++- .../templateparser/TemplateDetailVisual.kt | 9 +- .../templateparser/TemplateListVisual.kt | 12 +- 3 files changed, 214 insertions(+), 9 deletions(-) diff --git a/docs/Using-Operations-Service.md b/docs/Using-Operations-Service.md index 3aee587..3137c71 100644 --- a/docs/Using-Operations-Service.md +++ b/docs/Using-Operations-Service.md @@ -14,6 +14,7 @@ - [UserOperation](#useroperation) - [Creating a Custom Operation](#creating-a-custom-operation) - [ProximityCheck](#proximitycheck) +- [Templates](#templates) ## Introduction @@ -553,6 +554,13 @@ class OperationUIData { * Type of PostApprovalScreen is presented with different classes (Starting with `PostApprovalScreen*`) */ val postApprovalScreen: PostApprovalScreen? + + /** + * Detailed information about displaying the operation data + * + * Contains prearranged structure of the operation attributes for the app to display + */ + val templates: Templates? } ``` @@ -664,4 +672,196 @@ data class PACData( - mentioned JWT should be in the format `{“typ”:”JWT”, “alg”:”none”}.{“oid”:”5b753d0d-d59a-49b7-bec4-eae258566dbb”, “potp”:”12345678”} ` - Accepted formats: - - notice that the totp key in JWT and in query shall be `potp`! \ No newline at end of file + - notice that the totp key in JWT and in query shall be `potp`! + +## Templates + +`Templates` are part of `OperationUIData`. +`Templates` class provides detailed information about displaying operation data within the application. + + +`typealias AttributeName = String` is used across the `Templates`. It explicitly says that the Strings that will be assigned to properties is actually `OperationAttributes.AttributeLabel.id` and its **value** shall displayed. + +Definition of the `Templates `: + +```kotlin +data class Templates( + /** How the operation should look like in the list of operations */ + val list: ListTemplate?, + + /** How the operation detail should look like when viewed individually. */ + val detail: DetailTemplate? +) +``` + +`ListTemplate` and `DetailTemplate` go as follows: + +```kotlin +data class ListTemplate( + /** Prearranged name which can be processed by the app */ + val style: String?, + + /** Attribute which will be used for the header */ + val header: AttributeFormatted?, + + /** Attribute which will be used for the title */ + val title: AttributeFormatted?, + + /** Attribute which will be used for the message */ + val message: AttributeFormatted?, + + /** Attribute which will be used for the image */ + val image: AttributeId? +) + +data class DetailTemplate( + /** Predefined style name that can be processed by the app to customize the overall look of the operation. */ + val style: String?, + + /** Indicates if the header should be created from form data (title, message) or customized for a specific operation */ + val showTitleAndMessage: Boolean?, + + /** Sections of the operation data. */ + val sections: List
? + ) { + + data class Section( + /** Prearranged name which can be processed by the app to customize the section */ + val style: String?, + + /** Attribute for section title */ + val title: AttributeId?, + + /** Each section can have multiple cells of data */ + val cells: List? + ) { + + data class Cell( + /** Which attribute shall be used */ + val name: AttributeId, + + /** Prearranged name which can be processed by the app to customize the cell */ + val style: String?, + + /** Should be the title visible or hidden */ + val visibleTitle: Boolean?, + + /** Should be the content copyable */ + val canCopy: Boolean?, + + /** Define if the cell should be collapsable */ + val collapsable: Collapsable?, + + /** If value should be centered */ + val centered: Boolean? + ) { + + enum class Collapsable { + + /** The cell should not be collapsable */ + NO, + + /** The cell should be collapsable and in collapsed state */ + COLLAPSED, + + /** The cell should be collapsable and in expanded state */ + YES + } + } + } +} + +``` + +###Template Visual Parser + +For convenience we provide a utility class responsible for preparing visual representations of `UserOperation` from received `Templates`. The parser translates `AttributeNames` from templates and returnes usable Strings values instead. Parser also walways returns the source template from which the data was created. + +```kotlin +class TemplateVisualParser { + + companion object { + + /** Prepares the visual representation for the given `UserOperation` in a list view. */ + fun prepareForList(operation: UserOperation): TemplateListVisual { + return operation.prepareVisualListDetail() + } + + /** Prepares the visual representation for a detail view of the given `UserOperation`. */ + fun prepareForDetail(operation: UserOperation): TemplateDetailVisual { + return operation.prepareVisualDetail() + } + } +} + +``` + + +#### TemplateListVisual + +`TemplateListVisual` holds the visual data for displaying a `UserOperation` in a list view (RecyclerView/ListView/LazyColumn). + +```kotlin +data class TemplateListVisual( + /** The header of the cell */ + val header: String? = null, + /** The title of the cell */ + val title: String? = null, + /** The message (subtitle) of the cell */ + val message: String? = null, + /** Predefined style of the cell on which the implementation can react */ + val style: String? = null, + /** URL of the cell thumbnail */ + val thumbnailImageURL: String? = null, + /** Complete template from which the TemplateListVisual was created */ + val template: Templates.ListTemplate? = null +) +``` + +#### TemplateDetailVisual + +`TemplateDetailVisual` holds the visual data for displaying a detailed view of a `UserOperation`. It contains style to which the app can react and adjust the operation style. It also contains list of `UserOperationVisualSection `. + +```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. + +```kotlin +data class UserOperationVisualSection( + + /** Predefined style of the section to which the app can react and adjust the operation visual */ + val style: String? = null, + + /** The title value for the section */ + val title: String? = null, + + /** An array of cells with `FormData` header and message or visual cells based on `OperationAttributes` */ + val cells: List +) +``` + +`UserOperationVisualCell` is the basic building block of the UserOperation. We differentiate between 5 different cell types: +
    +
  1. `UserOperationHeaderVisualCell` - is a header in a user operation's detail header view.
  2. + - it is created from UserOperation FormData title +
  3. `UserOperationMessageVisualCell` - is a message cell in a user operation's header view.
  4. + - it is created from UserOperation FormData message +
  5. `UserOperationHeadingVisualCell` - is a heading ("section separator") cell in a user operation's detailed view.
  6. + - it is created from `HEADING` FormData attribute +
  7. `UserOperationImageVisualCell` - is an image cell in a user operation's detailed view.
  8. + - it is created from `IMAGE` FormData attribute +
  9. `UserOperationValueAttributeVisualCell` - 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 at this moment \ No newline at end of file diff --git a/library/src/main/java/com/wultra/android/mtokensdk/api/operation/templateparser/TemplateDetailVisual.kt b/library/src/main/java/com/wultra/android/mtokensdk/api/operation/templateparser/TemplateDetailVisual.kt index 7764b1c..9616eea 100644 --- a/library/src/main/java/com/wultra/android/mtokensdk/api/operation/templateparser/TemplateDetailVisual.kt +++ b/library/src/main/java/com/wultra/android/mtokensdk/api/operation/templateparser/TemplateDetailVisual.kt @@ -25,7 +25,6 @@ import com.wultra.android.mtokensdk.api.operation.model.NoteAttribute import com.wultra.android.mtokensdk.api.operation.model.Templates import com.wultra.android.mtokensdk.api.operation.model.UserOperation import com.wultra.android.mtokensdk.log.WMTLogger -import java.net.URL /** * `TemplateDetailVisual` holds the visual data for displaying a detailed view of a user operation. @@ -126,10 +125,10 @@ data class UserOperationValueAttributeVisualCell( data class UserOperationImageVisualCell( /** The URL of the thumbnail image */ - val urlThumbnail: URL, + val urlThumbnail: String, /** The URL of the full size image */ - val urlFull: URL? = null, + val urlFull: String? = null, /** Predefined style of the section cell, app shall react to it and should change the visual of the cell */ val style: String? = null, @@ -276,8 +275,8 @@ private fun createCell( Attribute.Type.IMAGE -> { val image = attr as? ImageAttribute ?: return null UserOperationImageVisualCell( - urlThumbnail = URL(image.thumbnailUrl), - urlFull = image.originalUrl?.let { URL(it) }, + urlThumbnail = image.thumbnailUrl, + urlFull = image.originalUrl, style = templateCell?.style, attribute = image, cellTemplate = templateCell diff --git a/library/src/main/java/com/wultra/android/mtokensdk/api/operation/templateparser/TemplateListVisual.kt b/library/src/main/java/com/wultra/android/mtokensdk/api/operation/templateparser/TemplateListVisual.kt index aba5723..2f783d6 100644 --- a/library/src/main/java/com/wultra/android/mtokensdk/api/operation/templateparser/TemplateListVisual.kt +++ b/library/src/main/java/com/wultra/android/mtokensdk/api/operation/templateparser/TemplateListVisual.kt @@ -32,11 +32,17 @@ import java.net.URL * `TemplateListVisual` holds the visual data for displaying a user operation in a list view (RecyclerView/ListView). */ data class TemplateListVisual( + /** The header of the cell */ val header: String? = null, + /** The title of the cell */ val title: String? = null, + /** The message (subtitle) of the cell */ val message: String? = null, + /** Predefined style of the cell on which the implementation can react */ val style: String? = null, - val thumbnailImageURL: URL? = null, + /** URL of the cell thumbnail */ + val thumbnailImageURL: String? = null, + /** Complete template from which the TemplateListVisual was created */ val template: Templates.ListTemplate? = null ) @@ -54,11 +60,11 @@ fun UserOperation.prepareVisualListDetail(): TemplateListVisual { val message: String? = listTemplate?.message?.replacePlaceholders(attributes) ?: if (this.formData.message.isNotEmpty()) this.formData.message else null - val imageUrl: URL? = listTemplate?.image?.let { imgAttr -> + val imageUrl: String? = listTemplate?.image?.let { imgAttr -> this.formData.attributes .filterIsInstance() .firstOrNull { it.label.id == imgAttr } - ?.let { URL(it.thumbnailUrl) } + ?.thumbnailUrl } return TemplateListVisual( From fc9c90554985a5399d3bc15c27ab43f1116f0a02 Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Wed, 7 Aug 2024 16:39:12 +0200 Subject: [PATCH 5/6] Fix ktlint --- .../mtokensdk/api/operation/templateparser/TemplateListVisual.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/library/src/main/java/com/wultra/android/mtokensdk/api/operation/templateparser/TemplateListVisual.kt b/library/src/main/java/com/wultra/android/mtokensdk/api/operation/templateparser/TemplateListVisual.kt index 2f783d6..c422cb0 100644 --- a/library/src/main/java/com/wultra/android/mtokensdk/api/operation/templateparser/TemplateListVisual.kt +++ b/library/src/main/java/com/wultra/android/mtokensdk/api/operation/templateparser/TemplateListVisual.kt @@ -26,7 +26,6 @@ import com.wultra.android.mtokensdk.api.operation.model.NoteAttribute import com.wultra.android.mtokensdk.api.operation.model.Templates import com.wultra.android.mtokensdk.api.operation.model.UserOperation import com.wultra.android.mtokensdk.log.WMTLogger -import java.net.URL /** * `TemplateListVisual` holds the visual data for displaying a user operation in a list view (RecyclerView/ListView). From 3c3817af25d1ec547b31ad14e7b2515c497aab2f Mon Sep 17 00:00:00 2001 From: Marek Stransky Date: Mon, 19 Aug 2024 11:36:13 +0200 Subject: [PATCH 6/6] Fix docs --- docs/Using-Operations-Service.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Using-Operations-Service.md b/docs/Using-Operations-Service.md index 3137c71..04444d6 100644 --- a/docs/Using-Operations-Service.md +++ b/docs/Using-Operations-Service.md @@ -773,7 +773,7 @@ data class DetailTemplate( ``` -###Template Visual Parser +### Template Visual Parser For convenience we provide a utility class responsible for preparing visual representations of `UserOperation` from received `Templates`. The parser translates `AttributeNames` from templates and returnes usable Strings values instead. Parser also walways returns the source template from which the data was created. @@ -864,4 +864,4 @@ data class UserOperationVisualSection( > [!WARNING] -> At this moment `PARTY_INFO` & `UNKNOWN` attributes are not supported at this moment \ No newline at end of file +> At this moment `PARTY_INFO` & `UNKNOWN` attributes are not supported \ No newline at end of file