|
7 | 7 | - [Start Periodic Polling](#start-periodic-polling) |
8 | 8 | - [Approve an Operation](#approve-an-operation) |
9 | 9 | - [Reject an Operation](#reject-an-operation) |
| 10 | +- [Mobile Token Data](#mobile-token-data) |
10 | 11 | - [Operation detail](#operation-detail) |
11 | 12 | - [Claim the Operation](#claim-the-operation) |
12 | 13 | - [Off-line Authorization](#off-line-authorization) |
@@ -192,74 +193,209 @@ fun approveWithBiometrics(operation: IOperation) { |
192 | 193 | } |
193 | 194 | ``` |
194 | 195 |
|
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 |
198 | 197 |
|
| 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. |
199 | 199 |
|
200 | 200 | ```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 { |
234 | 205 | // show success UI |
235 | | - }.onFailure { error -> |
| 206 | + }.onFailure { |
236 | 207 | // show error UI |
237 | 208 | } |
238 | 209 | } |
239 | 210 | } |
240 | 211 | ``` |
241 | 212 |
|
242 | | -Similarly to approving an operation, you can also pass mobileTokenData when rejecting an operation. |
| 213 | +## Mobile Token Data |
243 | 214 |
|
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. |
245 | 217 |
|
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: |
247 | 221 |
|
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: |
249 | 227 |
|
250 | 228 | ```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 |
259 | 252 | } |
260 | 253 | } |
261 | 254 | ``` |
262 | 255 |
|
| 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 | +--- |
263 | 399 |
|
264 | 400 | ## Operation detail |
265 | 401 |
|
@@ -621,16 +757,19 @@ Types: |
621 | 757 |
|
622 | 758 | A pre-approval screen can contain the following building blocks: |
623 | 759 |
|
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). |
634 | 773 |
|
635 | 774 | #### PostApprovalScreen: |
636 | 775 | `WMTPostApprovalScreen*` classes commonly contain `heading` and `message` and different payload data |
|
0 commit comments