Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented Templates #157

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 201 additions & 1 deletion docs/Using-Operations-Service.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- [UserOperation](#useroperation)
- [Creating a Custom Operation](#creating-a-custom-operation)
- [ProximityCheck](#proximitycheck)
- [Templates](#templates)

## Introduction
<!-- end -->
Expand Down Expand Up @@ -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?
}
```

Expand Down Expand Up @@ -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`!
- 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<Section>?
) {

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<Cell>?
) {

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<UserOperationVisualSection>
)
```

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>
)
```

`UserOperationVisualCell` is the basic building block of the UserOperation. We differentiate between 5 different cell types:
<ol>
<li>`UserOperationHeaderVisualCell` - is a header in a user operation's detail header view.</li>
- it is created from UserOperation FormData title
<li>`UserOperationMessageVisualCell` - is a message cell in a user operation's header view.</li>
- it is created from UserOperation FormData message
<li>`UserOperationHeadingVisualCell` - is a heading ("section separator") cell in a user operation's detailed view.</li>
- it is created from `HEADING` FormData attribute
<li>`UserOperationImageVisualCell` - is an image cell in a user operation's detailed view.</li>
- it is created from `IMAGE` FormData attribute
<li>`UserOperationValueAttributeVisualCell` - is value attribute cell in a user operation's detailed view.</li>
- it is created from the remaining (`AMOUNT`, `AMOUNT_CONVERSION `, `KEY_VALUE`, `NOTE`) FormData attribute
</ol>

> [!WARNING]
> At this moment `PARTY_INFO` & `UNKNOWN` attributes are not supported
151 changes: 151 additions & 0 deletions library/src/androidTest/java/OperationUIDataTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -208,6 +209,64 @@ 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) {
fail("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)

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 */
private val jsonDecoder: Gson = OperationsUtils.defaultGsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").create()

Expand All @@ -220,6 +279,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",
Expand Down Expand Up @@ -451,4 +518,88 @@ class OperationUIDataTests {
}
}
"""

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": {
"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",
"centered": true
},
{
"style": "CONVERSION",
"name": "operation.conversion",
"canCopy": true,
"collapsable": "NO"
},
{
"name": "operation.conversion2",
"visibleTitle": true,
"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
}
}
]
}
}
}
"""
}
Loading
Loading