Skip to content

Commit a1362d2

Browse files
authored
Added Mobile token data Builder (#219)
* Add MobileTokenData Builder * Add test for MobileTokenDataBuilder * Add docs * Improve PreApprovalScreenRecorder * Fix Ktlint * Remarks * Minor docs adjustment * Refactor `MobileTokenDataRecord` from interface to abstract class * Simplify `PreApprovalScreensRecorder.toValue()` + improve docs * Improvements * Simplification * Improve docs * Remarks * PR remarks * Fix integration test
1 parent cd84147 commit a1362d2

File tree

7 files changed

+604
-60
lines changed

7 files changed

+604
-60
lines changed

docs/Migration-2.4.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ Version `2.4.x` introduces a new, extensible **Pre‑approval UI Template** and
3434
7. **New RejectionReason**
3535
- New rejection reason `PREAPPROVAL` indicates the user cancelled the operation during the Pre-Approval flow.
3636

37+
8. **New MobileTokenData Builder**
38+
- Introduced MobileTokenData.Builder, a helper for composing structured data attached to operations during authorization or rejection.
39+
- Supports both generic key–value entries (builder.put(key, value)) and structured records (builder.put(record)), e.g. PreApprovalScreensRecorder.
40+
- Structured records implement MobileTokenDataRecord (stable key, build(): Any that returns the value to store).
41+
- builder.build() returns a Map<String, Any> snapshot you assign to operation.mobileTokenData.
42+
3743
---
3844

3945
### Removed / Changed Functionality

docs/Using-Operations-Service.md

Lines changed: 198 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- [Start Periodic Polling](#start-periodic-polling)
88
- [Approve an Operation](#approve-an-operation)
99
- [Reject an Operation](#reject-an-operation)
10+
- [Mobile Token Data](#mobile-token-data)
1011
- [Operation detail](#operation-detail)
1112
- [Claim the Operation](#claim-the-operation)
1213
- [Off-line Authorization](#off-line-authorization)
@@ -192,74 +193,209 @@ fun approveWithBiometrics(operation: IOperation) {
192193
}
193194
```
194195

195-
### Passing Additional Mobile Token Data
196-
197-
With PowerAuth server 1.10+, you can pass additional customer-specific data during operation authorization using the `mobileTokenData` property. This can be useful for fraud detection systems (FDS) or other custom business logic.
196+
## Reject an Operation
198197

198+
To reject an operation use `IOperationsService.rejectOperation`. Operation rejection is confirmed by the possession factor so there is no need for creating `PowerAuthAuthentication` object. You can simply use it with the following example.
199199

200200
```kotlin
201-
import com.wultra.android.mtokensdk.api.operation.model.IOperation
202-
import com.wultra.android.mtokensdk.api.operation.model.ProximityCheck
203-
import io.getlime.security.powerauth.sdk.PowerAuthAuthentication
204-
205-
// Create a custom operation with mobile token data
206-
class CustomOperation(
207-
override val id: String,
208-
override val data: String,
209-
override var proximityCheck: ProximityCheck? = null,
210-
override var mobileTokenData: Map<String, Any>? = null
211-
) : IOperation
212-
213-
// Approve operation with additional FDS data
214-
fun approveWithFDSData() {
215-
val fdsData: Map<String, Any> = mapOf(
216-
"deviceFingerprint" to "abc123def456",
217-
"riskScore" to 0.8,
218-
"location" to mapOf(
219-
"latitude" to 50.0755,
220-
"longitude" to 14.4378
221-
)
222-
)
223-
224-
val operation = CustomOperation(
225-
id = "operationId123",
226-
data = "operationData",
227-
mobileTokenData = fdsData
228-
)
229-
230-
val auth = PowerAuthAuthentication.possessionWithPassword("password123")
231-
232-
operationsService.authorizeOperation(operation, auth) { result ->
233-
result.onSuccess {
201+
// Reject operation with some reason
202+
fun reject(operation: IOperation, reason: RejectionData) {
203+
this.operationsService.rejectOperation(operation, reason) {
204+
it.onSuccess {
234205
// show success UI
235-
}.onFailure { error ->
206+
}.onFailure {
236207
// show error UI
237208
}
238209
}
239210
}
240211
```
241212

242-
Similarly to approving an operation, you can also pass mobileTokenData when rejecting an operation.
213+
## Mobile Token Data
243214

244-
The `mobileTokenData` is completely optional and the structure is customer-specific. If you don't need this functionality, you can continue using operations without providing this property.
215+
With PowerAuth Server **1.10+**, you can pass additional, customer-specific metadata during operation authorization using the `mobileTokenData` property.
216+
Since PowerAuth Server **2.0+** you can pass additional mobileTokenData to reject method as well.
245217

246-
## Reject an Operation
218+
This feature is especially useful for **fraud detection systems (FDS)**, customer risk evaluation, or other backend-specific business logic.
219+
220+
You can provide this data in two ways:
247221

248-
To reject an operation use `IOperationsService.rejectOperation`. Operation rejection is confirmed by the possession factor so there is no need for creating `PowerAuthAuthentication ` object. You can simply use it with the following example.
222+
---
223+
224+
### Direct Map Approach
225+
226+
If you already have a static set of key–value pairs to attach, you can directly construct a `Map<String, Any>` and assign it to your operation:
249227

250228
```kotlin
251-
// Reject operation with some reason
252-
fun reject(operation: IOperation, reason: RejectionData) {
253-
this.operationsService.rejectOperation(operation, reason) {
254-
it.onSuccess {
255-
// show success UI
256-
}.onFailure {
257-
// show error UI
258-
}
229+
// Example: directly attaching a static map of metadata
230+
val fdsData = mapOf(
231+
"deviceFingerprint" to "abc123def456",
232+
"riskScore" to 0.8,
233+
"location" to mapOf(
234+
"latitude" to 50.0755,
235+
"longitude" to 14.4378
236+
)
237+
)
238+
239+
val operation = CustomOperation(
240+
id = "operationId123",
241+
data = "operationData",
242+
mobileTokenData = fdsData
243+
)
244+
245+
val auth = PowerAuthAuthentication.possessionWithPassword("password123")
246+
247+
operationsService.authorizeOperation(operation, auth) { result ->
248+
result.onSuccess {
249+
// Operation approved successfully
250+
}.onFailure {
251+
// Handle network or SDK error
259252
}
260253
}
261254
```
262255

256+
---
257+
258+
### Builder-Based Approach
259+
260+
For **more dynamic, structured, or multi-step** data, use the helper `MobileTokenData.Builder`.
261+
262+
The builder provides a safe, thread-synchronized API for collecting and organizing data before finalizing it into a map for submission.
263+
264+
### MobileTokenData Builder
265+
266+
The `MobileTokenData.Builder` helps you safely compose structured data for an operation before it’s approved or rejected.
267+
268+
- **Initialize** it with optional `initialData` map.
269+
- **Add generic entries** using `put(key, value)`.
270+
- **Attach structured records** such as `PreApprovalScreensRecorder` using `put(record)`.
271+
- **Extend** it with your own record types by implementing the `MobileTokenDataRecord` interface.
272+
273+
#### Example
274+
275+
```kotlin
276+
// Optional initial data entries (e.g. FDS hints)
277+
val initialData = mapOf("deviceFingerprint" to "abc123")
278+
279+
// Create the builder
280+
val builder = MobileTokenData.Builder(initialData)
281+
282+
// Add generic entries
283+
builder.put("riskScore", 0.82)
284+
285+
// Assign to the operation
286+
operation.mobileTokenData = builder.build()
287+
```
288+
289+
---
290+
291+
## Record Helpers
292+
293+
Sometimes, additional data attached to `mobileTokenData` is not just a few key–value pairs.
294+
It can represent **structured sections of information** (for example, a timeline of user actions or device events).
295+
296+
To support these cases, the SDK defines the **`MobileTokenDataRecord` interface**.
297+
298+
#### The `MobileTokenDataRecord` Interface
299+
300+
A `MobileTokenDataRecord` represents a single top-level entry in the final `mobileTokenData` map.
301+
It defines **two key responsibilities**:
302+
303+
1. Provide a stable `key` — the top-level field name under which your record will appear.
304+
2. Implement `build()` — a method that returns the **value** (any serializable object) for that key.
305+
306+
```kotlin
307+
interface MobileTokenDataRecord {
308+
/** Top-level key under which this record is stored. */
309+
val key: String
310+
311+
/** Produces the value object to store for this key. */
312+
fun build(): Any
313+
}
314+
```
315+
316+
This lets you encapsulate structured or time-dependent data, keep your `mobileTokenData` composition organized, and reuse record instances when needed.
317+
318+
---
319+
320+
### Example: Custom Record
321+
322+
You can implement your own record for any structured data section — for example, to log environment variables or app configuration info.
323+
324+
```kotlin
325+
class CustomRecord : MobileTokenDataRecord {
326+
override val key = "customSection"
327+
private val data = mutableMapOf<String, Any>()
328+
329+
fun add(name: String, value: Any) = apply { data[name] = value }
330+
331+
override fun build(): Any = data // return a value snapshot
332+
}
333+
```
334+
335+
Usage example:
336+
337+
```kotlin
338+
val builder = MobileTokenData.Builder()
339+
val record = CustomRecord()
340+
.add("flag", true)
341+
.add("mode", "debug")
342+
343+
// Either pass the whole record…
344+
builder.put(record)
345+
// …or manually by key/value
346+
// builder.put(record.key, record.build())
347+
348+
operation.mobileTokenData = builder.build()
349+
```
350+
351+
---
352+
353+
#### Predefined Record Helper: `PreApprovalScreensRecorder`
354+
355+
The SDK includes a predefined implementation, `PreApprovalScreensRecorder`, which records how users navigate through **Pre-Approval screens**.
356+
357+
Each recorded “visit” contains:
358+
359+
- Screen identifier (`screen`)
360+
- Opening timestamp
361+
- Closing timestamp
362+
- User action (`CONTINUE`, `CLOSE`, `REJECT`, `SCAN`, etc.)
363+
364+
The `PreApprovalScreensRecorder` exposes few methods for recording the user flow:
365+
366+
- `begin(id: String)` – starts a new visit for the given screen ID.
367+
If another visit is already open, it is automatically added to the list (without a closing timestamp or action).
368+
- `end(id: String, action: Action)` – closes the current visit if the given id matches.
369+
If no visit is open, but the most recent recorded visit has the same id and is still unclosed, it is finalized instead.
370+
- `reset()` - resets recorded visits
371+
372+
Timestamps are aligned with server time via `PowerAuthSDK.timeSynchronizationService`.
373+
374+
```kotlin
375+
// Create MobileTokenData.Builder instance
376+
val builder = MobileTokenData.Builder()
377+
378+
// The PowerAuthSDK instance provides a timeSynchronizationService used
379+
// to create accurate, server-aligned timestamps for each recorded event.
380+
val screenRecorder = PreApprovalScreensRecorder(powerAuthSDK)
381+
382+
// Display UI for the PreApproval screen and record that it was shown
383+
screenRecorder.begin(screen.id)
384+
// Record when user leaves the PreApproval screen
385+
screenRecorder.end(screen.id, PreApprovalScreensRecorder.Action.CONTINUE)
386+
387+
// ... repeat for all the screens from the operations.ui.preApprovalScreens list
388+
389+
// When your PreApproval flow is finished pass the WMTPreApprovalScreensRecorder to the WMTMobileTokenData.Builder
390+
builder.put(screenRecorder)
391+
392+
// Assign created MobileTokenData to the Operation before approving/rejecting
393+
operation.mobileTokenData = builder.build()
394+
```
395+
396+
The `mobileTokenData` is completely optional and the structure is customer-specific. If you don't need this functionality, you can continue using operations without providing this property.
397+
398+
---
263399

264400
## Operation detail
265401

@@ -621,16 +757,19 @@ Types:
621757

622758
A pre-approval screen can contain the following building blocks:
623759

624-
• Heading and message – textual content displayed at the top of the screen.
625-
• Optional metadata – id (unique identifier), backButton (show navigation back button), and image (in-app asset identifier).
626-
• Elements – structured items that form the main content of the screen:
627-
- List item – text with optional icon with style (INFO, WARNING, DANGER).
628-
- Alert – highlighted box with style (INFO, WARNING, DANGER).
629-
- Button – action element with LINK, MAIL, or PHONE.
630-
• Controls – configuration of approve/decline actions:
631-
- Decline – BACK or REJECT, with optional text.
632-
- Approve – SLIDER or BUTTON, with optional text and optional countdown (counter).
633-
- Layout options – axis (HORIZONTAL or VERTICAL) and flip (swap order of controls).
760+
- Heading and message – textual content displayed at the top of the screen.
761+
- Optional metadata
762+
- id - unique identifier)
763+
- backButton - show navigation back button
764+
- image - in-app asset identifier
765+
- Elements – structured items that form the main content of the screen:
766+
- List item – text with optional icon with style (INFO, WARNING, DANGER).
767+
- Alert – highlighted box with style (INFO, WARNING, DANGER).
768+
- Button – action element with LINK, MAIL, or PHONE.
769+
- Controls – configuration of approve/decline actions:
770+
- Decline – BACK or REJECT, with optional text.
771+
- Approve – SLIDER or BUTTON, with optional text and optional countdown (counter).
772+
- Layout options – axis (HORIZONTAL or VERTICAL) and flip (swap order of controls).
634773

635774
#### PostApprovalScreen:
636775
`WMTPostApprovalScreen*` classes commonly contain `heading` and `message` and different payload data

library/consumer-proguard-rules.pro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@
1010
<fields>;
1111
}
1212
# handle R8 full mode optimizations
13-
-keep, allowobfuscation class com.wultra.android.mtokensdk.api.**
13+
-keep, allowobfuscation class com.wultra.android.mtokensdk.api.**

0 commit comments

Comments
 (0)