From a690adabb1eb1567bda6af531a6fe3bdcfbcd54d Mon Sep 17 00:00:00 2001 From: yaswanth-pula-skyflow Date: Fri, 31 Jul 2026 11:08:37 +0530 Subject: [PATCH 1/2] SK-3004:Add mockCVV token support in collect elements. --- .../main/kotlin/Skyflow/CollectContainer.kt | 8 +- .../client/FlowDBCollectAPICallback.kt | 7 +- .../collect/client/FlowDBMixedAPICallback.kt | 7 +- .../kotlin/Skyflow/collect/client/MockCVV.kt | 117 +++++++++++++++++ .../Skyflow/composable/ComposableContainer.kt | 8 +- .../kotlin/Skyflow/core/FlowDBAPIClient.kt | 5 +- .../src/test/java/com/Skyflow/MockCVVTest.kt | 118 ++++++++++++++++++ 7 files changed, 258 insertions(+), 12 deletions(-) create mode 100644 Skyflow/src/main/kotlin/Skyflow/collect/client/MockCVV.kt create mode 100644 Skyflow/src/test/java/com/Skyflow/MockCVVTest.kt diff --git a/Skyflow/src/main/kotlin/Skyflow/CollectContainer.kt b/Skyflow/src/main/kotlin/Skyflow/CollectContainer.kt index 6574734..8204cbf 100644 --- a/Skyflow/src/main/kotlin/Skyflow/CollectContainer.kt +++ b/Skyflow/src/main/kotlin/Skyflow/CollectContainer.kt @@ -1,5 +1,6 @@ package Skyflow +import Skyflow.collect.client.CVVMap import Skyflow.collect.client.FlowDBCollectRequestBody import Skyflow.collect.client.FlowDBMixedAPICallback import org.json.JSONObject @@ -150,7 +151,7 @@ internal fun Container.post(callback: Callback, options: Colle val mixedCallback = FlowDBMixedAPICallback( client.apiClient, combinedUpdateBody, insertBody, callback, collectOptions, - configuration.options.logLevel + configuration.options.logLevel, CVVMap.capture(collectElements) ) client.apiClient.getAccessToken(mixedCallback) return @@ -166,7 +167,7 @@ internal fun Container.post(callback: Callback, options: Colle insertOptions, configuration.options.logLevel ) - this.client.apiClient.post(requestBody, callback, collectOptions) + this.client.apiClient.post(requestBody, callback, collectOptions, cvvMap = CVVMap.capture(this.collectElements)) } fun Container.collect(callback: CollectCallback, options: CollectOptions? = CollectOptions()) { @@ -191,7 +192,8 @@ fun Container.update(tableName: String, skyflowID: String, cal skyflowID, configuration.options.logLevel ) - this.client.apiClient.post(requestBody, callback, options, "update") + this.client.apiClient.post(requestBody, callback, options, "update", + CVVMap.captureForUpdate(this.collectElements, skyflowID)) } catch (e: Exception) { callback.onFailure(Utils.constructErrorResponse(e)) } diff --git a/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBCollectAPICallback.kt b/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBCollectAPICallback.kt index 585303f..f400098 100644 --- a/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBCollectAPICallback.kt +++ b/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBCollectAPICallback.kt @@ -21,7 +21,8 @@ internal class FlowDBCollectAPICallback( val callback: Skyflow.Callback, private val options: CollectOptions, val logLevel: LogLevel, - private val endpoint: String = "insert" + private val endpoint: String = "insert", + private val cvvMap: CVVMap = CVVMap.EMPTY ) : Skyflow.Callback { private val okHttpClient = apiClient.okHttpClient private val tag = FlowDBCollectAPICallback::class.qualifiedName @@ -128,6 +129,10 @@ internal class FlowDBCollectAPICallback( } } + // Swap real CVV tokens for mock placeholders before returning to the app. The entered + // value still went to the vault unchanged; only the token in the response is replaced. + replaceCVVTokensInRecord(fieldsObject, tableName, skyflowId, cvvMap) + val resultRecord = JSONObject() .put("tableName", tableName) .put("skyflowId", skyflowId) diff --git a/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBMixedAPICallback.kt b/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBMixedAPICallback.kt index ac39e04..7ab5db4 100644 --- a/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBMixedAPICallback.kt +++ b/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBMixedAPICallback.kt @@ -13,7 +13,8 @@ internal class FlowDBMixedAPICallback( private val insertBody: JSONObject?, private val finalCallback: Callback, private val options: CollectOptions, - val logLevel: LogLevel + val logLevel: LogLevel, + private val cvvMap: CVVMap = CVVMap.EMPTY ) : Callback { private val totalCalls = (if (updateBody != null) 1 else 0) + (if (insertBody != null) 1 else 0) @@ -23,11 +24,11 @@ internal class FlowDBMixedAPICallback( override fun onSuccess(responseBody: Any) { val token = responseBody.toString() updateBody?.let { body -> - FlowDBCollectAPICallback(apiClient, body, makeSubCallback(), options, logLevel, "update") + FlowDBCollectAPICallback(apiClient, body, makeSubCallback(), options, logLevel, "update", cvvMap) .onSuccess(token) } insertBody?.let { body -> - FlowDBCollectAPICallback(apiClient, body, makeSubCallback(), options, logLevel, "insert") + FlowDBCollectAPICallback(apiClient, body, makeSubCallback(), options, logLevel, "insert", cvvMap) .onSuccess(token) } } diff --git a/Skyflow/src/main/kotlin/Skyflow/collect/client/MockCVV.kt b/Skyflow/src/main/kotlin/Skyflow/collect/client/MockCVV.kt new file mode 100644 index 0000000..a8249ac --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/collect/client/MockCVV.kt @@ -0,0 +1,117 @@ +package Skyflow.collect.client + +import Skyflow.SkyflowElementType +import Skyflow.TextField +import org.json.JSONObject +import java.security.SecureRandom + +/** + * Captures the actual value entered into each CVV collect element so its token can be swapped + * for a mock placeholder in the response, without the real value ever reaching the app. + * + * Because Android collect elements are in-process objects that directly know their own element + * type and entered value, no identifier plumbing is needed: we read [TextField.fieldType] and + * [TextField.getValue] at request-assembly time and key the entered value by table name (inserts) + * or record id / skyflowID (updates), mirroring how the vault echoes records back. + */ +internal class CVVMap( + val byTable: Map>, + val byRecordId: Map> +) { + fun isEmpty(): Boolean = byTable.isEmpty() && byRecordId.isEmpty() + + companion object { + val EMPTY = CVVMap(emptyMap(), emptyMap()) + + /** + * Builds the map from a set of collect elements. Update elements carry their own skyflowID + * (they are the ones filtered by a non-empty skyflowId) and are keyed by record id; insert + * elements are keyed by table name. + */ + internal fun capture(elements: List): CVVMap { + val byTable = LinkedHashMap>() + val byRecordId = LinkedHashMap>() + for (element in elements) { + if (element.fieldType != SkyflowElementType.CVV) continue + val value = element.getValue() + if (value.isEmpty()) continue + val skyflowId = element.skyflowId + if (!skyflowId.isNullOrEmpty()) { + byRecordId.getOrPut(skyflowId) { LinkedHashMap() }[element.columnName] = value + } else { + byTable.getOrPut(element.tableName) { LinkedHashMap() }[element.columnName] = value + } + } + return CVVMap(byTable, byRecordId) + } + + /** + * Builds the map for the standalone update flow, where the skyflowID is supplied by the + * caller rather than carried on the elements. All CVV elements are keyed by that record id. + */ + internal fun captureForUpdate(elements: List, skyflowId: String): CVVMap { + val columns = LinkedHashMap() + for (element in elements) { + if (element.fieldType != SkyflowElementType.CVV) continue + val value = element.getValue() + if (value.isEmpty()) continue + columns[element.columnName] = value + } + return if (columns.isEmpty()) EMPTY else CVVMap(emptyMap(), mapOf(skyflowId to columns)) + } + } +} + +private val secureRandom = SecureRandom() + +/** + * Generates a numeric mock CVV placeholder of [length] digits that is guaranteed to differ from + * [actualValue]. Leading zeros are allowed because this is a display string, not a number. Each + * attempt is a single secure-random draw (digit = nextInt(10) per position); it regenerates on the + * rare collision. It runs a handful of times per submit, so speed is not a concern. + */ +internal fun generateMockCVV(length: Int, actualValue: String): String { + if (length <= 0) return "" + while (true) { + val builder = StringBuilder(length) + for (i in 0 until length) { + builder.append(secureRandom.nextInt(10)) + } + val candidate = builder.toString() + if (candidate != actualValue) return candidate + } +} + +/** + * Replaces the token value of every captured CVV column in [tokens] with a freshly generated mock + * placeholder that matches the entered length and never equals that element's own entered value. + * + * The vault returns each column as a list of `{ token, tokenGroupName }` entries, so one mock is + * generated per column and applied to every entry for that column. Updates are matched by record id + * first, then inserts by table name. Non-CVV columns and hashed data are left untouched. + * + * Cross-element collision is intentionally ignored: a mock may coincidentally equal a *different* + * element's entered value, but entered values never leave the device to the app, so there is no + * observable leak. Only the per-element guarantee (mock != that element's own entered value) matters. + */ +internal fun replaceCVVTokensInRecord( + tokens: JSONObject, + tableName: String, + skyflowId: String, + cvvMap: CVVMap +) { + if (cvvMap.isEmpty()) return + val columns = cvvMap.byRecordId[skyflowId] + ?: (if (tableName.isNotEmpty()) cvvMap.byTable[tableName] else null) + ?: return + for ((column, enteredValue) in columns) { + val entries = tokens.optJSONArray(column) ?: continue + val mock = generateMockCVV(enteredValue.length, enteredValue) + for (i in 0 until entries.length()) { + val entry = entries.optJSONObject(i) ?: continue + if (entry.has("token")) { + entry.put("token", mock) + } + } + } +} diff --git a/Skyflow/src/main/kotlin/Skyflow/composable/ComposableContainer.kt b/Skyflow/src/main/kotlin/Skyflow/composable/ComposableContainer.kt index 815986b..5c2089b 100644 --- a/Skyflow/src/main/kotlin/Skyflow/composable/ComposableContainer.kt +++ b/Skyflow/src/main/kotlin/Skyflow/composable/ComposableContainer.kt @@ -1,6 +1,7 @@ package Skyflow.composable import Skyflow.* +import Skyflow.collect.client.CVVMap import Skyflow.collect.client.FlowDBCollectRequestBody import Skyflow.collect.client.FlowDBMixedAPICallback import org.json.JSONArray @@ -253,7 +254,7 @@ private fun Container.post(callback: Callback, options: Col val mixedCallback = FlowDBMixedAPICallback( client.apiClient, combinedUpdateBody, insertBody, callback, collectOptions, - configuration.options.logLevel + configuration.options.logLevel, CVVMap.capture(collectElements) ) client.apiClient.getAccessToken(mixedCallback) return @@ -269,7 +270,7 @@ private fun Container.post(callback: Callback, options: Col insertOptions, configuration.options.logLevel ) - this.client.apiClient.post(requestBody, callback, collectOptions) + this.client.apiClient.post(requestBody, callback, collectOptions, cvvMap = CVVMap.capture(this.collectElements)) } fun Container.update(tableName: String, skyflowID: String, callback: Callback, options: CollectOptions = CollectOptions()) { @@ -279,7 +280,8 @@ fun Container.update(tableName: String, skyflowID: String, configuration.vaultID, tableName, this.collectElements, skyflowID, configuration.options.logLevel ) - this.client.apiClient.post(requestBody, callback, options, "update") + this.client.apiClient.post(requestBody, callback, options, "update", + CVVMap.captureForUpdate(this.collectElements, skyflowID)) } catch (e: Exception) { callback.onFailure(Utils.constructErrorResponse(e)) } diff --git a/Skyflow/src/main/kotlin/Skyflow/core/FlowDBAPIClient.kt b/Skyflow/src/main/kotlin/Skyflow/core/FlowDBAPIClient.kt index 0ea1cd6..4e402f3 100644 --- a/Skyflow/src/main/kotlin/Skyflow/core/FlowDBAPIClient.kt +++ b/Skyflow/src/main/kotlin/Skyflow/core/FlowDBAPIClient.kt @@ -1,6 +1,7 @@ package Skyflow.core import Skyflow.* +import Skyflow.collect.client.CVVMap import Skyflow.collect.client.FlowDBCollectAPICallback import Skyflow.reveal.FlowDBRevealApiCallback import Skyflow.utils.Utils @@ -48,9 +49,9 @@ internal class FlowDBAPIClient( } } - fun post(requestBody: JSONObject, callback: Callback, options: CollectOptions, endpoint: String = "insert") { + fun post(requestBody: JSONObject, callback: Callback, options: CollectOptions, endpoint: String = "insert", cvvMap: CVVMap = CVVMap.EMPTY) { try { - val collectApiCallback = FlowDBCollectAPICallback(this, requestBody, callback, options, logLevel, endpoint) + val collectApiCallback = FlowDBCollectAPICallback(this, requestBody, callback, options, logLevel, endpoint, cvvMap) this.getAccessToken(collectApiCallback) } catch (e: Exception) { callback.onFailure(Utils.constructError(e)) diff --git a/Skyflow/src/test/java/com/Skyflow/MockCVVTest.kt b/Skyflow/src/test/java/com/Skyflow/MockCVVTest.kt new file mode 100644 index 0000000..5a50709 --- /dev/null +++ b/Skyflow/src/test/java/com/Skyflow/MockCVVTest.kt @@ -0,0 +1,118 @@ +package com.Skyflow + +import Skyflow.collect.client.CVVMap +import Skyflow.collect.client.generateMockCVV +import Skyflow.collect.client.replaceCVVTokensInRecord +import org.json.JSONArray +import org.json.JSONObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class MockCVVTest { + + // ---- generateMockCVV ---- + + @Test + fun `generateMockCVV produces numeric string of requested length`() { + for (length in intArrayOf(3, 4)) { + repeat(200) { + val mock = generateMockCVV(length, "999999") + assertEquals(length, mock.length) + assertTrue("expected all digits, got $mock", mock.all { it.isDigit() }) + } + } + } + + @Test + fun `generateMockCVV never equals the entered value`() { + // Repeated draws must always differ from the entered value for the same element. + repeat(2000) { + assertNotEquals("123", generateMockCVV(3, "123")) + } + repeat(2000) { + assertNotEquals("4321", generateMockCVV(4, "4321")) + } + } + + private fun tokenList(vararg tokens: String): JSONArray { + val array = JSONArray() + for (t in tokens) { + array.put(JSONObject().put("token", t).put("tokenGroupName", "grp")) + } + return array + } + + // ---- replaceCVVTokensInRecord ---- + + @Test + fun `insert flow swaps CVV column matched by table name and leaves other columns intact`() { + val tokens = JSONObject() + .put("cvv", tokenList("realCvvToken")) + .put("card_number", tokenList("realCardToken")) + + val cvvMap = CVVMap(byTable = mapOf("cards" to mapOf("cvv" to "321")), byRecordId = emptyMap()) + replaceCVVTokensInRecord(tokens, tableName = "cards", skyflowId = "newlyGeneratedId", cvvMap = cvvMap) + + val swapped = tokens.getJSONArray("cvv").getJSONObject(0) + assertEquals(3, swapped.getString("token").length) + assertTrue(swapped.getString("token").all { it.isDigit() }) + assertNotEquals("321", swapped.getString("token")) + assertNotEquals("realCvvToken", swapped.getString("token")) + // tokenGroupName preserved. + assertEquals("grp", swapped.getString("tokenGroupName")) + // Non-CVV column untouched. + assertEquals("realCardToken", tokens.getJSONArray("card_number").getJSONObject(0).getString("token")) + } + + @Test + fun `update flow swaps CVV column matched by record id`() { + val tokens = JSONObject().put("cvv", tokenList("realCvvToken")) + val cvvMap = CVVMap(byTable = emptyMap(), byRecordId = mapOf("rec-1" to mapOf("cvv" to "4321"))) + + replaceCVVTokensInRecord(tokens, tableName = "cards", skyflowId = "rec-1", cvvMap = cvvMap) + + val swapped = tokens.getJSONArray("cvv").getJSONObject(0).getString("token") + assertEquals(4, swapped.length) + assertNotEquals("4321", swapped) + } + + @Test + fun `record id match takes precedence over table match`() { + val tokens = JSONObject().put("cvv", tokenList("realCvvToken")) + val cvvMap = CVVMap( + byTable = mapOf("cards" to mapOf("cvv" to "111")), // 3-digit + byRecordId = mapOf("rec-1" to mapOf("cvv" to "2222")) // 4-digit + ) + + replaceCVVTokensInRecord(tokens, tableName = "cards", skyflowId = "rec-1", cvvMap = cvvMap) + + // Length must follow the record-id entry (4), proving record id won. + assertEquals(4, tokens.getJSONArray("cvv").getJSONObject(0).getString("token").length) + } + + @Test + fun `same mock is applied across all token entries of a CVV column`() { + val tokens = JSONObject().put("cvv", tokenList("tokA", "tokB", "tokC")) + val cvvMap = CVVMap(byTable = mapOf("cards" to mapOf("cvv" to "321")), byRecordId = emptyMap()) + + replaceCVVTokensInRecord(tokens, tableName = "cards", skyflowId = "id", cvvMap = cvvMap) + + val entries = tokens.getJSONArray("cvv") + val first = entries.getJSONObject(0).getString("token") + for (i in 0 until entries.length()) { + assertEquals(first, entries.getJSONObject(i).getString("token")) + } + } + + @Test + fun `no CVV columns captured leaves tokens unchanged`() { + val tokens = JSONObject().put("cvv", tokenList("realCvvToken")) + replaceCVVTokensInRecord(tokens, tableName = "cards", skyflowId = "id", cvvMap = CVVMap.EMPTY) + assertEquals("realCvvToken", tokens.getJSONArray("cvv").getJSONObject(0).getString("token")) + } +} From 63e65faf0495273bb003834bc43283d47916dcfd Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Fri, 31 Jul 2026 14:34:13 +0530 Subject: [PATCH 2/2] SK-3004: rename CollectElementInput.table to tableName, fix empty-CVV mock, add CVV tests - Rename CollectElementInput.table parameter to tableName; update all usages across SDK (Element, CollectContainer, ComposableContainer, TextField), samples, tests, and README - Fix CVV mock: capture empty-value CVV elements so their tokens are replaced with "" instead of leaking the real vault token; add explicit isEmpty guard at replacement site to avoid calling generateMockCVV(0,"") - Add 14 unit tests for MockCVV covering empty-entered-value (flat and nested), generateMockCVV(0) backstop, and all prior nested-path cases Co-Authored-By: Claude Sonnet 4.6 --- README.md | 22 +-- .../main/kotlin/Skyflow/CollectContainer.kt | 4 +- .../kotlin/Skyflow/CollectElementInput.kt | 6 +- Skyflow/src/main/kotlin/Skyflow/Element.kt | 4 +- Skyflow/src/main/kotlin/Skyflow/TextField.kt | 2 +- .../client/FlowDBCollectRequestBody.kt | 11 +- .../kotlin/Skyflow/collect/client/MockCVV.kt | 29 +-- .../Skyflow/composable/ComposableContainer.kt | 4 +- .../com/Skyflow/ComposableElementsTests.kt | 2 +- .../java/com/Skyflow/InputFormattingTest.kt | 16 +- .../src/test/java/com/Skyflow/MockCVVTest.kt | 165 ++++++++++++++++++ .../com/Skyflow/CardBrandChoiceActivity.kt | 8 +- .../main/java/com/Skyflow/CollectActivity.kt | 4 +- .../java/com/Skyflow/ComposableActivity.kt | 10 +- .../com/Skyflow/InputFormattingCollect.kt | 2 +- .../java/com/Skyflow/UpdateCollectActivity.kt | 6 +- 16 files changed, 239 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 669b543..4d27acf 100644 --- a/README.md +++ b/README.md @@ -414,7 +414,7 @@ the `context` param takes android `Context` object as described below: ```kotlin val collectElementInput = Skyflow.CollectElementInput( - table = "string", //the table this data belongs to + tableName = "string", //the table this data belongs to column = "string", //the column into which this data should be inserted type = Skyflow.ElementType.CARD_NUMBER, //Skyflow.ElementType enum inputStyles = Skyflow.Styles(), /*optional styles that should be applied to the form element*/ @@ -683,12 +683,12 @@ val container = skyflowClient.container(type = Skyflow.ContainerType.COLLECT) // Create a CollectElementInput val cardNumberInput = Skyflow.CollectElementInput( - table = "cards", + tableName = "cards", column = "cardNumber", type = Skyflow.ElementType.CARD_NUMBER, ) val cardHolderNameInput = Skyflow.CollectElementInput( - table = "cards", + tableName = "cards", column = "cardHolderName", type = Skyflow.ElementType.CARDHOLDER_NAME, ) @@ -763,7 +763,7 @@ val container = skyflowClient.container(type = Skyflow.ContainerType.COLLECT) // Create a CollectElementInput val cardNumberInput = Skyflow.CollectElementInput( - table = "cards", + tableName = "cards", column = "cardNumber", type = Skyflow.ElementType.CARD_NUMBER, ) @@ -801,7 +801,7 @@ val container = skyflowClient.container(type = Skyflow.ContainerType.COLLECT) // Create a CollectElementInput val cardNumberInput = Skyflow.CollectElementInput( - table = "cards", + tableName = "cards", column = "cardNumber", type = Skyflow.ElementType.CARD_NUMBER, ) @@ -1272,13 +1272,13 @@ val container = skyflowClient.container(type: Skyflow.ContainerType.COMPOSABLE, // Create a CollectElementInput val cardNumberInput = Skyflow.CollectElementInput( - table = "cards", + tableName = "cards", column = "cardNumber", type = Skyflow.ElementType.CARD_NUMBER, ) val cardHolderNameInput = Skyflow.CollectElementInput( - table = "cards", + tableName = "cards", column = "cardHolderName", type = Skyflow.ElementType.CARDHOLDER_NAME, ) @@ -1391,13 +1391,13 @@ val container = skyflowClient.container(type: Skyflow.ContainerType.COMPOSABLE, // Create a CollectElementInput val cardNumberInput = Skyflow.CollectElementInput( - table = "cards", + tableName = "cards", column = "cardNumber", type = Skyflow.ElementType.CARD_NUMBER, ) val cardHolderNameInput = Skyflow.CollectElementInput( - table = "cards", + tableName = "cards", column = "cardHolderName", type = Skyflow.ElementType.CARDHOLDER_NAME, ) @@ -1414,7 +1414,7 @@ try { // Update table, column, inputStyles properties on cardNumber. cardNumber.update(update = CollectElementInput( - table = "cards", + tableName = "cards", column = "cardHolderName", inputStyles = Skyflow.Styles(base: Style(borderColor: UIColor.red)) )) @@ -1458,7 +1458,7 @@ val container = skyflowClient.container(type: Skyflow.ContainerType.COMPOSABLE, // Create a CollectElementInput val cardNumberInput = Skyflow.CollectElementInput( - table = "cards", + tableName = "cards", column = "cardNumber", type = Skyflow.ElementType.CARD_NUMBER, ) diff --git a/Skyflow/src/main/kotlin/Skyflow/CollectContainer.kt b/Skyflow/src/main/kotlin/Skyflow/CollectContainer.kt index 8204cbf..21bce16 100644 --- a/Skyflow/src/main/kotlin/Skyflow/CollectContainer.kt +++ b/Skyflow/src/main/kotlin/Skyflow/CollectContainer.kt @@ -73,13 +73,13 @@ internal fun Container.validateElement(element: TextField, err throw SkyflowInternalError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, configuration.options.logLevel, arrayOf(element.columnName)) } when { - element.collectInput.table.equals(null) -> { + element.collectInput.tableName.equals(null) -> { throw SkyflowInternalError(SkyflowErrorCode.MISSING_TABLE_IN_ELEMENT, tag, configuration.options.logLevel, arrayOf(element.fieldType.toString())) } element.collectInput.column.equals(null) -> { throw SkyflowInternalError(SkyflowErrorCode.MISSING_COLUMN, tag, configuration.options.logLevel, arrayOf(element.fieldType.toString())) } - element.collectInput.table!!.isEmpty() -> { + element.collectInput.tableName!!.isEmpty() -> { throw SkyflowInternalError(SkyflowErrorCode.ELEMENT_EMPTY_TABLE_NAME, tag, configuration.options.logLevel, arrayOf(element.fieldType.toString())) } element.collectInput.column!!.isEmpty() -> { diff --git a/Skyflow/src/main/kotlin/Skyflow/CollectElementInput.kt b/Skyflow/src/main/kotlin/Skyflow/CollectElementInput.kt index 65294f6..ac70a56 100644 --- a/Skyflow/src/main/kotlin/Skyflow/CollectElementInput.kt +++ b/Skyflow/src/main/kotlin/Skyflow/CollectElementInput.kt @@ -3,7 +3,7 @@ package Skyflow import com.Skyflow.collect.elements.validations.ValidationSet class CollectElementInput( - internal var table: String? = null, + internal var tableName: String? = null, internal var column: String? = null, internal var inputStyles: Styles = Styles(), internal var labelStyles: Styles = Styles(), @@ -23,7 +23,7 @@ class CollectElementInput( internal lateinit var altText: String constructor( - table: String? = null, + tableName: String? = null, column: String? = null, type: SkyflowElementType, inputStyles: Styles = Styles(), @@ -35,7 +35,7 @@ class CollectElementInput( validations: ValidationSet = ValidationSet(), skyflowId: String? = null ) : this( - table, + tableName, column, inputStyles, labelStyles, diff --git a/Skyflow/src/main/kotlin/Skyflow/Element.kt b/Skyflow/src/main/kotlin/Skyflow/Element.kt index 236c348..49513de 100644 --- a/Skyflow/src/main/kotlin/Skyflow/Element.kt +++ b/Skyflow/src/main/kotlin/Skyflow/Element.kt @@ -31,8 +31,8 @@ open class Element @JvmOverloads constructor( this.collectInput = collectInput this.options = options this.fieldType = this.collectInput.type - if(!this.collectInput.table.equals(null)) - tableName = this.collectInput.table!! + if(!this.collectInput.tableName.equals(null)) + tableName = this.collectInput.tableName!! if(!this.collectInput.column.equals(null)) columnName = this.collectInput.column!! isRequired = this.options.required diff --git a/Skyflow/src/main/kotlin/Skyflow/TextField.kt b/Skyflow/src/main/kotlin/Skyflow/TextField.kt index e2c9188..fc87b27 100644 --- a/Skyflow/src/main/kotlin/Skyflow/TextField.kt +++ b/Skyflow/src/main/kotlin/Skyflow/TextField.kt @@ -305,7 +305,7 @@ class TextField @JvmOverloads constructor( } fun update(updateCollectInput: CollectElementInput) { - this.collectInput.table = updateCollectInput.table + this.collectInput.tableName = updateCollectInput.tableName this.collectInput.column = updateCollectInput.column this.collectInput.label = updateCollectInput.label this.collectInput.placeholder = updateCollectInput.placeholder diff --git a/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBCollectRequestBody.kt b/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBCollectRequestBody.kt index 312f8cd..ffc5eba 100644 --- a/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBCollectRequestBody.kt +++ b/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBCollectRequestBody.kt @@ -21,7 +21,7 @@ internal class FlowDBCollectRequestBody { // Merge additionalFields insert records into tableMap options.additionalFields?.records?.forEach { rec -> val existing = tableMap.getOrPut(rec.tableName) { mutableListOf() } - rec.data.forEach { (k, v) -> existing.add(CollectRequestRecord(k, v.toString())) } + rec.data.forEach { (k, v) -> existing.add(CollectRequestRecord(k, anyToJsonValue(v))) } } val recordsArray = JSONArray() @@ -109,6 +109,15 @@ internal class FlowDBCollectRequestBody { return tableMap } + private fun anyToJsonValue(v: Any?): Any { + if (v is Map<*, *>) { + val obj = JSONObject() + v.forEach { (mk, mv) -> obj.put(mk.toString(), anyToJsonValue(mv)) } + return obj + } + return v ?: JSONObject.NULL + } + private fun createJSONKey(obj: JSONObject, columnName: String, value: Any) { val keys = columnName.split(".").toTypedArray() if (obj.has(keys[0])) { diff --git a/Skyflow/src/main/kotlin/Skyflow/collect/client/MockCVV.kt b/Skyflow/src/main/kotlin/Skyflow/collect/client/MockCVV.kt index a8249ac..5c6254d 100644 --- a/Skyflow/src/main/kotlin/Skyflow/collect/client/MockCVV.kt +++ b/Skyflow/src/main/kotlin/Skyflow/collect/client/MockCVV.kt @@ -34,7 +34,6 @@ internal class CVVMap( for (element in elements) { if (element.fieldType != SkyflowElementType.CVV) continue val value = element.getValue() - if (value.isEmpty()) continue val skyflowId = element.skyflowId if (!skyflowId.isNullOrEmpty()) { byRecordId.getOrPut(skyflowId) { LinkedHashMap() }[element.columnName] = value @@ -53,9 +52,7 @@ internal class CVVMap( val columns = LinkedHashMap() for (element in elements) { if (element.fieldType != SkyflowElementType.CVV) continue - val value = element.getValue() - if (value.isEmpty()) continue - columns[element.columnName] = value + columns[element.columnName] = element.getValue() } return if (columns.isEmpty()) EMPTY else CVVMap(emptyMap(), mapOf(skyflowId to columns)) } @@ -86,9 +83,15 @@ internal fun generateMockCVV(length: Int, actualValue: String): String { * Replaces the token value of every captured CVV column in [tokens] with a freshly generated mock * placeholder that matches the entered length and never equals that element's own entered value. * - * The vault returns each column as a list of `{ token, tokenGroupName }` entries, so one mock is - * generated per column and applied to every entry for that column. Updates are matched by record id - * first, then inserts by table name. Non-CVV columns and hashed data are left untouched. + * tokens is keyed only by the TOP-LEVEL column name. Nested sub-fields appear as separate entries + * in that column's list, each carrying a dotted "path" field. The replacement rule: + * - Flat column (no dot in column name): replace entries that have NO "path" field. + * - Nested column (e.g. "address.city.street"): split at first dot → topKey="address", + * nestedPath="city.street"; replace ONLY the entry whose "path" is EXACTLY "city.street". + * Exact equality prevents "city" from matching "city.street" or "city.ward". + * + * One mock is generated per column (same value applied to all matching entries). Updates are matched + * by record id first, then inserts by table name. Non-CVV columns and hashed data are untouched. * * Cross-element collision is intentionally ignored: a mock may coincidentally equal a *different* * element's entered value, but entered values never leave the device to the app, so there is no @@ -105,11 +108,17 @@ internal fun replaceCVVTokensInRecord( ?: (if (tableName.isNotEmpty()) cvvMap.byTable[tableName] else null) ?: return for ((column, enteredValue) in columns) { - val entries = tokens.optJSONArray(column) ?: continue - val mock = generateMockCVV(enteredValue.length, enteredValue) + val dotIndex = column.indexOf('.') + val topKey = if (dotIndex == -1) column else column.substring(0, dotIndex) + val nestedPath = if (dotIndex == -1) null else column.substring(dotIndex + 1) + + val entries = tokens.optJSONArray(topKey) ?: continue + val mock = if (enteredValue.isEmpty()) "" else generateMockCVV(enteredValue.length, enteredValue) for (i in 0 until entries.length()) { val entry = entries.optJSONObject(i) ?: continue - if (entry.has("token")) { + val entryPath = if (entry.has("path")) entry.optString("path") else null + val matches = if (nestedPath == null) entryPath == null else entryPath == nestedPath + if (matches && entry.has("token")) { entry.put("token", mock) } } diff --git a/Skyflow/src/main/kotlin/Skyflow/composable/ComposableContainer.kt b/Skyflow/src/main/kotlin/Skyflow/composable/ComposableContainer.kt index 5c2089b..74609d6 100644 --- a/Skyflow/src/main/kotlin/Skyflow/composable/ComposableContainer.kt +++ b/Skyflow/src/main/kotlin/Skyflow/composable/ComposableContainer.kt @@ -157,7 +157,7 @@ private fun Container.validateElement( ) } when { - element.collectInput.table.equals(null) -> { + element.collectInput.tableName.equals(null) -> { throw SkyflowInternalError( SkyflowErrorCode.MISSING_TABLE_IN_ELEMENT, tag, @@ -173,7 +173,7 @@ private fun Container.validateElement( arrayOf(element.fieldType.toString()) ) } - element.collectInput.table!!.isEmpty() -> { + element.collectInput.tableName!!.isEmpty() -> { throw SkyflowInternalError( SkyflowErrorCode.ELEMENT_EMPTY_TABLE_NAME, tag, diff --git a/Skyflow/src/test/java/com/Skyflow/ComposableElementsTests.kt b/Skyflow/src/test/java/com/Skyflow/ComposableElementsTests.kt index 1dfd0b2..a001050 100644 --- a/Skyflow/src/test/java/com/Skyflow/ComposableElementsTests.kt +++ b/Skyflow/src/test/java/com/Skyflow/ComposableElementsTests.kt @@ -59,7 +59,7 @@ class ComposableElementsTests { ContainerOptions(layout = arrayOf(1)) ) val collectInput = CollectElementInput( - table = "cards", column = "card_number", + tableName = "cards", column = "card_number", type = SkyflowElementType.CARD_NUMBER, placeholder = "card number" ) diff --git a/Skyflow/src/test/java/com/Skyflow/InputFormattingTest.kt b/Skyflow/src/test/java/com/Skyflow/InputFormattingTest.kt index 12639ac..a15b272 100644 --- a/Skyflow/src/test/java/com/Skyflow/InputFormattingTest.kt +++ b/Skyflow/src/test/java/com/Skyflow/InputFormattingTest.kt @@ -42,56 +42,56 @@ class InputFormattingTest { private fun createCollectElements() { nameInput = CollectElementInput( - table = "cards", + tableName = "cards", column = "Name", SkyflowElementType.CARDHOLDER_NAME, placeholder = "name", ) cardNumberInput = CollectElementInput( - table = "cards", + tableName = "cards", column = "card_number", SkyflowElementType.CARD_NUMBER, placeholder = "card number", ) cvvInput = CollectElementInput( - table = "cards", + tableName = "cards", column = "CVV", SkyflowElementType.CVV, placeholder = "cvv", ) pinInput = CollectElementInput( - table = "cards", + tableName = "cards", column = "PIN", SkyflowElementType.PIN, placeholder = "pin", ) monthInput = CollectElementInput( - table = "cards", + tableName = "cards", column = "exp_month", SkyflowElementType.EXPIRATION_MONTH, placeholder = "expiry month", ) yearInput = CollectElementInput( - table = "cards", + tableName = "cards", column = "exp_year", SkyflowElementType.EXPIRATION_YEAR, placeholder = "yyyy", ) dateInput = CollectElementInput( - table = "cards", + tableName = "cards", column = "exp_date", SkyflowElementType.EXPIRATION_DATE, placeholder = "mm/yy", ) phoneInput = CollectElementInput( - table = "cards", + tableName = "cards", column = "Phone Number", SkyflowElementType.INPUT_FIELD, placeholder = "phone number", diff --git a/Skyflow/src/test/java/com/Skyflow/MockCVVTest.kt b/Skyflow/src/test/java/com/Skyflow/MockCVVTest.kt index 5a50709..68e011e 100644 --- a/Skyflow/src/test/java/com/Skyflow/MockCVVTest.kt +++ b/Skyflow/src/test/java/com/Skyflow/MockCVVTest.kt @@ -17,6 +17,12 @@ class MockCVVTest { // ---- generateMockCVV ---- + @Test + fun `generateMockCVV returns empty string for length 0`() { + assertEquals("", generateMockCVV(0, "")) + assertEquals("", generateMockCVV(-1, "")) + } + @Test fun `generateMockCVV produces numeric string of requested length`() { for (length in intArrayOf(3, 4)) { @@ -47,6 +53,36 @@ class MockCVVTest { return array } + /** Builds a path-carrying token entry as the vault returns for nested JSON columns. */ + private fun pathEntry(path: String, token: String): JSONObject = + JSONObject().put("path", path).put("token", token).put("tokenGroupName", "grp") + + /** + * Builds the address column token list from the background doc: + * [whole-col, pincode, city, city.street, city.ward] + */ + private fun addressTokenList(): JSONArray = JSONArray().apply { + put(JSONObject().put("token", "whole-col-tok").put("tokenGroupName", "grp")) // no path + put(pathEntry("pincode", "pincode-tok")) + put(pathEntry("city", "city-tok")) + put(pathEntry("city.street", "street-tok")) + put(pathEntry("city.ward", "ward-tok")) + } + + /** + * Builds the address column token list extended with a nested CVV sub-field: + * address.details.cvv — sits alongside city.street / city.ward to verify exact isolation. + */ + private fun addressWithCvvTokenList(): JSONArray = JSONArray().apply { + put(JSONObject().put("token", "whole-col-tok").put("tokenGroupName", "grp")) // no path + put(pathEntry("pincode", "pincode-tok")) + put(pathEntry("city", "city-tok")) + put(pathEntry("city.street", "street-tok")) + put(pathEntry("city.ward", "ward-tok")) + put(pathEntry("details", "details-tok")) + put(pathEntry("details.cvv", "real-cvv-tok")) + } + // ---- replaceCVVTokensInRecord ---- @Test @@ -115,4 +151,133 @@ class MockCVVTest { replaceCVVTokensInRecord(tokens, tableName = "cards", skyflowId = "id", cvvMap = CVVMap.EMPTY) assertEquals("realCvvToken", tokens.getJSONArray("cvv").getJSONObject(0).getString("token")) } + + // ---- empty entered-value tests ---- + + @Test + fun `flat CVV with empty entered value sets token to empty string, sibling columns untouched`() { + val tokens = JSONObject() + .put("cvv", tokenList("realCvvToken")) + .put("card_number", tokenList("realCardToken")) + val cvvMap = CVVMap(byTable = mapOf("cards" to mapOf("cvv" to "")), byRecordId = emptyMap()) + + replaceCVVTokensInRecord(tokens, tableName = "cards", skyflowId = "id", cvvMap = cvvMap) + + assertEquals("", tokens.getJSONArray("cvv").getJSONObject(0).getString("token")) + assertEquals("realCardToken", tokens.getJSONArray("card_number").getJSONObject(0).getString("token")) + } + + @Test + fun `nested CVV with empty entered value sets only that path entry to empty string, siblings untouched`() { + val tokens = JSONObject().put("address", addressTokenList()) + val cvvMap = CVVMap(byTable = mapOf("nested" to mapOf("address.pincode" to "")), byRecordId = emptyMap()) + + replaceCVVTokensInRecord(tokens, tableName = "nested", skyflowId = "id", cvvMap = cvvMap) + + val addr = tokens.getJSONArray("address") + assertEquals("", addr.getJSONObject(1).getString("token")) // pincode entry: empty + assertEquals("whole-col-tok", addr.getJSONObject(0).getString("token")) + assertEquals("city-tok", addr.getJSONObject(2).getString("token")) + assertEquals("street-tok", addr.getJSONObject(3).getString("token")) + assertEquals("ward-tok", addr.getJSONObject(4).getString("token")) + } + + // ---- nested JSON column tests ---- + + @Test + fun `one-level nested column replaces only its path entry, leaves parent and siblings intact`() { + val tokens = JSONObject() + .put("address", addressTokenList()) + .put("card_number", tokenList("card-tok")) + val cvvMap = CVVMap(byTable = mapOf("nested" to mapOf("address.pincode" to "500")), byRecordId = emptyMap()) + + replaceCVVTokensInRecord(tokens, tableName = "nested", skyflowId = "id", cvvMap = cvvMap) + + val addr = tokens.getJSONArray("address") + // whole-column entry (no path): untouched + assertEquals("whole-col-tok", addr.getJSONObject(0).getString("token")) + // pincode entry: replaced with 3-digit mock ≠ "500" + val pincodeToken = addr.getJSONObject(1).getString("token") + assertEquals(3, pincodeToken.length) + assertTrue(pincodeToken.all { it.isDigit() }) + assertNotEquals("500", pincodeToken) + // city, city.street, city.ward: untouched + assertEquals("city-tok", addr.getJSONObject(2).getString("token")) + assertEquals("street-tok", addr.getJSONObject(3).getString("token")) + assertEquals("ward-tok", addr.getJSONObject(4).getString("token")) + // other column untouched + assertEquals("card-tok", tokens.getJSONArray("card_number").getJSONObject(0).getString("token")) + } + + @Test + fun `two-level nested column replaces only exact path entry, parent and sibling paths intact`() { + val tokens = JSONObject().put("address", addressTokenList()) + val cvvMap = CVVMap(byTable = mapOf("nested" to mapOf("address.city.street" to "123")), byRecordId = emptyMap()) + + replaceCVVTokensInRecord(tokens, tableName = "nested", skyflowId = "id", cvvMap = cvvMap) + + val addr = tokens.getJSONArray("address") + // whole-column, pincode, city, city.ward: all untouched + assertEquals("whole-col-tok", addr.getJSONObject(0).getString("token")) + assertEquals("pincode-tok", addr.getJSONObject(1).getString("token")) + assertEquals("city-tok", addr.getJSONObject(2).getString("token")) + assertEquals("ward-tok", addr.getJSONObject(4).getString("token")) + // city.street: replaced with 3-digit mock ≠ "123" + val streetToken = addr.getJSONObject(3).getString("token") + assertEquals(3, streetToken.length) + assertTrue(streetToken.all { it.isDigit() }) + assertNotEquals("123", streetToken) + assertNotEquals("street-tok", streetToken) + } + + @Test + fun `nested CVV column replaces only its exact path entry, all sibling paths intact`() { + // address.details.cvv — two-level nested CVV sub-field + val tokens = JSONObject() + .put("address", addressWithCvvTokenList()) + .put("card_number", tokenList("card-tok")) + val cvvMap = CVVMap(byTable = mapOf("nested" to mapOf("address.details.cvv" to "4321")), byRecordId = emptyMap()) + + replaceCVVTokensInRecord(tokens, tableName = "nested", skyflowId = "id", cvvMap = cvvMap) + + val addr = tokens.getJSONArray("address") + // whole-column, pincode, city, city.street, city.ward, details: all untouched + assertEquals("whole-col-tok", addr.getJSONObject(0).getString("token")) + assertEquals("pincode-tok", addr.getJSONObject(1).getString("token")) + assertEquals("city-tok", addr.getJSONObject(2).getString("token")) + assertEquals("street-tok", addr.getJSONObject(3).getString("token")) + assertEquals("ward-tok", addr.getJSONObject(4).getString("token")) + assertEquals("details-tok", addr.getJSONObject(5).getString("token")) + // details.cvv: replaced with 4-digit mock ≠ "4321" + val cvvToken = addr.getJSONObject(6).getString("token") + assertEquals(4, cvvToken.length) + assertTrue(cvvToken.all { it.isDigit() }) + assertNotEquals("4321", cvvToken) + assertNotEquals("real-cvv-tok", cvvToken) + // other column untouched + assertEquals("card-tok", tokens.getJSONArray("card_number").getJSONObject(0).getString("token")) + } + + @Test + fun `flat column with mixed list replaces only path-less entries, leaves path-carrying entries intact`() { + val mixed = JSONArray().apply { + put(JSONObject().put("token", "flat-1").put("tokenGroupName", "grp")) // no path + put(JSONObject().put("token", "flat-2").put("tokenGroupName", "grp")) // no path + put(pathEntry("some.sub", "sub-tok")) // has path + } + val tokens = JSONObject().put("cvv", mixed) + val cvvMap = CVVMap(byTable = mapOf("cards" to mapOf("cvv" to "321")), byRecordId = emptyMap()) + + replaceCVVTokensInRecord(tokens, tableName = "cards", skyflowId = "id", cvvMap = cvvMap) + + val entries = tokens.getJSONArray("cvv") + // path-less entries replaced with same mock + val mock = entries.getJSONObject(0).getString("token") + assertEquals(3, mock.length) + assertTrue(mock.all { it.isDigit() }) + assertNotEquals("321", mock) + assertEquals(mock, entries.getJSONObject(1).getString("token")) + // path-carrying entry untouched + assertEquals("sub-tok", entries.getJSONObject(2).getString("token")) + } } diff --git a/samples/src/main/java/com/Skyflow/CardBrandChoiceActivity.kt b/samples/src/main/java/com/Skyflow/CardBrandChoiceActivity.kt index 29e8ce7..5531f1e 100644 --- a/samples/src/main/java/com/Skyflow/CardBrandChoiceActivity.kt +++ b/samples/src/main/java/com/Skyflow/CardBrandChoiceActivity.kt @@ -47,7 +47,7 @@ class CardBrandChoiceActivity : AppCompatActivity() { validationSet.add(LengthMatchRule(2, 20, "not valid")) val cardNumberInput = CollectElementInput( - table = "", + tableName = "", column = "", SkyflowElementType.CARD_NUMBER, inputStyles = styles, @@ -58,7 +58,7 @@ class CardBrandChoiceActivity : AppCompatActivity() { ) val expiryDateInput = CollectElementInput( - table = "", + tableName = "", column = "", SkyflowElementType.EXPIRATION_DATE, inputStyles = styles, @@ -69,7 +69,7 @@ class CardBrandChoiceActivity : AppCompatActivity() { ) val nameInput = CollectElementInput( - table = "", + tableName = "", column = "", SkyflowElementType.CARDHOLDER_NAME, inputStyles = styles, @@ -81,7 +81,7 @@ class CardBrandChoiceActivity : AppCompatActivity() { ) val cvvInput = CollectElementInput( - table = "", + tableName = "", column = "", type = SkyflowElementType.CVV, inputStyles = styles, diff --git a/samples/src/main/java/com/Skyflow/CollectActivity.kt b/samples/src/main/java/com/Skyflow/CollectActivity.kt index 2b34883..04f4160 100644 --- a/samples/src/main/java/com/Skyflow/CollectActivity.kt +++ b/samples/src/main/java/com/Skyflow/CollectActivity.kt @@ -41,7 +41,7 @@ class CollectActivity : AppCompatActivity() { val errorStyles = Styles(errorStyle) val cardNumberInput = CollectElementInput( - table = "", + tableName = "", column = "", type = SkyflowElementType.CARD_NUMBER, inputStyles = styles, @@ -50,7 +50,7 @@ class CollectActivity : AppCompatActivity() { placeholder = "Card Number" ) val expiryInput = CollectElementInput( - table = "", + tableName = "", column = "", type = SkyflowElementType.EXPIRATION_DATE, inputStyles = styles, diff --git a/samples/src/main/java/com/Skyflow/ComposableActivity.kt b/samples/src/main/java/com/Skyflow/ComposableActivity.kt index 0af79eb..173c741 100644 --- a/samples/src/main/java/com/Skyflow/ComposableActivity.kt +++ b/samples/src/main/java/com/Skyflow/ComposableActivity.kt @@ -140,7 +140,7 @@ class ComposableActivity : AppCompatActivity() { val options = CollectElementOptions(true) val nameInput = CollectElementInput( - table = "cards", + tableName = "cards", column = "cardholder_name", type = SkyflowElementType.CARDHOLDER_NAME, inputStyles = styles, @@ -152,7 +152,7 @@ class ComposableActivity : AppCompatActivity() { val name = composableContainer.create(this, nameInput, options) val cardNumberInput = CollectElementInput( - table = "cards", + tableName = "cards", column = "card_number", type = SkyflowElementType.CARD_NUMBER, inputStyles = cardNumberStyles, @@ -168,7 +168,7 @@ class ComposableActivity : AppCompatActivity() { ) val expiryMonthInput = CollectElementInput( - table = "cards", + tableName = "cards", column = "exp_month", type = SkyflowElementType.EXPIRATION_MONTH, inputStyles = expDateStyles, @@ -180,7 +180,7 @@ class ComposableActivity : AppCompatActivity() { val expMonth = composableContainer.create(this, expiryMonthInput, options) val cvvInput = CollectElementInput( - table = "cards", + tableName = "cards", column = "cvv", type = SkyflowElementType.CVV, inputStyles = cvvStyles, @@ -192,7 +192,7 @@ class ComposableActivity : AppCompatActivity() { val cvv = composableContainer.create(this, cvvInput, options) val pinInput = CollectElementInput( - table = "cards", + tableName = "cards", column = "pin", type = SkyflowElementType.PIN, inputStyles = cvvStyles, diff --git a/samples/src/main/java/com/Skyflow/InputFormattingCollect.kt b/samples/src/main/java/com/Skyflow/InputFormattingCollect.kt index 4d69e7d..cf58d53 100644 --- a/samples/src/main/java/com/Skyflow/InputFormattingCollect.kt +++ b/samples/src/main/java/com/Skyflow/InputFormattingCollect.kt @@ -130,7 +130,7 @@ class InputFormattingCollect : AppCompatActivity() { ) val input = CollectElementInput( - table = "cards", + tableName = "cards", column = "zip_code", type = SkyflowElementType.INPUT_FIELD, styles, diff --git a/samples/src/main/java/com/Skyflow/UpdateCollectActivity.kt b/samples/src/main/java/com/Skyflow/UpdateCollectActivity.kt index b9ce71a..e7bc569 100644 --- a/samples/src/main/java/com/Skyflow/UpdateCollectActivity.kt +++ b/samples/src/main/java/com/Skyflow/UpdateCollectActivity.kt @@ -50,7 +50,7 @@ class UpdateCollectActivity : AppCompatActivity() { val skyflowId = "" val cardNumberInput = CollectElementInput( - table = "", + tableName = "", column = "", type = SkyflowElementType.CARD_NUMBER, inputStyles = inputStyles, @@ -60,7 +60,7 @@ class UpdateCollectActivity : AppCompatActivity() { skyflowId = skyflowId ) val nameInput = CollectElementInput( - table = "", + tableName = "", column = "", type = SkyflowElementType.CARDHOLDER_NAME, inputStyles = inputStyles, @@ -70,7 +70,7 @@ class UpdateCollectActivity : AppCompatActivity() { skyflowId = skyflowId ) val expiryInput = CollectElementInput( - table = "", + tableName = "", column = "", type = SkyflowElementType.EXPIRATION_DATE, inputStyles = inputStyles,