diff --git a/.gitignore b/.gitignore index 8ccc4f8..12ec793 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ .externalNativeBuild .cxx local.properties + +flowvault-samples/ diff --git a/README.md b/README.md index 8282a99..104ea25 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,37 @@ Skyflow’s android SDK can be used to securely collect, tokenize, and display sensitive data in the mobile without exposing your front-end infrastructure to sensitive data. # Table of Contents -* [Installation](#installation) - * [Requirements](#requirements) - * [Configuration](#configuration) -* [Initializing Skyflow-android](#initializing-skyflow-android) -* [Securely collecting data client-side](#securely-collecting-data-client-side) -* [Securely collecting data client-side using composable elements](#securely-collecting-data-client-side-using-composable-elements) -* [Securely revealing data client-side](#securely-revealing-data-client-side) + +1. [**Installation**](#installation) + 1. [Requirements](#requirements) + 2. [Configuration](#configuration) +2. [**Quick Start**](#quick-start) + 1. [Collect data](#collect-data) + 2. [Reveal data](#reveal-data) +3. [**Initializing skyflow-android**](#initializing-skyflow-android) +4. [**Securely collecting data client-side**](#securely-collecting-data-client-side) + 1. [Using Skyflow Elements to collect data](#using-skyflow-elements-to-collect-data) + 2. [Using Skyflow Elements to update data](#using-skyflow-elements-to-update-data) + 3. [Validations](#validations) + 4. [Event Listener on Collect Elements](#event-listener-on-collect-elements) + 5. [UI Error for Collect Elements](#ui-error-for-collect-elements) + 6. [Set and Clear value for Collect Elements (DEV ENV ONLY)](#set-and-clear-value-for-collect-elements-dev-env-only) +5. [**Securely collecting data client-side using composable elements**](#securely-collecting-data-client-side-using-composable-elements) + 1. [Using Skyflow Composable Elements to collect data](#using-skyflow-composable-elements-to-collect-data) + 2. [Using Skyflow Composable Elements to update data](#using-skyflow-composable-elements-to-update-data) + 3. [Event Listeners on Composable Elements](#event-listeners-on-composable-elements) + 4. [Update Composable Elements](#update-composable-elements) + 5. [Event Listeners on Composable Container](#event-listeners-on-composable-container) +6. [**Securely revealing data client-side**](#securely-revealing-data-client-side) + 1. [Using Skyflow Elements to reveal data](#using-skyflow-elements-to-reveal-data) + 2. [UI Error for Reveal Elements](#ui-error-for-reveal-elements) + 3. [Set token for Reveal Elements](#set-token-for-reveal-elements) + 4. [Set and Clear altText for Reveal Elements](#set-and-clear-alttext-for-reveal-elements) +7. [**Typed callbacks and response handling**](#typed-callbacks-and-response-handling) + 1. [Collect with typed callbacks](#collect-with-typed-callbacks) + 2. [Reveal with typed callbacks](#reveal-with-typed-callbacks) +8. [**Limitation**](#limitation) +9. [**Reporting a Vulnerability**](#reporting-a-vulnerability) # Installation @@ -23,19 +47,22 @@ Skyflow’s android SDK can be used to securely collect, tokenize, and display s - Android Gradle Plugin 8.6.0 and above ## Configuration +--- +> **Note:** The Android SDK is distributed through GitHub Package Registry (not Maven Central), so adding it as a dependency requires GitHub authentication — even for public packages. This is why the steps below include generating a Personal Access Token. The iOS and JS SDKs don't require this step. + ### Step 1: Generate a Personal Access Token for GitHub -- Inside you GitHub account: +- Inside your GitHub account: - Settings -> Developer Settings -> Personal Access Tokens -> Generate new token - Make sure you select the following scopes (“read:packages”) and Generate a token - After Generating make sure to copy your new personal access token. You cannot see it again! The only option is to generate a new key. ### Step 2: Store your GitHub — Personal Access Token details - Create a github.properties file within your root Android project -- In case of a public repository make sure you add this file to .gitignore for keep the token private +- In case of a public repository make sure you add this file to .gitignore to keep the token private - Add properties gpr.usr=GITHUB_USER_NAME and gpr.key=PERSONAL_ACCESS_TOKEN - Replace GITHUB_USER_NAME with personal / organisation Github user NAME and PERSONAL_ACCESS_TOKEN with the token generated in [Step 1](#step-1-generate-a-personal-access-token-for-github) -Alternatively you can also add the GPR_USER_NAME and GPR_PAT values to your environment variables on you local machine or build server to avoid creating a github properties file +Alternatively you can also add the GPR_USER_NAME and GPR_PAT values to your environment variables on your local machine or build server to avoid creating a github properties file ### Step 3: Adding the dependency to the project @@ -97,6 +124,71 @@ Alternatively you can also add the GPR_USER_NAME and GPR_PAT values to your envi ``` +# Quick Start + +### Collect data + +Collect a card number and get back a token: + +```kotlin +// 1. Configure and initialize the client +val config = Skyflow.Configuration( + vaultID = "", + vaultURL = "", + tokenProvider = myTokenProvider // your Skyflow.TokenProvider implementation — see below +) +val skyflowClient = Skyflow.init(config) + +// 2. Create a container and a Collect Element for the card number +val container = skyflowClient.container(Skyflow.ContainerType.COLLECT) +val cardNumberInput = Skyflow.CollectElementInput( + tableName = "cards", + column = "card_number", + type = Skyflow.ElementType.CARD_NUMBER +) +val cardNumberElement = container.create(context = this, input = cardNumberInput) +parent.addView(cardNumberElement) // it's a View — mount it like any other + +// 3. Collect the value and get back a token +container.collect(object : CollectCallback { + override fun onSuccess(response: CollectResponse) { + println(response.records) + } + override fun onFailure(error: SkyflowError) { + println(error.message) + } +}) +``` + +### Reveal data + +Reveal a token back to its real value (using the same `skyflowClient` from above): + +```kotlin +// 1. Create a container and a Reveal Element for the token +val revealContainer = skyflowClient.container(Skyflow.ContainerType.REVEAL) +val cardNumberReveal = Skyflow.RevealElementInput( + token = "f3907186-e7e2-466f-91e5-48e12c2bcbc1", + label = "Card Number" +) +val revealElement = revealContainer.create(context = this, input = cardNumberReveal) +parent.addView(revealElement) // shows the real value once revealed + +// 2. Reveal it +revealContainer.reveal(object : RevealCallback { + override fun onSuccess(response: RevealResponse) { + println(response.records) + } + override fun onFailure(error: SkyflowError) { + println(error.message) + } +}) +``` + +That's the whole round trip — no card data ever touches your app code on the way in, and it's only ever displayed, never returned to your code, on the way out. Everything below covers this in depth: `TokenProvider`, styling, validation, updates, upsert, and more. + +--- + # Initializing skyflow-android ---- Use the ```init()``` method to initialize a Skyflow client as shown below. @@ -108,15 +200,15 @@ val config = Skyflow.Configuration( vaultID = , vaultURL = , tokenProvider = demoTokenProvider, - options: Skyflow.Options( - logLevel : Skyflow.LogLevel, // optional, if not specified loglevel is ERROR. - env: SKyflow.Env //optiuona, if not specified env is PROD. - ) + options = Skyflow.Options( + logLevel = Skyflow.LogLevel.INFO, // optional, if not specified logLevel is ERROR + env = Skyflow.Env.PROD // optional, if not specified env is PROD + ) ) val skyflowClient = Skyflow.init(config) ``` -For the tokenProvider parameter, pass in an implementation of the Skyflow.TokenProvider interface that declares a getAccessToken method which retrieves a Skyflow bearer token from your backend. This function will be invoked when the SDK needs to insert or retrieve data from the vault. +For the tokenProvider parameter, pass in an implementation of the Skyflow.TokenProvider interface that declares a getBearerToken method which retrieves a Skyflow bearer token from your backend. This function will be invoked when the SDK needs to insert or retrieve data from the vault. For example, if the response of the consumer tokenAPI is in the below format @@ -200,7 +292,6 @@ For `env` parameter, there are 2 accepted values in Skyflow.Env --- # Securely collecting data client-side -- [**Inserting data into the vault**](#inserting-data-into-the-vault) - [**Using Skyflow Elements to collect data**](#using-skyflow-elements-to-collect-data) - [**Using Skyflow Elements to update data**](#using-skyflow-elements-to-update-data) - [**Event Listener on Collect Elements**](#event-listener-on-collect-elements) @@ -208,64 +299,6 @@ For `env` parameter, there are 2 accepted values in Skyflow.Env - [**Set and Clear value for Collect Elements (DEV ENV ONLY)**](#set-and-clear-value-for-collect-elements-dev-env-only) -## Inserting data into the vault - -To insert data into the vault from the integrated application, use the ```insert(records: JSONObject, options: InsertOptions?= InsertOptions() , callback: Skyflow.Callback)``` method of the Skyflow client. The records parameter takes a JSON object of the records to be inserted in the below format. The options parameter takes a object of optional parameters for the insertion. `insert` method also support upsert operations. See below: - -```json5 -{ - "records": [ - { - table: "string", //table into which record should be inserted - fields: { - column1: "value", //column names should match vault column names - ///... additional fields - } - }, - ///...additional records - ] -} -``` - -An example of an insert call is given below: - -```kt -//Upsert options -val upsertArray = JSONArray() -val upsertColumn = JSONObject() -upsertColumn.put("table", "cards") -upsertColumn.put("column", "card_number") -upsertArray.put(upsertColumn) -val insertOptions = Skyflow.InsertOptions(tokens= false,upsert= upsertArray) /*indicates whether or not tokens should be returned for the inserted data. Defaults to 'true'*/ -val insertCallback = InsertCallback() //Custom callback - implementation of Skyflow.Callback -val records = JSONObject() -val recordsArray = JSONArray() -val record = JSONObject() -record.put("table", "cards") -val fields = JSONObject() -fields.put("expiry_date", "12/2028") -fields.put("cardNumber", "41111111111") -record.put("fields", fields) -recordsArray.put(record) -records.put("records", recordsArray) -skyflowClient.insert(records = records, options = insertOptions, callback = insertCallback); -``` - -**Response :** -```json -{ - "records": [ - { - "table": "cards", - "fields":{ - "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - "expiry_date": "1989cb56-63da-4482-a2df-1f74cd0dd1a5" - } - } - ] -} -``` - ## Using Skyflow Elements to collect data **Skyflow Elements** provide developers with pre-built form elements to securely collect sensitive data client-side. This reduces your PCI compliance scope by not exposing your front-end application to sensitive data. Follow the steps below to securely collect data with Skyflow Elements in your application. @@ -284,8 +317,8 @@ To create a collect element, we must first construct `Skyflow.CollectElementInpu ```kt Skyflow.CollectElementInput( - table : String, //the table this data belongs to - column : String, //the column into which this data should be inserted + tableName: String, // required, the table this data belongs to + column: String, // required, the column into which this data should be inserted type: Skyflow.ElementType //Skyflow.ElementType enum inputStyles: Skyflow.Styles, //optional styles that should be applied to the form element labelStyles: Skyflow.Styles, //optional styles that will be applied to the label of the collect element @@ -296,7 +329,7 @@ Skyflow.CollectElementInput( validations: ValidationSet // optional set of validations for collect element ) ``` -The `table` and `column` parameters indicate which table and column in the vault the Element corresponds to. +The `tableName` and `column` fields indicate which table and column in the vault the Element corresponds to. Note: Use dot delimited strings to specify columns nested inside JSON fields (e.g. address.street.line1). The `inputStyles` field accepts a Skyflow.Styles object which consists of multiple `Skyflow.Style` objects which should be applied to the form element in the following states: @@ -372,7 +405,7 @@ The parameters in `Skyflow.Style` object that are respected for `label` and `err Other parameters in the `Skyflow.Style` object are ignored for `label` and `errorText` text views. -Finally, the `type` field takes a Skyflow ElementType. Each type applies the appropriate regex and validations to the form element. There are currently 5 types: +Finally, the `type` field takes a Skyflow ElementType. Each type applies the appropriate regex and validations to the form element. There are currently 8 types: - `INPUT_FIELD` - `CARDHOLDER_NAME` - `CARD_NUMBER` @@ -442,10 +475,10 @@ Collect Element Options examples for INPUT_FIELD Example 1 ```kotlin Skyflow.CollectElementOptions( - required: true, - enableCardIcon: true, - format: "+91 XXXX-XX-XXXX", - translation: hashmapOf('X' to "[0-9]") + required = true, + enableCardIcon = true, + format = "+91 XXXX-XX-XXXX", + translation = hashmapOf('X' to "[0-9]") ) ``` User input: "1234121234" @@ -455,10 +488,10 @@ Value displayed in INPUT_FIELD: "+91 1234-12-1234" Example 2 ```kotlin Skyflow.CollectElementOptions( - required: true, - enableCardIcon: true, - format: "AY XX-XXX-XXXX", - translation: hashmapOf('X' to "[0-9]", 'Y' to "[A-Z]") + required = true, + enableCardIcon = true, + format = "AY XX-XXX-XXXX", + translation = hashmapOf('X' to "[0-9]", 'Y' to "[A-Z]") ) ``` User input: "B1234121234" @@ -470,7 +503,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*/ @@ -478,17 +511,17 @@ val collectElementInput = Skyflow.CollectElementInput( errorTextStyles = Skyflow.Styles(), //optional styles that will be applied to the errorText of the collect element label = "string", //optional label for the form element placeholder = "string", //optional placeholder for the form element - altText: String, //(DEPRECATED) optional string that acts as an initial value for the collect element + altText = "string", //(DEPRECATED) optional string that acts as an initial value for the collect element validations = ValidationSet() // optional set of validations for the input element ) val collectElementOptions = Skyflow.CollectElementOptions( required = false, //indicates whether the field is marked as required. Defaults to 'false' - enableCardIcon = true //indicates whether card icon should be enabled (only for CARD_NUMBER inputs) + enableCardIcon = true, //indicates whether card icon should be enabled (only for CARD_NUMBER inputs) format = "mm/yy" //Format for the element (only applies currently for EXPIRATION_DATE element type) ) -const element = container.create(context = Context, collectElementInput, collectElementOptions) +const element = container.create(context = this, collectElementInput, collectElementOptions) ``` @@ -519,203 +552,32 @@ fun clearFields(elements: List) { ``` -### Step 4 : Collect data from Elements -When the form is ready to be submitted, call the collect(options: Skyflow.CollectOptions? = nil, callback: Skyflow.Callback) method on the container object. The options parameter takes `Skyflow.CollectOptions` object. - -`Skyflow.CollectOptions` takes two optional fields -- `tokens`: indicates whether tokens for the collected data should be returned or not. Defaults to 'true' -- `additionalFields`: Non-PCI elements data to be inserted into the vault which should be in the `records` object format as described in the above [Inserting data into vault](#Inserting-data-into-the-vault) section. - -```kt -// NON-PCI fields object creation -val nonPCIRecords = JSONObject() -val recordsArray = JSONArray() -val record = JSONObject() -record.put("table", "persons") -val fields = JSONObject() -fields.put("gender", "MALE") -record.put("fields", fields) -recordsArray.put(record) -nonPCIRecords.put("records", recordsArray) - -val options = Skyflow.CollectOptions(tokens = true, additonalFields = nonPCIRecords) -val insertCallback = InsertCallback() //Custom callback - implementation of Skyflow.callback -container.collect(options, insertCallback) -``` -### End to end example of collecting data with Skyflow Elements - -#### [Sample Code](https://github.com/skyflowapi/skyflow-android/blob/main/samples/src/main/java/com/Skyflow/CollectActivity.kt): -```kt -//Initialize skyflow configuration -val config = Skyflow.Configuration(vaultId = VAULT_ID, vaultURL = VAULT_URL, tokenProvider = demoTokenProvider) - -//Initialize skyflow client -val skyflowClient = Skyflow.initialize(config) - -//Create a CollectContainer -val container = skyflowClient.container(type = Skyflow.ContainerType.COLLECT) - -//Initialize and set required options -val options = Skyflow.CollectElementOptions(required = true) - -//Create Skyflow.Styles with individual Skyflow.Style variants -val baseStyle = Skyflow.Style(borderColor = Color.BLUE) -val baseTextStyle = Skyflow.Style(textColor = Color.BLACK) -val completedStyle = Skyflow.Style(textColor = Color.GREEN) -val focusTextStyle = Skyflow.Style(textColor = Color.RED) -val inputStyles = Skyflow.Styles(base = baseStyle, complete = completedStyle) -val labelStyles = Skyflow.Styles(base = baseTextStyle, focus = focusTextStyle) -val errorTextStyles = Skyflow.Styles(base = baseTextStyle) - -//Create a CollectElementInput -val input = Skyflow.CollectElementInput( - table = "cards", - column = "cardNumber", - type = Skyflow.ElementType.CARD_NUMBER - inputStyles = inputStyles, - labelStyles = labelStyles, - errorTextStyles = errorTextStyles, - label = "card number", - placeholder = "card number", -) - -//Create a CollectElementOptions instance -val options = Skyflow.CollectElementOptions(required = true) +### Step 4: Collect data from Elements -//Create a Collect Element from the Collect Container -val skyflowElement = container.create(context = Context,input, options) +Call `collect(callback, options)` on the container. `CollectOptions` accepts optional `additionalFields` (non-PCI data) and `upsert` parameters. -//Can interact with this object as a normal UIView Object and add to View - -// Non-PCI fields data -val nonPCIRecords = JSONObject() -val recordsArray = JSONArray() -val record = JSONObject() -record.put("table", "persons") -val fields = JSONObject() -fields.put("gender", "MALE") -record.put("fields", fields) -recordsArray.put(record) -nonPCIRecords.put("records", recordsArray) - -//Initialize and set required options for insertion -val collectOptions = Skyflow.CollectOptions(tokens = true, additionalFields = nonPCIRecords) - -//Implement a custom Skyflow.Callback to be called on Insertion success/failure -public class InsertCallback: Skyflow.Callback { - override fun onSuccess(responseBody: Any) { - print(responseBody) - } - override fun onFailure(_ error: Error) { - print(error) +```kotlin +val options = CollectOptions( + additionalFields = AdditionalFields(records = listOf( + AdditionalFieldsRecord(tableName = "persons", data = mapOf("gender" to "MALE")) + )) +) +container.collect(object : CollectCallback { + override fun onSuccess(response: CollectResponse) { + response.records.forEach { record -> + if (record.httpCode == 200) Log.d(TAG, "success: ${record.tokens}") + else Log.d(TAG, "error [${record.httpCode}]: ${record.error}") + } } -} - -//Initialize InsertCallback which is an implementation of Skyflow.Callback interface -val insertCallback = InsertCallback() - -//Call collect method on CollectContainer -container.collect(options = collectOptions, callback = insertCallback) - -``` -#### Sample Response : -``` -{ - "records": [ - { - "table": "cards", - "fields": { - "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1" - } - }, - { - "table": "persons", - "fields": { - "gender": "12f670af-6c7d-4837-83fb-30365fbc0b1e", - } + override fun onFailure(error: SkyflowError) { + Log.d(TAG, "failure: ${error.message}") } - ] -} - +}, options) ``` -### End to end example of upsert support with Skyflow Elements - -#### [Sample Code](https://github.com/skyflowapi/skyflow-android/blob/main/samples/src/main/java/com/Skyflow/UpsertFeature.kt): -```kt -val config = Skyflow.Configuration(vaultId = VAULT_ID, vaultURL = VAULT_URL, tokenProvider = demoTokenProvider) -val skyflowClient = Skyflow.initialize(config) -val container = skyflowClient.container(type = Skyflow.ContainerType.COLLECT) -val options = Skyflow.CollectElementOptions(required = true) -val baseStyle = Skyflow.Style(borderColor = Color.BLUE) -val baseTextStyle = Skyflow.Style(textColor = Color.BLACK) -val completedStyle = Skyflow.Style(textColor = Color.GREEN) -val focusTextStyle = Skyflow.Style(textColor = Color.RED) -val inputStyles = Skyflow.Styles(base = baseStyle, complete = completedStyle) -val labelStyles = Skyflow.Styles(base = baseTextStyle, focus = focusTextStyle) -val errorTextStyles = Skyflow.Styles(base = baseTextStyle) - -val cardNumberInput = Skyflow.CollectElementInput( - table = "cards", - column = "card_number", - type = Skyflow.ElementType.CARD_NUMBER - inputStyles = inputStyles, - labelStyles = labelStyles, - errorTextStyles = errorTextStyles, - label = "Card number", - placeholder = "enter your card number", -) - -val nameInput = Skyflow.CollectElementInput( - table = "cards", - column = "full_name", - type = Skyflow.ElementType.CARD_NUMBER - inputStyles = inputStyles, - labelStyles = labelStyles, - errorTextStyles = errorTextStyles, - label = "Full name", - placeholder = "enter your name", -) -val cardNumberElement = container.create(context = Context,cardNumberInput, options) -val nameElement = container.create(context = Context,namerInput, options) - -//Upsert options -val upsertArray = JSONArray() -val upsertColumn = JSONObject() -upsertColumn.put("table", "cards") -upsertColumn.put("column", "card_number") -upsertArray.put(upsertColumn) - -val collectOptions = Skyflow.CollectOptions(tokens = true,upsert = upsertArray) - -public class InsertCallback: Skyflow.Callback { - override fun onSuccess(responseBody: Any) { - print(responseBody) - } - override fun onFailure(_ error: Error) { - print(error) - } -} - -val insertCallback = InsertCallback() -container.collect(options = collectOptions, callback = insertCallback) -``` -#### Sample Response : -``` -{ - "records": [ - { - "table": "cards", - "fields": { - "card_number": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - "name": "f3907186-e7e2-464f-91e5-48e12c2bfsi9" - } - } - ] -} +#### [Sample Code](https://github.com/skyflowapi/skyflow-android/blob/main/samples/src/main/java/com/Skyflow/CollectActivity.kt) -``` ## Using Skyflow Elements to update data @@ -735,8 +597,8 @@ To create a collect Element, construct a `Skyflow.CollectElementInput` object as ```kt val collectElementInput = Skyflow.CollectElementInput( - table: String, // optional, the table this data belongs to - column: String, // optional, the column into which this data should be inserted + tableName: String, // required, the table this data belongs to + column: String, // required, the column into which this data should be inserted type: Skyflow.ElementType, // Skyflow.ElementType enum inputStyles: Skyflow.Styles, // optional styles that should be applied to the form element labelStyles: Skyflow.Styles, // optional styles that will be applied to the label of the collect element @@ -745,15 +607,14 @@ val collectElementInput = Skyflow.CollectElementInput( placeholder: String, // optional placeholder for the form element altText: String, // (DEPRECATED) optional that acts as an initial value for the collect element validations: ValidationSet, // optional set of validations for the input element - skyflowID: String // The skyflow_id of the record to be updated + skyflowId: String // The skyflow_id of the record to be updated ) ``` -The `table` and `column` fields indicate which table and column in the vault the Element corresponds to. +The `tableName` and `column` fields indicate which table and column in the vault the Element corresponds to. **Note:** - Use dot delimited strings to specify columns nested inside JSON fields (e.g. `address.street.line1`) -- `table` and `column` are optional only if the element is being used in invokeConnection() Along with `CollectElementInput`, you can define other options in the `CollectElementOptions` object as described in the [collect section](#step-2-create-a-collect-element). @@ -785,175 +646,34 @@ fun clearFieldsOnSubmit(elements: List) { When the form is ready to submit, call the `collect(options?)` method on the container object. The `options` parameter takes an object of optional parameters as shown below: -- `tokens`: indicates whether tokens for the collected data should be returned or not. Defaults to 'true' -- `additionalFields`: Non-PCI elements data to update or insert into the vault which should be in the records object format. -- `upsert`: To support upsert operations while collecting data from Skyflow elements, pass the table and column marked as unique in the table. - -```kt -// Non-PCI records with skyflowID for update -val nonPCIRecords = JSONObject().apply { - val recordsArray = JSONArray() - val record = JSONObject().apply { - put("table", "persons") - val fields = JSONObject().apply { - put("gender", "MALE") - put("skyflow_id", "") // skyflowID for update - } - put("fields", fields) - } - recordsArray.put(record) - put("records", recordsArray) -} - -// Upsert options -val upsertArray = JSONArray() -val upsertColumn = JSONObject().apply { - put("table", "cards") - put("column", "card_number") -} -upsertArray.put(upsertColumn) - -// Send the non-PCI records as additionalFields of CollectOptions (optional) -// and apply upsert using `upsert` field of CollectOptions (optional) -val options = CollectOptions(tokens = true, additionalFields = nonPCIRecords, upsert = upsertArray) - -// Custom callback - implementation of Skyflow.Callback -val insertCallback = InsertCallback() -container.collect(callback = insertCallback, options = options) -``` - -**Note:** `skyflowID` is required if you want to update the data. If `skyflowID` isn't specified, the `collect(options?)` method creates a new record in the vault. - -### End to end example of updating data with Skyflow Elements - -```kt -// Initialize skyflow configuration -val tokenProvider = DemoTokenProvider() -val config = Configuration( - vaultID = "", - vaultURL = "", - tokenProvider = tokenProvider -) - -// Initialize skyflow client -val skyflowClient = init(config) - -// Create a CollectContainer -val container = skyflowClient.container(ContainerType.COLLECT) - -// Create Skyflow.Styles with individual Skyflow.Style variants -val padding = Padding(8, 8, 8, 8) -val baseStyle = Style(borderColor = Color.BLUE) -val baseTextStyle = Style(textColor = Color.BLACK) -val completeStyle = Style(borderColor = Color.GREEN) -val focusTextStyle = Style(textColor = Color.RED) -val inputStyles = Styles(base = baseStyle, complete = completeStyle) -val labelStyles = Styles(base = baseTextStyle, focus = focusTextStyle) -val errorTextStyles = Styles(base = baseTextStyle) - -// Create a CollectElementInput with skyflowID for update -val input = CollectElementInput( - table = "cards", - column = "card_number", - type = SkyflowElementType.CARD_NUMBER, - inputStyles = inputStyles, - labelStyles = labelStyles, - errorTextStyles = errorTextStyles, - label = "Card Number", - placeholder = "XXXX XXXX XXXX XXXX", - skyflowID = "431eaa6c-5c15-4513-aa15-29f50babe882" // skyflowID for update -) - -// Create an option to require the element -val requiredOption = CollectElementOptions(required = true, enableCopy = true) - -// Create a Collect Element from the Collect Container -val skyflowElement = container.create(context = this, input = input, options = requiredOption) +- `additionalFields`: Non-PCI data to update or insert alongside element values, as an `AdditionalFields` object. +- `upsert`: To support upsert operations, pass a list of `UpsertOptions` specifying the table, update type, and unique columns. -// Can interact with this object as a normal View Object and add to View -val parent = findViewById(R.id.parent) -parent.addView(skyflowElement) - -// Non-PCI records with skyflowID for update -val nonPCIRecords = JSONObject().apply { - val recordsArray = JSONArray() - // Update existing person record - val personRecord = JSONObject().apply { - put("table", "persons") - val fields = JSONObject().apply { - put("gender", "MALE") - put("skyflowID", "77dc3caf-c452-49e1-8625-07219d7567bf") // skyflowID for update - } - put("fields", fields) - } - // Update existing card record with additional fields - val cardRecord = JSONObject().apply { - put("table", "cards") - val fields = JSONObject().apply { - put("first_name", "Joe") - put("skyflowID", "431eaa6c-5c15-4513-aa15-29f50babe882") // same skyflowID as collect element +```kotlin +val options = CollectOptions( + additionalFields = AdditionalFields(records = listOf( + AdditionalFieldsRecord( + tableName = "persons", + data = mapOf("gender" to "MALE"), + skyflowId = "" + ) + )) +) +container.collect(object : CollectCallback { + override fun onSuccess(response: CollectResponse) { + response.records.forEach { record -> + if (record.httpCode == 200) Log.d(TAG, "update success: ${record.tokens}") + else Log.d(TAG, "update error [${record.httpCode}]: ${record.error}") } - put("fields", fields) - } - recordsArray.put(personRecord) - recordsArray.put(cardRecord) - put("records", recordsArray) -} - -// Upsert options -val upsertOptions = JSONArray().apply { - val upsertColumn = JSONObject().apply { - put("table", "cards") - put("column", "card_number") - } - put(upsertColumn) -} - -// Send the Non-PCI records as additionalFields of CollectOptions (optional) -// and apply upsert using optional field `upsert` of CollectOptions -val collectOptions = CollectOptions(tokens = true, additionalFields = nonPCIRecords, upsert = upsertOptions) - -// Implement a custom Skyflow.Callback to call on update success/failure -class InsertCallback : Callback { - override fun onSuccess(responseBody: Any) { - Log.d(TAG, "Update successful: $responseBody") } - - override fun onFailure(exception: Any) { - Log.e(TAG, "Update failed: ${(exception as Exception).message}") + override fun onFailure(error: SkyflowError) { + Log.d(TAG, "update failure: ${error.message}") } -} - -// Initialize custom Skyflow.Callback -val insertCallback = InsertCallback() - -// Call collect method on CollectContainer -container.collect(callback = insertCallback, options = collectOptions) +}, options) ``` -#### Skyflow returns tokens for the record you just updated: -```json -{ - "records": [ - { - "table": "persons", - "fields": { - "gender": "12f670af-6c7d-4837-83fb-30365fbc0b1e", - "skyflow_id": "77dc3caf-c452-49e1-8625-07219d7567bf" - } - }, - { - "table": "cards", - "fields": { - "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882", - "card_number": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - "first_name": "131e70dc-6f76-4319-bdd3-96281e051051" - } - } - ] -} -``` +**Note:** `skyflowId` is required to update an existing record. Without it, a new record is inserted. ### Validations @@ -1046,24 +766,24 @@ val state = { //create skyflow client with loglevel:"DEBUG" val config = Skyflow.Configuration(vaultID = VAULT_ID, vaultURL = VAULT_URL, tokenProvider = demoTokenProvider, options = Skyflow.Options(logLevel = Skyflow.LogLevel.DEBUG)) -val skyflowClient = Skyflow.initialize(config) +val skyflowClient = Skyflow.init(config) 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, ) -val cardNumber = container.create(context = Context, input = cardNumberInput) -val cardHolderName = container.create(context = Context, input = cardHolderNameInput) +val cardNumber = container.create(context = this, input = cardNumberInput) +val cardHolderName = container.create(context = this, input = cardHolderNameInput) //subscribing to CHANGE event, which gets triggered when element changes cardNumber.on(eventName = Skyflow.EventName.CHANGE) { state -> @@ -1126,13 +846,13 @@ Helps to display custom error messages on the Skyflow Elements through the metho //create skyflow client with loglevel:"DEBUG" val config = Skyflow.Configuration(vaultID = VAULT_ID, vaultURL = VAULT_URL, tokenProvider = demoTokenProvider, options = Skyflow.Options(logLevel = Skyflow.LogLevel.DEBUG)) -val skyflowClient = Skyflow.initialize(config) +val skyflowClient = Skyflow.init(config) 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, ) @@ -1165,12 +885,12 @@ val config = Skyflow.Configuration( tokenProvider = demoTokenProvider, options = Skyflow.Options(env = Skyflow.Env.DEV) ) -val skyflowClient = Skyflow.initialize(config) +val skyflowClient = Skyflow.init(config) 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, ) @@ -1225,8 +945,8 @@ Composable Elements use the following schema: ```kotlin val composableElementInput = Skyflow.CollectElementInput( - table: String, // optional, the table this data belongs to - column: String, // optional, the column into which this data should be inserted + tableName: String, // required, the table this data belongs to + column: String, // required, the column into which this data should be inserted inputStyles: Skyflow.Styles, // optional styles that should be applied to the form element labelStyles: Skyflow.Styles, // optional styles that will be applied to the label of the collect element errorTextStyles: Skyflow.Styles, // optional styles that will be applied to the errorText of the collect element @@ -1237,7 +957,7 @@ val composableElementInput = Skyflow.CollectElementInput( type: Skyflow.ElementType, // Skyflow.ElementType enum ) ``` -The `table` and `column` fields indicate which table and column in the vault the Element correspond to. +The `tableName` and `column` fields indicate which table and column in the vault the Element corresponds to. **Note**: - Use dot delimited strings to specify columns nested inside JSON fields (e.g. `address.street.line1`) @@ -1370,10 +1090,10 @@ Collect Element Options examples for INPUT_FIELD Example 1 ```kotlin Skyflow.CollectElementOptions( - required: true, - enableCardIcon: true, - format: "+91 XXXX-XX-XXXX", - translation: hashmapOf('X' to "[0-9]") + required = true, + enableCardIcon = true, + format = "+91 XXXX-XX-XXXX", + translation = hashmapOf('X' to "[0-9]") ) ``` User input: "1234121234" @@ -1383,10 +1103,10 @@ Value displayed in INPUT_FIELD: "+91 1234-12-1234" Example 2 ```kotlin Skyflow.CollectElementOptions( - required: true, - enableCardIcon: true, - format: "AY XX-XXX-XXXX", - translation: hashmapOf('X' to "[0-9]", 'Y' to "[A-Z]") + required = true, + enableCardIcon = true, + format = "AY XX-XXX-XXXX", + translation = hashmapOf('X' to "[0-9]", 'Y' to "[A-Z]") ) ``` User input: "B1234121234" @@ -1397,8 +1117,8 @@ Once the `Skyflow.CollectElementInput` and `Skyflow.CollectElementOptions` objec ```kotlin val composableElementInput = Skyflow.CollectElementInput( - table: String, // the table this data belongs to - column: String, // the column into which this data should be inserted + tableName: String, // required, the table this data belongs to + column: String, // required, the column into which this data should be inserted inputStyles: Skyflow.Styles, // optional styles that should be applied to the form element labelStyles: Skyflow.Styles, // optional styles that will be applied to the label of the collect element errorTextStyles: Skyflow.Styles, // optional styles that will be applied to the errorText of the collect element @@ -1410,12 +1130,12 @@ val composableElementInput = Skyflow.CollectElementInput( ) val collectElementOptions = Skyflow.CollectElementOptions( - required: false, // indicates whether the field is marked as required. Defaults to 'false', - enableCardIcon: true, // indicates whether card icon should be enabled (only for CARD_NUMBER inputs) - format: "mm/yy" // Format for the element + required = false, // indicates whether the field is marked as required. Defaults to 'false' + enableCardIcon = true, // indicates whether card icon should be enabled (only for CARD_NUMBER inputs) + format = "mm/yy" // Format for the element ) -val element = container.create(context = Context, input: composableElementInput, options: collectElementOptions) +val element = container.create(context = this, input: composableElementInput, options: collectElementOptions) ``` ### Step 3: Mount Elements to the Screen @@ -1442,190 +1162,29 @@ fun clearFieldsOnSubmit(elements: List) { ``` ### Step 4: Collect data from elements -When the form is ready to be submitted, call the `collect(options: Skyflow.CollectOptions? = CollectOptions(), callback: Skyflow.Callback)` method on the container object. The options parameter takes `Skyflow.CollectOptions` object. - -`Skyflow.CollectOptions` takes three optional fields -- `tokens`: indicates whether tokens for the collected data should be returned or not. Defaults to 'true' -- `additionalFields`: Non-PCI elements data to be inserted into the vault which should be in the `records` object format as described in the above [Inserting data into vault](#Inserting-data-into-the-vault) section. -- `upsert`: To support upsert operations, the table containing the data and a column marked as unique in that table. - -```kotlin -// NON-PCI fields object creation -val nonPCIRecords = JSONObject() -val recordsArray = JSONArray() - -val record = JSONObject() -record.put("table", "persons") - -val fields = JSONObject() -fields.put("gender", "MALE") - -record.put("fields", fields) -recordsArray.put(record) - -nonPCIRecords.put("records", recordsArray) - -//Upsert options -val upsertArray = JSONArray() - -val upsertColumn = JSONObject() -upsertColumn.put("table", "cards") -upsertColumn.put("column", "card_number") - -upsertArray.put(upsertColumn) +When the form is ready to be submitted, call the `collect(callback: CollectCallback, options: CollectOptions? = null)` method on the container object. The options parameter takes a `CollectOptions` object. -val options = Skyflow.CollectOptions(tokens = true, additonalFields = nonPCIRecords, upsert = upsertArray) -val insertCallback = InsertCallback() //Custom callback - implementation of Skyflow.callback -container.collect(options, insertCallback) -``` -#### End to end example of collecting data with Composable Elements +`Skyflow.CollectOptions` takes two optional fields +- `additionalFields`: Non-PCI data to be inserted alongside element values. See [Additional fields](#additional-fields-non-pci-data). +- `upsert`: To support upsert operations, the table and a unique column. See [Upsert support](#upsert-support). -##### [Sample Code](https://github.com/skyflowapi/skyflow-android/blob/main/samples/src/main/java/com/Skyflow/ComposableActivity.kt): ```kotlin -//Initialize skyflow configuration -val config = Skyflow.Configuration(vaultID = VAULT_ID, vaultURL = VAULT_URL, tokenProvider = demoTokenProvider) - -//Initialize skyflow client -val skyflowClient = Skyflow.init(config) - -//Create a ComposableContainer -val container = skyflowClient.container( - type = Skyflow.ContainerType.COMPOSABLE, - options = ContainerOptions(layout = arrayOf(1, 2)) -) - -//Initialize and set required options -val options = Skyflow.CollectElementOptions(required = true) - -//Create Skyflow.Styles with individual Skyflow.Style variants -val baseCardStyle = Skyflow.Style(borderColor = Color.TRANSPARENT) -val baseDateStyle = Skyflow.Style(borderColor = Color.TRANSPARENT, width = 300) -val baseCvvStyle = Skyflow.Style(borderColor = Color.TRANSPARENT, width = 200) -val completedStyle = Skyflow.Style(textColor = Color.TRANSPARENT) -val baseTextStyle = Skyflow.Style(textColor = Color.BLACK) -val focusTextStyle = Skyflow.Style(textColor = Color.RED) -val cardStyles = Skyflow.Styles(base = baseCardStyle, complete = completedStyle) -val dateStyles = Skyflow.Styles(base = baseDateStyle, complete = completedStyle) -val cvvStyles = Skyflow.Styles(base = baseCvvStyle, complete = completedStyle) -val labelStyles = Skyflow.Styles(base = baseTextStyle, focus = focusTextStyle) -val errorTextStyles = Skyflow.Styles(base = baseTextStyle) - -//Create a CollectElementInput -val cardNumber = Skyflow.CollectElementInput( - table = "cards", - column = "cardNumber", - type = Skyflow.ElementType.CARD_NUMBER - inputStyles = cardStyles, - labelStyles = labelStyles, - errorTextStyles = errorTextStyles, - label = "card number", - placeholder = "card number", -) - -val expDate = Skyflow.CollectElementInput( - table = "cards", - column = "expiryDate", - type = Skyflow.ElementType.EXPIRATION_DATE - inputStyles = dateStyles, - labelStyles = labelStyles, - errorTextStyles = errorTextStyles, - label = "Expiry Date", - placeholder = "mm/yy", -) - -val cvv = Skyflow.CollectElementInput( - table = "cards", - column = "cvv", - type = Skyflow.ElementType.CVV - inputStyles = cvvStyles, - labelStyles = labelStyles, - errorTextStyles = errorTextStyles, - label = "CVV", - placeholder = "***", -) - -//Create a CollectElementOptions instance -val options = Skyflow.CollectElementOptions(required = true) - -//Create a Composable Element from the Composable Container -val cardNumberElement = container.create(context = Context, cardNumber, options) -val expDateElement = container.create(context = Context, expDate, options) -val cvvElement = container.create(context = Context, cvv, options) - -//Fetch composable layout and add to main view -try { - val composableLayout = container.getComposableLayout() - parent.addView(composableLayout) -} catch (error: Exception) { - println(error) -} - -// Non-PCI fields data -val nonPCIRecords = JSONObject() -val recordsArray = JSONArray() - -val record = JSONObject() -record.put("table", "persons") - -val fields = JSONObject() -fields.put("gender", "MALE") - -record.put("fields", fields) -recordsArray.put(record) - -nonPCIRecords.put("records", recordsArray) - -//Upsert options -val upsertArray = JSONArray() - -val upsertColumn = JSONObject() -upsertColumn.put("table", "cards") -upsertColumn.put("column", "card_number") - -upsertArray.put(upsertColumn) - -//Initialize and set required options for insertion -val collectOptions = Skyflow.CollectOptions(tokens = true, additionalFields = nonPCIRecords, upsert = upsertArray) - -//Implement a custom Skyflow.Callback to be called on Insertion success/failure -class InsertCallback: Skyflow.Callback { - override fun onSuccess(responseBody: Any) { - print(responseBody) - } - override fun onFailure(_ error: Error) { - print(error) - } -} - -//Initialize InsertCallback which is an implementation of Skyflow.Callback interface -val insertCallback = InsertCallback() - -//Call collect method on CollectContainer -container.collect(options = collectOptions, callback = insertCallback) -``` -##### Sample Response : -```json -{ - "records": [ - { - "table": "cards", - "fields": { - "cardNumber": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - "expiryDate": "d0369871-91e5-466f-e7e2-48e12c2bcbc2", - "cvv": "c7093186-466f-e7e2-91e5-48e12c2bcbc3", - } - }, - { - "table": "persons", - "fields": { - "gender": "12f670af-6c7d-4837-83fb-30365fbc0b1e", - } +val options = CollectOptions( + additionalFields = AdditionalFields(records = listOf( + AdditionalFieldsRecord(tableName = "persons", data = mapOf("gender" to "MALE")) + )) +) +composableContainer.collect(object : CollectCallback { + override fun onSuccess(response: CollectResponse) { + response.records.forEach { record -> + if (record.httpCode == 200) Log.d(TAG, "success: ${record.tokens}") + else Log.d(TAG, "error [${record.httpCode}]: ${record.error}") + } } - ] -} + override fun onFailure(error: SkyflowError) { Log.d(TAG, "failure: ${error.message}") } +}, options) ``` -[For information on validations, see validations.](#validations) ## Using Skyflow Composable Elements to update data @@ -1653,8 +1212,8 @@ Composable Elements use the following schema: ```kt val composableElementInput = Skyflow.CollectElementInput( - table: String, // optional, the table this data belongs to - column: String, // optional, the column into which this data should be updated + tableName: String, // required, the table this data belongs to + column: String, // required, the column into which this data should be updated type: Skyflow.ElementType, // Skyflow.ElementType enum inputStyles: Skyflow.Styles, // optional styles that should be applied to the form element labelStyles: Skyflow.Styles, // optional styles that will be applied to the label of the collect element @@ -1663,11 +1222,11 @@ val composableElementInput = Skyflow.CollectElementInput( placeholder: String, // optional placeholder for the form element altText: String, // (DEPRECATED) optional that acts as an initial value for the collect element validations: ValidationSet, // optional set of validations for the input element - skyflowID: String // The skyflow_id of the record to be updated + skyflowId: String // The skyflow_id of the record to be updated ) ``` -The `table` and `column` fields indicate which table and column in the vault the Element corresponds to. +The `tableName` and `column` fields indicate which table and column in the vault the Element corresponds to. **Note:** - Use dot delimited strings to specify columns nested inside JSON fields (e.g. `address.street.line1`) @@ -1715,271 +1274,54 @@ fun clearFieldsOnSubmit(elements: List) { ### Step 4: Update data from Elements -When you submit the form, call the `collect(options: Skyflow.CollectOptions? = null, callback: Skyflow.Callback)` method on the container object. +When you submit the form, call the `collect(callback: CollectCallback, options: CollectOptions? = null)` method on the container object. -The options parameter takes a `Skyflow.CollectOptions` object as shown below: +The options parameter takes a `CollectOptions` object with the following optional fields: -- `tokens`: Whether or not tokens for the collected data are returned. Defaults to 'true' -- `additionalFields`: Non-PCI elements data to insert into the vault, specified in the records object format. -- `upsert`: To support upsert operations, the table containing the data and a column marked as unique in that table. +- `additionalFields`: Non-PCI data to insert alongside element values, as an `AdditionalFields` object. +- `upsert`: To support upsert operations, pass a list of `UpsertOptions` specifying the table, update type, and unique columns. -```kt -// Non-PCI records with skyflowID for update -val nonPCIRecords = JSONObject().apply { - val recordsArray = JSONArray() - val record = JSONObject().apply { - put("table", "persons") - val fields = JSONObject().apply { - put("gender", "MALE") - put("skyflowID", "") // skyflowID for update +```kotlin +val options = CollectOptions( + additionalFields = AdditionalFields(records = listOf( + AdditionalFieldsRecord( + tableName = "persons", + data = mapOf("gender" to "MALE"), + skyflowId = "" + ) + )) +) +composableContainer.collect(object : CollectCallback { + override fun onSuccess(response: CollectResponse) { + response.records.forEach { record -> + if (record.httpCode == 200) Log.d(TAG, "update success: ${record.tokens}") + else Log.d(TAG, "update error [${record.httpCode}]: ${record.error}") } - put("fields", fields) } - recordsArray.put(record) - put("records", recordsArray) -} + override fun onFailure(error: SkyflowError) { Log.d(TAG, "update failure: ${error.message}") } +}, options) +``` -// Upsert options -val upsertOptions = JSONArray().apply { - val upsertColumn = JSONObject().apply { - put("table", "cards") - put("column", "card_number") - } - put(upsertColumn) -} -// Send the non-PCI records as additionalFields of CollectOptions (optional) -// and apply upsert using `upsert` field of CollectOptions (optional) -val options = CollectOptions(tokens = true, additionalFields = nonPCIRecords, upsert = upsertOptions) +**Note:** `skyflowId` is required to update an existing record. Without it, a new record is inserted. -// Custom callback - implementation of Skyflow.Callback -val insertCallback = InsertCallback() -container.collect(callback = insertCallback, options = options) -``` +## Event Listeners on Composable Elements +You can communicate with Skyflow Elements by listening to element events: -### End to end example of updating data with Composable Elements +```kotlin +element.on(eventName: Skyflow.EventName) { state -> + // handle function +} +``` -```kt -// Initialize skyflow configuration -val config = Configuration( - vaultID = "", - vaultURL = "", - tokenProvider = demoTokenProvider -) +The SDK supports four events: -// Initialize skyflow client -val skyflowClient = init(config) +- `CHANGE`: Triggered when the Element's value changes. +- `READY`: Triggered when the Element is fully rendered. +- `FOCUS`: Triggered when the Element gains focus. +- `BLUR`: Triggered when the Element loses focus. -// Create container options with layout -val containerOptions = ContainerOptions( - layout = arrayOf(1, 2), - styles = Styles(base = Style(borderColor = Color.GRAY)), - errorTextStyles = Styles(base = Style(textColor = Color.RED)) -) - -// Create a Composable Container -val container = skyflowClient.container( - type = ContainerType.COMPOSABLE, - options = containerOptions -) - -// Create Skyflow.Styles with individual Skyflow.Style variants -val padding = Padding(8, 8, 8, 8) -val baseStyle = Style(borderColor = Color.BLUE) -val baseTextStyle = Style(textColor = Color.BLACK) -val completeStyle = Style(borderColor = Color.GREEN) -val focusTextStyle = Style(textColor = Color.RED) -val inputStyles = Styles(base = baseStyle, complete = completeStyle) -val labelStyles = Styles(base = baseTextStyle, focus = focusTextStyle) -val errorTextStyles = Styles(base = baseTextStyle) - -// Create Composable Elements with skyflowID for update -val cardHolderNameElementInput = CollectElementInput( - table = "cards", - column = "cardholder_name", - type = SkyflowElementType.CARDHOLDER_NAME, - inputStyles = inputStyles, - labelStyles = labelStyles, - errorTextStyles = errorTextStyles, - label = "Cardholder Name", - placeholder = "John Doe", - skyflowID = "431eaa6c-5c15-4513-aa15-29f50babe882" // skyflowID for update -) - -// Create an option to require the element -val requiredOption = CollectElementOptions(required = true) - -// Create a Composable Element from the Composable Container -val cardHolderNameElement = container.create( - context = this, - input = cardHolderNameElementInput, - options = requiredOption -) - -val cardNumberElementInput = CollectElementInput( - table = "cards", - column = "card_number", - type = SkyflowElementType.CARD_NUMBER, - inputStyles = inputStyles, - labelStyles = labelStyles, - errorTextStyles = errorTextStyles, - label = "Card Number", - placeholder = "XXXX XXXX XXXX XXXX", - skyflowID = "431eaa6c-5c15-4513-aa15-29f50babe882" // same skyflowID - will be merged in single update -) - -val cardNumberElement = container.create( - context = this, - input = cardNumberElementInput, - options = requiredOption -) - -val cvvElementInput = CollectElementInput( - table = "cards", - column = "cvv", - type = SkyflowElementType.CVV, - inputStyles = inputStyles, - labelStyles = labelStyles, - errorTextStyles = errorTextStyles, - label = "CVV", - placeholder = "CVV", - skyflowID = "431eaa6c-5c15-4513-aa15-29f50babe882" // same skyflowID - will be merged in single update -) - -val cvvElement = container.create( - context = this, - input = cvvElementInput, - options = requiredOption -) - -// Add composable layout to screen -val parent = findViewById(R.id.parent) -try { - val composableLayout = container.getComposableLayout() - parent.addView(composableLayout) -} catch (error: Exception) { - println(error) -} - -// Non-PCI records with skyflowID for update -val nonPCIRecords = JSONObject().apply { - val recordsArray = JSONArray() - val record = JSONObject().apply { - put("table", "persons") - val fields = JSONObject().apply { - put("gender", "MALE") - put("skyflowID", "77dc3caf-c452-49e1-8625-07219d7567bf") // skyflowID for update - } - put("fields", fields) - } - recordsArray.put(record) - put("records", recordsArray) -} - -// Upsert options -val upsertOptions = JSONArray().apply { - val upsertColumn = JSONObject().apply { - put("table", "cards") - put("column", "card_number") - } - put(upsertColumn) -} - -// Send the Non-PCI records as additionalFields of CollectOptions (optional) -// and apply upsert using optional field `upsert` of CollectOptions -val collectOptions = CollectOptions( - tokens = true, - additionalFields = nonPCIRecords, - upsert = upsertOptions -) - -// Implement a custom Skyflow.Callback to call on update success/failure -class InsertCallback : Callback { - override fun onSuccess(responseBody: Any) { - Log.d(TAG, "Update successful: $responseBody") - } - - override fun onFailure(exception: Any) { - Log.e(TAG, "Update failed: ${(exception as Exception).message}") - } -} - -// Initialize custom Skyflow.Callback -val insertCallback = InsertCallback() - -// Call collect method on CollectContainer -container.collect(callback = insertCallback, options = collectOptions) -``` - -### Sample Success Response: - -```json -{ - "records": [ - { - "table": "persons", - "fields": { - "gender": "12f670af-6c7d-4837-83fb-30365fbc0b1e", - "skyflow_id": "77dc3caf-c452-49e1-8625-07219d7567bf" - } - }, - { - "table": "cards", - "fields": { - "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882", - "card_number": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - "cardholder_name": "131e70dc-6f76-4319-bdd3-96281e051051", - "cvv": "098834fe-de99-4fc8-abdf-88c18a28a2cf" - } - } - ] -} -``` - -### Sample Partial Error Response: - -```json -{ - "records": [ - { - "table": "cards", - "fields": { - "skyflow_id": "431eaa6c-5c15-4513-aa15-29f50babe882", - "card_number": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", - "cardholder_name": "131e70dc-6f76-4319-bdd3-96281e051051", - "cvv": "098834fe-de99-4fc8-abdf-88c18a28a2cf" - } - } - ], - "errors": [ - { - "error": { - "code": 400, - "description": "Update failed. skyflow_ids [77dc3caf-c452-49e1-8625-07219d7567bf] are invalid. Specify valid Skyflow IDs. - request-id: cb397-8521-42c2-870c-92dbeec", - "type": 400 - } - } - ] -} -``` - -## Event Listeners on Composable Elements -You can communicate with Skyflow Elements by listening to element events: - -```kotlin -element.on(eventName: Skyflow.EventName) { state -> - // handle function -} -``` - -The SDK supports four events: - -- `CHANGE`: Triggered when the Element's value changes. -- `READY`: Triggered when the Element is fully rendered. -- `FOCUS`: Triggered when the Element gains focus. -- `BLUR`: Triggered when the Element loses focus. - -The handler `(state: JSONObject) -> Unit` is a callback function you provide, that will be called when the event is fired with the state object as shown below. +The handler `(state: JSONObject) -> Unit` is a callback function you provide, that will be called when the event is fired with the state object as shown below. ```kotlin val state = { @@ -2003,7 +1345,7 @@ val config = Skyflow.Configuration( vaultID = VAULT_ID, vaultURL = VAULT_URL, tokenProvider = demoTokenProvider, - options = Skyflow.Options(logLevel: Skyflow.LogLevel.DEBUG) + options = Skyflow.Options(logLevel = Skyflow.LogLevel.DEBUG) ) val skyflowClient = Skyflow.init(config) @@ -2019,19 +1361,19 @@ 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, ) -val cardNumber = container.create(context = Context, input = cardNumberInput) -val cardHolderName = container.create(context = Context, input = cardHolderNameInput) +val cardNumber = container.create(context = this, input = cardNumberInput) +val cardHolderName = container.create(context = this, input = cardHolderNameInput) try { val composableLayout = container.getComposableLayout() @@ -2096,7 +1438,7 @@ You can update composable element properties with the `update` interface. The `update` interface takes the below object: ```kotlin val updateElement = Skyflow.CollectElementInput( - table: String, // optional the table this data belongs to + tableName: String, // optional the table this data belongs to column: String, // optional the column into which this data should be inserted inputStyles: Skyflow.Styles, // optional styles that should be applied to the form element labelStyles: Skyflow.Styles, // optional styles that will be applied to the label of the collect element @@ -2122,7 +1464,7 @@ val config = Skyflow.Configuration( vaultID = VAULT_ID, vaultURL = VAULT_URL, tokenProvider = demoTokenProvider, - options = Skyflow.Options(logLevel: Skyflow.LogLevel.DEBUG) + options = Skyflow.Options(logLevel = Skyflow.LogLevel.DEBUG) ) val skyflowClient = Skyflow.init(config) @@ -2138,19 +1480,19 @@ 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, ) -val cardNumber = container.create(context = Context, input = cardNumberInput) -val cardHolderName = container.create(context = Context, input = cardHolderNameInput) +val cardNumber = container.create(context = this, input = cardNumberInput) +val cardHolderName = container.create(context = this, input = cardHolderNameInput) try { val composableLayout = container.getComposableLayout() @@ -2161,7 +1503,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)) )) @@ -2189,7 +1531,7 @@ val config = Skyflow.Configuration( vaultID = VAULT_ID, vaultURL = VAULT_URL, tokenProvider = demoTokenProvider, - options = Skyflow.Options(logLevel: Skyflow.LogLevel.DEBUG) + options = Skyflow.Options(logLevel = Skyflow.LogLevel.DEBUG) ) val skyflowClient = Skyflow.init(config) @@ -2205,12 +1547,12 @@ 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 cardNumber = container.create(context = Context, input = cardNumberInput) +val cardNumber = container.create(context = this, input = cardNumberInput) try { val composableLayout = container.getComposableLayout() @@ -2228,275 +1570,11 @@ container.on(EventName.SUBMIT) { --- # Securely revealing data client-side -- [**Retrieving data from the vault**](#retrieving-data-from-the-vault) - [**Using Skyflow Elements to reveal data**](#using-skyflow-elements-to-reveal-data) - [**UI Error for Reveal Elements**](#ui-error-for-reveal-elements) - [**Set token for Reveal Elements**](#set-token-for-reveal-elements) - [**Set and clear altText for Reveal Elements**](#set-and-clear-alttext-for-reveal-elements) -## Retrieving data from the vault -For non-PCI use-cases, retrieving data from the vault and revealing it in the mobile can be done either using the SkyflowID's or tokens as described below - -- ### Using tokens - To retrieve record data using tokens, use the `detokenize(records)` method. The `records` parameter takes a JSON object that contains tokens for record values to fetch: - - ```json5 - { - "records":[ - { - "token": "string", // token for the record to be fetched - "redaction": Skyflow.RedactionType // Optional. Redaction to apply for retrieved data. E.g. RedactionType.MASKED - } - ] - } - ``` - - Note: `redaction` defaults to [`RedactionType.PLAIN_TEXT`](#redaction-types). - - The following example code makes a detokenize call to reveal the masked value of a token: - ```kt - val getCallback = GetCallback() //Custom callback - implementation of Skyflow.Callback - - val records = JSONObject() - val recordsArray = JSONArray() - val recordObj = JSONObject() - recordObj.put("token", "45012507-f72b-4f5c-9bf9-86b133bae719") - recordObj.put("redaction", RedactionType.MASKED) - recordsArray.put(recordObj) - records.put("records", recordsArray) - - skyflowClient.detokenize(records = records, callback = getCallback) - ``` - The sample response: - ```json - { - "records": [ - { - "token": "131e70dc-6f76-4319-bdd3-96281e051051", - "value": "j***oe" - } - ] - } - ``` - -- ### Using Skyflow ID's or Unique Column Values - For retrieving data from the vault, use the `get(records: JSONObject, options: GetOptions? = GetOptions(), callback: Skyflow.Callback)` method. - - The `records` parameter takes a JSON object that contains an array of the records to fetch. Each object inside array should contain: - - - Either an array of Skyflow IDs to fetch - - Or a column name and an array of column values - - The second parameter, `options`, is a `GetOptions` object that retrieves tokens of Skyflow IDs. - - Notes: - - You can use either Skyflow IDs or unique values to retrieve records. You can't use both at the same time. - - GetOptions parameter is applicable only for retrieving tokens using Skyflow ID. - - You can't pass GetOptions along with the redaction type. - - `tokens` defaults to false. - - ```json5 - { - "records":[ - { - "ids": JSONArray(), // Array of SkyflowID's of the records to be fetched - "table": String, // name of table holding the above skyflow_id's - "redaction": Skyflow.RedactionType // redaction to be applied to retrieved data - }, - { - "table": String, // name of table from where records are to be fetched - "redaction": Skyflow.RedactionType, // redaction to be applied to retrieved data - "columnName": String, // a unique column name - "colunmnValues": JSONArray() // Array of Column Values of the records to be fetched - } - ] - } - ``` - - An example of get call to fetch records: - ```kotlin - val getCallback = GetCallback() //Custom callback - implementation of Skyflow.Callback - - val recordsArray = JSONArray() - - val record = JSONObject() - val skyflowIDs = JSONArray() - skyflowIDs.put("f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9") - skyflowIDs.put("da26de53-95d5-4bdb-99db-8d8c66a35ff9") - - record.put("ids", skyflowIDs) - record.put("table", "cards") - record.put("redaction", RedactionType.PLAIN_TEXT) - - val record1 = JSONObject() - val recordSkyflowIDs = JSONArray() - recordSkyflowIDs.put("invalid skyflow id") // invalid skyflow ID - - record1.put("ids", recordSkyflowIDs) - record1.put("table", "cards") - record1.put("redaction", RedactionType.PLAIN_TEXT) - - val record2 = JSONObject() - val columnValues = JSONArray() - columnValues.put("john.doe@gmail.com") - columnValues.put("jane.doe@gmail.com") - - record2.put("table", "customers") - record2.put("redaction", RedactionType.PLAIN_TEXT) - record2.put("columnName", "email") - record2.put("columnValues", columnValues) - - val record3 = JSONObject() - val columnValues1 = JSONArray() - columnValues1.put("invalid column value") // invalid column value - - record3.put("table", "customers") - record3.put("redaction", RedactionType.PLAIN_TEXT) - record3.put("columnName", "email") - record3.put("columnValues", columnValues1) - - recordsArray.put(record) - recordsArray.put(record1) - recordsArray.put(record2) - recordsArray.put(record3) - - val records = JSONObject() - records.put("records", recordsArray) - - skyflowClient.getById(records = records, GetOptions(), callback = getCallback) - ``` - - The sample response: - ```json - { - "records": [ - { - "fields": { - "card_number": "4111111111111111", - "expiry_date": "11/35", - "fullname": "myname", - "id": "f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9" - }, - "table": "cards" - }, - { - "fields": { - "card_number": "4111111111111111", - "expiry_date": "10/23", - "fullname": "sam", - "id": "da26de53-95d5-4bdb-99db-8d8c66a35ff9" - }, - "table": "cards" - }, - { - "fields": { - "card_number": "4111111111111111", - "email": "john@doe@gmail.com", - "name": "john", - "id": "f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9" - }, - "table": "customers" - }, - { - "fields": { - "card_number": "4111111111111111", - "email": "jane@doe@gmail.com", - "name": "jane", - "id": "f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9" - }, - "table": "customers" - } - ], - "errors": [ - { - "error": { - "code": "404", - "description": "No Records Found" - }, - "ids": ["invalid skyflow id"] - }, - { - "error": { - "code": "404", - "description": "No Records Found" - }, - "columnName": "customers", - "columnValues": ["invalid column value"] - } - ] - } - ``` - - An example of get call to fetch tokens: - ```kotlin - val getCallback = GetCallback() //Custom callback - implementation of Skyflow.Callback - - val recordsArray = JSONArray() - - val validRecord = JSONObject() - val validSkyflowIDs = JSONArray() - validSkyflowIDs.put("f8d8a622-b557-4c6b-a12c-c5ebe0b0bfd9") - validSkyflowIDs.put("da26de53-95d5-4bdb-99db-8d8c66a35ff9") - - validRecord.put("ids", validSkyflowIDs) - validRecord.put("table", "cards") - - val invalidRecord = JSONObject() - val invalidRecordSkyflowIDs = JSONArray() - invalidRecordSkyflowIDs.put("invalid skyflow id") // invalid skyflow ID - - invalidRecord.put("ids", invalidRecordSkyflowIDs) - invalidRecord.put("table", "cards") - - recordsArray.put(validRecord) - recordsArray.put(invalidRecord) - - val records = JSONObject() - records.put("records", recordsArray) - - skyflowClient.getById(records = records, GetOptions(true), callback = getCallback) - ``` - - The sample Response: - ```json - { - "records": [ - { - "fields": { - "card_number": "9802-3257-3113-0294", - "expiry_date": "45012507-f72b-4f5c-9bf9-86b133bae719", - "fullname": "131e2507-f72b-4f5c-9bf9-86b133bae719", - }, - "table": "cards" - }, - { - "fields": { - "card_number": "0294-3213-3157-9802", - "expiry_date": "131e2507-f72b-4f5c-9bf9-86b133bae719", - "fullname": "45012507-f72b-4f5c-9bf9-86b133bae719", - }, - "table": "cards" - } - ], - "errors": [ - { - "error": { - "code": "404", - "description": "No Records Found" - }, - "ids": ["invalid skyflow id"] - } - ] - } - ``` -### Redaction types - There are four enum values in Skyflow.RedactionType: - - `PLAIN_TEXT` - - `MASKED` - - `REDACTED` - - `DEFAULT` - - ## Using Skyflow Elements to reveal data Skyflow Elements can be used to securely reveal data in an application without exposing your front end to the sensitive data. This is great for use-cases like card issuance where you may want to reveal the card number to a user without increasing your PCI compliance scope. ### Step 1: Create a container @@ -2506,23 +1584,21 @@ val container = skyflowClient.container(type = Skyflow.ContainerType.REVEAL) ``` ### Step 2: Create a reveal Element -Next, define a Skyflow Element to reveal data as shown below: -```kt -val revealElementInput = Skyflow.RevealElementInput( - token = "string", - redaction = Skyflow.RedactionType, // optional. Redaction to apply for retrieved data. E.g. RedactionType.MASKED - inputStyles = Skyflow.Styles(), //optional, styles to be applied to the element - labelStyles = Skyflow.Styles(), //optional, styles to be applied to the label of the reveal element - errorTextStyles = Skyflow.Styles(), //optional styles that will be applied to the errorText of the reveal element - label = "cardNumber" //optional, label for the element, - altText = "XXXX XXXX XXXX XXXX" //optional, string that is shown before reveal, will show token if altText is not provided - ) +Next, define a `RevealElementInput` for each element to reveal: +```kotlin +val revealElementInput = RevealElementInput( + token = "", // token of the data to reveal + inputStyles = Skyflow.Styles(), // optional styles for the element + labelStyles = Skyflow.Styles(), // optional styles for the label + errorTextStyles = Skyflow.Styles(), // optional styles for the error text + label = "Card Number", // optional label + altText = "•••• •••• •••• ••••" // optional placeholder shown before reveal +) ``` -`Notes`: -- `token` is optional only if it is being used in invokeConnection() -- `redaction` defaults to [`RedactionType.PLAIN_TEXT`](#redaction-types) + +**Note:** Redaction is not set on individual reveal elements. To apply a redaction to a token group, use `tokenGroupRedactions` on `RevealOptions` passed to `reveal()`. See [Reveal with typed callbacks](#reveal-with-typed-callbacks). The `inputStyles` parameter accepts a styles object as described in the [previous section](#step-2-create-a-collect-element) for collecting data but the only state available for a reveal element is the base state. @@ -2569,10 +1645,9 @@ Reveal Element Options examples: Example 1: ```kotlin -let element = container.create(input: revealElementInput) -Skyflow.RevealElementOptions( - format: "(XXX) XXX-XXXX", - translation: hashmapOf('X' to "[0-9]") +val options = Skyflow.RevealElementOptions( + format = "(XXX) XXX-XXXX", + translation = hashmapOf('X' to "[0-9]") ) ``` Value from vault: "1234121234" @@ -2581,9 +1656,9 @@ Value displayed in element: "(123) 412-1234" Example 2: ```kotlin -Skyflow.RevealElementOptions( - format: "XXXX-XXXXXX-XXXXX", - translation: hashmapOf('X' to "[0-9]") +val options = Skyflow.RevealElementOptions( + format = "XXXX-XXXXXX-XXXXX", + translation = hashmapOf('X' to "[0-9]") ) ``` Value from vault: "374200000000004" @@ -2593,9 +1668,13 @@ Value displayed in element: "3742-000000-00004" Once you've defined a `Skyflow.RevealElementInput` object and `Skyflow.RevealElementOptions`, you can use the `create()` method of the container to create the Element as shown below: ```kotlin -let element = container.create(input: revealElementInput, options: Skyflow.RevealElementOptions(format: "XXXX-XXXXXX-XXXXX", -translation: hashmapOf('X' to "[0-9]") -)) +val element = container.create( + input = revealElementInput, + options = Skyflow.RevealElementOptions( + format = "XXXX-XXXXXX-XXXXX", + translation = hashmapOf('X' to "[0-9]") + ) +) ``` ### Step 3: Mount Elements to the Screen @@ -2603,12 +1682,38 @@ translation: hashmapOf('X' to "[0-9]") Elements used for revealing data are mounted to the screen the same way as Elements used for collecting data. Refer to Step 3 of the [section above](#step-3-mount-elements-to-the-screen). ### Step 4: Reveal data -When the sensitive data is ready to be retrieved and revealed, call the `reveal()` method on the container as shown below: -```kt -val revealCallback = RevealCallback() //Custom callback - implementation of Skyflow.Callback -container.reveal(callback = revealCallback) +When the sensitive data is ready to be retrieved and revealed, call the `reveal()` method on the container with a typed `RevealCallback`: + +```kotlin +revealContainer.reveal(object : RevealCallback { + override fun onSuccess(response: RevealResponse) { + response.records.forEach { record -> + if (record.httpCode == 200) { + Log.d(TAG, "revealed: token=${record.token}, group=${record.tokenGroupName}") + } else { + Log.e(TAG, "partial error [${record.httpCode}]: ${record.error}") + } + } + } + override fun onFailure(error: SkyflowError) { + Log.e(TAG, "reveal failed: ${error.message}") + } +}) ``` + +To apply redaction per token group, pass `RevealOptions`: + +```kotlin +val options = RevealOptions( + tokenGroupRedactions = listOf( + TokenGroupRedaction(tokenGroupName = "", redaction = "") + ) +) +revealContainer.reveal(object : RevealCallback { ... }, options) +``` + + ### UI Error for Reveal Elements Helps to display custom error messages on the Skyflow Elements through the methods `setError` and `resetError` on the elements. @@ -2627,99 +1732,372 @@ The `setAltText(value: String)` method can be used to set the altText of the Rev ### End to end example of revealing data with Skyflow Elements #### [Sample Code](https://github.com/skyflowapi/skyflow-android/blob/main/samples/src/main/java/com/Skyflow/RevealActivity.kt): -```kt -//Initialize skyflow configuration -val config = Skyflow.Configuration(vaultId = , vaultURL = , tokenProvider = demoTokenProvider) +```kotlin +// Initialize skyflow configuration +val config = Configuration( + vaultID = "", + vaultURL = "", + tokenProvider = demoTokenProvider +) -//Initialize skyflow client -val skyflowClient = Skyflow.initialize(config) +// Initialize skyflow client +val skyflowClient = init(config) -//Create a Reveal Container -val container = skyflowClient.container(type = Skyflow.ContainerType.REVEAL) +// Create a Reveal Container +val container = skyflowClient.container(ContainerType.REVEAL) +// Create Skyflow.Styles with individual Skyflow.Style variants +val baseStyle = Style(borderColor = Color.BLUE) +val baseTextStyle = Style(textColor = Color.BLACK) +val inputStyles = Styles(base = baseStyle) +val labelStyles = Styles(base = baseTextStyle) +val errorTextStyles = Styles(base = baseTextStyle) -//Create Skyflow.Styles with individual Skyflow.Style variants -val baseStyle = Skyflow.Style(borderColor = Color.BLUE) -val baseTextStyle = Skyflow.Style(textColor = Color.BLACK) -val inputStyles = Skyflow.Styles(base = baseStyle) -val labelStyles = Skyflow.Styles(base = baseTextStyle) -val errorTextStyles = Skyflow.Styles(base = baseTextStyle) - -//Create Reveal Elements -val cardNumberInput = Skyflow.RevealElementInput( - token = "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", - redaction = RedactionType.MASKED, - inputStyles = inputStyles, - labelStyles = labelStyles, - errorTextStyles = errorTextStyles, - label = "cardnumber", - altText = "XXXX XXXX XXXX XXXX" +// Create Reveal Elements — no redaction on individual elements +val cardNumberInput = RevealElementInput( + token = "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", + inputStyles = inputStyles, + labelStyles = labelStyles, + errorTextStyles = errorTextStyles, + label = "Card Number", + altText = "XXXX XXXX XXXX XXXX" ) -val cardNumberElement = container.create(context = Context, input = cardNumberInput) +val cardNumberElement = container.create(context = this, input = cardNumberInput) -val nameInput = Skyflow.RevealElementInput( - token = "89024714-6a26-4256-b9d4-55ad69aa4047", - redaction = RedactionType.DEFAULT, - inputStyles = inputStyles, - labelStyles = labelStyles, - errorTextStyles = errorTextStyles, - label = "fullname", - altText = "XXX" +val nameInput = RevealElementInput( + token = "89024714-6a26-4256-b9d4-55ad69aa4047", + inputStyles = inputStyles, + labelStyles = labelStyles, + errorTextStyles = errorTextStyles, + label = "Full Name", + altText = "XXX" ) -val nameElement = container.create(context = Context,input = nameInput) +val nameElement = container.create(context = this, input = nameInput) -//set error to the element +// Optionally set/reset custom error text on an element nameElement.setError("custom error") - -//reset error to the element nameElement.resetError() -//Can interact with these objects as a normal UIView Object and add to View +// Mount elements to the screen +parent.addView(cardNumberElement) +parent.addView(nameElement) +// Call reveal with typed RevealCallback +container.reveal(object : RevealCallback { + override fun onSuccess(response: RevealResponse) { + Log.d(TAG, response.toJson().toString()) + } + override fun onFailure(error: SkyflowError) { + Log.e(TAG, "reveal failed: ${error.message}") + } +}) +``` + +The `records` list contains both successful and failed tokens. Each record carries its own `httpCode` so you can handle mixed results in a single pass. + +#### Sample Response +```json +{ + "records": [ + { + "token": "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", + "tokenGroupName": "deterministic_string", + "metadata": { + "skyflowId": "3ac0424e-fe45-43a9-9193-2e6d2913cbd2", + "tableName": "cards" + }, + "httpCode": 200 + }, + { + "token": "89024714-6a26-4256-b9d4-55ad69aa4047", + "error": "Detokenize failed. Token 89024714-6a26-4256-b9d4-55ad69aa4047 is invalid. Specify a valid token.", + "httpCode": 404 + } + ] +} +``` + +A whole-request failure (e.g. auth error) skips `onSuccess` and delivers a `SkyflowError` to `onFailure`: +```json +{ + "grpcCode": 13, + "httpCode": 500, + "message": "Skyflow services experienced an internal error.", + "httpStatus": "Internal Server Error", + "details": [] +} +``` +--- + +# Typed callbacks and response handling + +The SDK provides typed callbacks and typed response objects for collect and reveal operations. Both successes and partial errors are returned in the same `records` list — each record carries its own `httpCode`, so you can handle mixed results without exceptions. -//Implement a custom Skyflow.Callback to be called on Reveal success/failure -public class RevealCallback: Skyflow.Callback { - override fun onSuccess(responseBody: Any) { - print(responseBody) +--- + +## Collect with typed callbacks + +### CollectCallback + +Implement `CollectCallback` to receive typed collect results: + +```kotlin +container.collect(object : CollectCallback { + override fun onSuccess(response: CollectResponse) { + Log.d(TAG, response.toJson().toString()) } - override fun onFailure(exception: Exception) { - print(exception) + override fun onFailure(error: SkyflowError) { + Log.d(TAG, "collect failure: code=${error.httpCode}, message=${error.message}") } +}) +``` + +#### Sample Response +```json +{ + "records": [ + { + "tableName": "pii_fields", + "skyflowId": "3ac0424e-fe45-43a9-9193-2e6d2913cbd2", + "tokens": { + "card_number": [ + { "token": "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", "tokenGroupName": "deterministic_string" } + ] + }, + "httpCode": 200 + } + ] } +``` + +### CollectOptions -//Initialize custom Skyflow.Callback -val revealCallback = RevealCallback() +#### Upsert support -//Call reveal method on RevealContainer -container.reveal(callback = revealCallback) +Pass `CollectOptions` with `upsert` to insert-or-update based on a unique column: +```kotlin +val options = CollectOptions( + upsert = listOf( + UpsertOptions( + tableName = "", + updateType = UpdateType.UPDATE, + uniqueColumns = listOf("") + ) + ) +) +container.collect(object : CollectCallback { ... }, options) ``` -The response below shows that some tokens assigned to the reveal elements get revealed successfully, while others fail and remain unrevealed. -#### Sample Response:Callback +#### Full-row overwrite with `UpdateType.REPLACE` + +`UpdateType` controls how a matched record is written: + +- `UpdateType.UPDATE` (used above) merges the fields in this request into the matched record — every other column on that record is left untouched. +- `UpdateType.REPLACE` overwrites the entire matched record — any column not included in this request (element values plus `additionalFields`) is cleared, not just left alone. + +Use `REPLACE` when a stale value from a previous write must not survive: the matched record ends up as exactly what this request contains, rather than layered on top of whatever was already there. + +```kotlin +val options = CollectOptions( + upsert = listOf( + UpsertOptions( + tableName = "", + updateType = UpdateType.REPLACE, + uniqueColumns = listOf("") + ) + ) +) +container.collect(object : CollectCallback { ... }, options) +``` + +#### Additional fields (non-PCI data) + +Pass non-PCI data alongside element values using `AdditionalFields`: + +```kotlin +val options = CollectOptions( + additionalFields = AdditionalFields( + records = listOf( + AdditionalFieldsRecord( + tableName = "", + data = mapOf("" to "") + // skyflowId = "" // set this to update an existing record + ) + ) + ) +) +container.collect(object : CollectCallback { ... }, options) +``` + + +**Note:** Set `skyflowId` on `AdditionalFieldsRecord` to update an existing record. Without it, a new record is inserted. + +### CollectResponse + +`CollectResponse.records` is a flat list of `CollectRecord` objects. Both successes and partial errors are included in the same list. + +```kotlin +data class CollectRecord( + val tableName: String?, + val skyflowId: String?, + val tokens: Map?, + val hashedData: Map?, + val error: String?, + val httpCode: Int +) +``` + +Both success and partial-error records are delivered as `CollectRecord` in the same `records` list. Check `httpCode` on each record to distinguish them. + +#### Sample response (success + partial error): ```json { - "success": [ - { - "token": "b63ec4e0-bbad-4e43-96e6-6bd50f483f75" + "records": [ + { + "tableName": "cards", + "skyflowId": "f1714ef8-8deb-489a-a18d-77e0e007f403", + "tokens": { + "cardNumber": [ + {"token": "f3907186-e7e2-466f-91e5-48e12c2bcbc1", "tokenGroupName": "deterministic_string"} + ] + }, + "httpCode": 200 + }, + { + "tableName": "persons", + "error": "Invalid request. Required field ssn is missing.", + "skyflowId": null, + "httpCode": 400 + } + ] +} +``` + +The SDK populates `tableName` on error records from the original request, so `CollectRecord.tableName` is never empty in the delivered response. + +#### If the entire request fails, `onFailure` delivers a `SkyflowError`: + +```kotlin +override fun onFailure(error: SkyflowError) { + Log.d(TAG, "httpCode=${error.httpCode}, message=${error.message}") +} +``` + + +#### Sample Code: +[CollectActivity.kt](https://github.com/skyflowapi/skyflow-android/blob/main/samples/src/main/java/com/Skyflow/CollectActivity.kt) + +--- + +## Reveal with typed callbacks + +### RevealCallback + +Implement `RevealCallback` to receive typed reveal results: + +```kotlin +revealContainer.reveal(object : RevealCallback { + override fun onSuccess(response: RevealResponse) { + response.records.forEach { record -> + if (record.httpCode == 200) { + Log.d(TAG, "reveal success: token=${record.token}") + } else { + Log.d(TAG, "reveal error [${record.httpCode}]: ${record.error}") + } + } } - ], - "errors": [ - { - "id": "89024714-6a26-4256-b9d4-55ad69aa4047", - "error": { - "code": 404, - "description": "Tokens not found for 89024714-6a26-4256-b9d4-55ad69aa4047" - } - } - ] + override fun onFailure(error: SkyflowError) { + Log.d(TAG, "reveal failure: code=${error.httpCode}, message=${error.message}") + } +}) +``` + + +### RevealOptions + +Apply a redaction to an entire token group using `RevealOptions.tokenGroupRedactions`. This is a request-level setting — the redaction applies to every token in the named group, not to individual reveal elements. + +```kotlin +val options = RevealOptions( + tokenGroupRedactions = listOf( + TokenGroupRedaction( + tokenGroupName = "", + redaction = "" + ) + ) +) +revealContainer.reveal(object : RevealCallback { ... }, options) +``` + + +### RevealResponse + +`RevealResponse.records` is a flat list of `RevealRecord` objects. Both successes and partial errors are included in the same list. + +```kotlin +data class RevealRecord( + val token: String, + val tokenGroupName: String?, + val metadata: Map?, // includes skyflowId, tableName + val error: String?, + val httpCode: Int +) +``` + +#### Sample success response: +```json +{ + "records": [ + { + "token": "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", + "tokenGroupName": "deterministic_string", + "metadata": { + "skyflowId": "3ac0424e-fe45-43a9-9193-2e6d2913cbd2", + "tableName": "cards" + }, + "httpCode": 200 + } + ] +} +``` + +#### Sample partial error response: +```json +{ + "records": [ + { + "token": "b63ec4e0-bbad-4e43-96e6-6bd50f483f75", + "tokenGroupName": "deterministic_string", + "metadata": { + "skyflowId": "3ac0424e-fe45-43a9-9193-2e6d2913cbd2", + "tableName": "cards" + }, + "httpCode": 200 + }, + { + "token": "a4b24714-6a26-4256-b9d4-55ad69aa4047", + "error": "Tokens not found for a4b24714-6a26-4256-b9d4-55ad69aa4047", + "httpCode": 404 + } + ] } ``` + +#### Sample Code: +[RevealActivity.kt](https://github.com/skyflowapi/skyflow-android/blob/main/samples/src/main/java/com/Skyflow/RevealActivity.kt) + +--- + ## Limitation Currently the skyflow collect elements and reveal elements can't be used in the XML layout definition, we have to add them to the views programatically. +## Reporting a Vulnerability + +If you discover a potential security issue in this project, please reach out to us at security@skyflow.com. Please do not create public GitHub issues or Pull Requests, as malicious actors could potentially view them. + diff --git a/Skyflow/build.gradle b/Skyflow/build.gradle index 1a5ecef..9d2da19 100644 --- a/Skyflow/build.gradle +++ b/Skyflow/build.gradle @@ -8,7 +8,7 @@ ext { mGroupId = "com.skyflowapi.android" mArtifactId = "skyflow-android-sdk" mVersionCode = 1 - mVersionName = "1.27.0" + mVersionName = "1.27.0-dev.7df0e54" mLibraryName = "skyflow-android" mLibraryDescription = "Skyflow’s android SDK can be used to securely collect, tokenize, and display sensitive data in the mobile without exposing your front-end infrastructure to sensitive data." @@ -17,6 +17,7 @@ ext { android { namespace "com.skyflow_android" compileSdk 35 + buildToolsVersion "35.0.0" defaultConfig { minSdk 21 diff --git a/Skyflow/src/main/kotlin/Skyflow/AdditionalFields.kt b/Skyflow/src/main/kotlin/Skyflow/AdditionalFields.kt new file mode 100644 index 0000000..d35fef7 --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/AdditionalFields.kt @@ -0,0 +1,9 @@ +package Skyflow + +data class AdditionalFields(val records: List) + +data class AdditionalFieldsRecord( + val tableName: String, + val data: Map, + val skyflowId: String? = null +) diff --git a/Skyflow/src/main/kotlin/Skyflow/Client.kt b/Skyflow/src/main/kotlin/Skyflow/Client.kt index d465822..2beffb1 100644 --- a/Skyflow/src/main/kotlin/Skyflow/Client.kt +++ b/Skyflow/src/main/kotlin/Skyflow/Client.kt @@ -2,234 +2,41 @@ package Skyflow import Skyflow.core.* import Skyflow.core.Logger -import Skyflow.get.GetOptions -import Skyflow.reveal.GetByIdRecord -import Skyflow.soap.SoapConnectionConfig import Skyflow.utils.Utils import android.content.Context import com.Skyflow.core.container.ContainerProtocol -import org.json.JSONArray import org.json.JSONObject -import org.xml.sax.InputSource -import java.io.StringReader -import javax.xml.parsers.DocumentBuilderFactory -import kotlin.Exception import kotlin.reflect.KClass class Client internal constructor( val configuration: Configuration, -){ +) { internal val tag = Client::class.qualifiedName - internal val apiClient = APIClient(configuration.vaultID, configuration.vaultURL, - configuration.tokenProvider,configuration.options.logLevel) + internal val apiClient = FlowDBAPIClient( + configuration.vaultID, + configuration.vaultURL, + configuration.tokenProvider, + configuration.options.logLevel, + okHttpClient = configuration.okHttpClient + ) - internal val elementMap = HashMap() - fun insert(records: JSONObject, options: InsertOptions? = InsertOptions(), callback: Callback){ - try { - Utils.checkVaultDetails(configuration) - Logger.info(tag, Messages.INSERTING_RECORDS.getMessage(configuration.vaultID), configuration.options.logLevel) - apiClient.post(records, loggingCallback(callback, Messages.INSERTING_RECORDS_SUCCESS.getMessage(configuration.vaultID)), options!!) - } - catch (e:Exception) - { - callback.onFailure(e) - } - } - fun detokenize(records: JSONObject, callback: Callback) { - try { - Utils.checkVaultDetails(configuration) - this.apiClient.get(records,loggingCallback(callback, Messages.DETOKENIZE_SUCCESS.getMessage())) - } - catch (e:Exception) - { - callback.onFailure(Utils.constructError(e)) - } - } - - fun getById(records: JSONObject, callback: Callback) - { - Logger.info(tag, Messages.GET_BY_ID_CALLED.getMessage(), configuration.options.logLevel) - try { - Utils.checkVaultDetails(configuration) - Logger.info(tag, Messages.GET_BY_ID_CALLED.getMessage(), configuration.options.logLevel) - val result = constructBodyForGetById(records) - this.apiClient.getById(result, callback) - } catch (e: Exception) { - callback.onFailure(Utils.constructError(e)) - } - } - - fun get(records: JSONObject, options: GetOptions?, callback: Callback) { - Logger.info(tag, Messages.GET_CALLED.getMessage(), configuration.options.logLevel) - try { - Utils.checkVaultDetails(configuration) - Logger.info(tag, Messages.GETTING_RECORDS.getMessage(), configuration.options.logLevel) - this.apiClient.get(records, options, callback) - } catch (e: Exception) { - callback.onFailure(Utils.constructError(e)) - } - } + internal val elementMap = HashMap() - @Deprecated("Support for this method will be removed soon. Please use any of the Server Side SDKs to invoke a connection", level = DeprecationLevel.WARNING) - internal fun invokeConnection(connectionConfig: ConnectionConfig, callback: Callback) { - try { - Logger.info(tag, Messages.INVOKE_CONNECTION_CALLED.getMessage(), configuration.options.logLevel) - val checkUrl = Utils.checkUrl(connectionConfig.connectionURL) - if(connectionConfig.connectionURL.isEmpty()) - throw SkyflowError(SkyflowErrorCode.EMPTY_CONNECTION_URL,tag, configuration.options.logLevel) - if (!checkUrl) - throw SkyflowError(SkyflowErrorCode.INVALID_CONNECTION_URL,tag, configuration.options.logLevel, arrayOf(connectionConfig.connectionURL)) - this.apiClient.invokeConnection(connectionConfig, callback,this) - } - catch (e:Exception) - { - callback.onFailure(Utils.constructError(e)) - } + // Internal stub kept for dormant PDB code (ConnectionApiCallback, SoapApiCallback) to compile. + // Not part of the public API and not called by any live code path. + @Suppress("unused") + internal fun detokenize(records: JSONObject, callback: Callback) { + callback.onFailure(Utils.constructError(Exception("detokenize is not available in FlowDB mode"))) } - @Deprecated("Support for this method will be removed soon. Please contact admin", level = DeprecationLevel.WARNING) - internal fun invokeSoapConnection(soapConnectionConfig: SoapConnectionConfig,callback: Callback) { - try { - validateSoapConnectionDetails(soapConnectionConfig) - this.apiClient.invokeSoapConnection(soapConnectionConfig, this, callback) - } - catch (e: Exception){ - callback.onFailure(e) - } - } - internal fun validateSoapConnectionDetails(soapConnectionConfig: SoapConnectionConfig) - { - if (soapConnectionConfig.connectionURL.isEmpty()) { - throw SkyflowError(SkyflowErrorCode.EMPTY_CONNECTION_URL, Utils.tag, configuration.options.logLevel) - } - if (!Utils.checkUrl(soapConnectionConfig.connectionURL)) { - throw SkyflowError(SkyflowErrorCode.INVALID_CONNECTION_URL, - Utils.tag, configuration.options.logLevel, params = arrayOf(soapConnectionConfig.connectionURL)) - } - if (soapConnectionConfig.requestXML.trim().isEmpty()) { - throw SkyflowError(SkyflowErrorCode.EMPTY_REQUEST_XML, Utils.tag, configuration.options.logLevel) - } - if (soapConnectionConfig.requestXML.trim().isNotEmpty()) { - try { - DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(InputSource(StringReader(soapConnectionConfig.requestXML))); - } catch (e: Exception) { - throw SkyflowError(SkyflowErrorCode.INVALID_REQUEST_XML, Utils.tag, configuration.options.logLevel, params = arrayOf(e.message.toString())) - } - } - if (soapConnectionConfig.responseXML.trim().isNotEmpty()) { - try { - DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(InputSource(StringReader(soapConnectionConfig.responseXML))); - } catch (e: Exception) { - throw SkyflowError(SkyflowErrorCode.INVALID_RESPONSE_XML, Utils.tag, configuration.options.logLevel, params = arrayOf(e.message.toString())) - } - } - } - - internal fun constructBodyForGetById(records: JSONObject): MutableList { - if (!records.has("records")) - throw SkyflowError( - SkyflowErrorCode.RECORDS_KEY_NOT_FOUND, - tag, configuration.options.logLevel - ) - else if (records.get("records") !is JSONArray) - throw SkyflowError(SkyflowErrorCode.INVALID_RECORDS, tag, configuration.options.logLevel) - val jsonArray = records.getJSONArray("records") - if (jsonArray.length() == 0) - throw SkyflowError(SkyflowErrorCode.EMPTY_RECORDS, tag, configuration.options.logLevel) - var i = 0 - val result = mutableListOf() - while (i < jsonArray.length()) { - val jsonObj = jsonArray.getJSONObject(i) - if (jsonObj == {}) - throw SkyflowError( - SkyflowErrorCode.EMPTY_RECORD_OBJECT, tag, configuration.options.logLevel, - arrayOf("$i") - ) - else if (!jsonObj.has("table")) { - throw SkyflowError( - SkyflowErrorCode.TABLE_KEY_NOY_FOUND, tag, configuration.options.logLevel, - arrayOf("$i") - ) - } else if (jsonObj.get("table") !is String) { - throw SkyflowError( - SkyflowErrorCode.INVALID_TABLE_NAME, tag, configuration.options.logLevel, - arrayOf("$i") - ) - } else if (!jsonObj.has("redaction")) { - throw SkyflowError( - SkyflowErrorCode.REDACTION_KEY_NOT_FOUND, tag, configuration.options.logLevel, - arrayOf("$i") - ) - } else if (!jsonObj.has("ids")) { - throw SkyflowError( - SkyflowErrorCode.IDS_KEY_NOT_FOUND, tag, configuration.options.logLevel, - arrayOf("$i") - ) - } else if (jsonObj.getString("table").isEmpty()) { - throw SkyflowError( - SkyflowErrorCode.EMPTY_TABLE_KEY, tag, configuration.options.logLevel, - arrayOf("$i") - ) - } else if (jsonObj.getString("redaction").isEmpty()) { - throw SkyflowError( - SkyflowErrorCode.EMPTY_REDACTION_VALUE, tag, configuration.options.logLevel, - arrayOf("$i") - ) - } else if (!(jsonObj.get("redaction").toString() == "PLAIN_TEXT" || - jsonObj.get("redaction").toString() == "DEFAULT" || - jsonObj.get("redaction").toString() == "MASKED" || - jsonObj.get("redaction").toString() == "REDACTED") - ) { - throw SkyflowError( - SkyflowErrorCode.INVALID_REDACTION_TYPE, tag, configuration.options.logLevel, - arrayOf("$i") - ) - } else { - var skyflowIds = jsonObj.get("ids") - try { - skyflowIds = skyflowIds as ArrayList - } catch (e: Exception) { - throw SkyflowError( - SkyflowErrorCode.INVALID_IDS, tag, configuration.options.logLevel, - arrayOf("$i") - ) - } - if (skyflowIds.isEmpty()) { - throw SkyflowError( - SkyflowErrorCode.EMPTY_RECORD_IDS, tag, configuration.options.logLevel, - arrayOf("$i") - ) - } - for (j in 0 until skyflowIds.size) { - if (skyflowIds[j].isEmpty()) { - throw SkyflowError( - SkyflowErrorCode.EMPTY_ID_IN_RECORD_IDS, - tag, configuration.options.logLevel, - arrayOf("$i") - ) - } - } - val record = GetByIdRecord( - skyflowIds, - jsonObj.get("table").toString(), - jsonObj.get("redaction").toString() - ) - result.add(record) - } - i++ - } - return result - } - - fun container(type: KClass) : Container{ - if(type == ContainerType.COLLECT){ + fun container(type: KClass): Container { + if (type == ContainerType.COLLECT) { Logger.info(tag, Messages.COLLECT_CONTAINER_CREATED.getMessage(), configuration.options.logLevel) - } - else if(type == ContainerType.REVEAL){ + } else if (type == ContainerType.REVEAL) { Logger.info(tag, Messages.REVEAL_CONTAINER_CREATED.getMessage(), configuration.options.logLevel) } - return Container(configuration,this) + return Container(configuration, this) } fun container( @@ -249,20 +56,4 @@ class Client internal constructor( } return Container(configuration, this, context, options) } - - inner class loggingCallback( - private val clientCallback: Callback, - private val successMessage: String, - ) : Callback { - override fun onSuccess(responseBody: Any) { - Logger.info(tag, successMessage, configuration.options.logLevel) - clientCallback.onSuccess(responseBody) - } - override fun onFailure(exception: Any) { - clientCallback.onFailure(exception) - } - } } - - - diff --git a/Skyflow/src/main/kotlin/Skyflow/CollectContainer.kt b/Skyflow/src/main/kotlin/Skyflow/CollectContainer.kt index a93115f..21bce16 100644 --- a/Skyflow/src/main/kotlin/Skyflow/CollectContainer.kt +++ b/Skyflow/src/main/kotlin/Skyflow/CollectContainer.kt @@ -1,13 +1,15 @@ package Skyflow -import Skyflow.collect.client.CollectRequestBody +import Skyflow.collect.client.CVVMap +import Skyflow.collect.client.FlowDBCollectRequestBody +import Skyflow.collect.client.FlowDBMixedAPICallback +import org.json.JSONObject import Skyflow.core.Logger import Skyflow.core.Messages import Skyflow.core.getMessage import Skyflow.utils.Utils import android.content.Context import com.Skyflow.core.container.ContainerProtocol -import org.json.JSONObject import java.util.* open class CollectContainer : ContainerProtocol { @@ -21,16 +23,8 @@ fun Container.create( options: CollectElementOptions = CollectElementOptions() ): TextField { Utils.checkInputFormatOptions(input.type, options, configuration.options.logLevel) - Logger.info( - tag, - Messages.VALIDATE_INPUT_FORMAT_OPTIONS.getMessage(input.label), - configuration.options.logLevel - ) - Logger.info( - tag, - Messages.CREATED_COLLECT_ELEMENT.getMessage(input.label), - configuration.options.logLevel - ) + Logger.info(tag, Messages.VALIDATE_INPUT_FORMAT_OPTIONS.getMessage(input.label), configuration.options.logLevel) + Logger.info(tag, Messages.CREATED_COLLECT_ELEMENT.getMessage(input.label), configuration.options.logLevel) val collectElement = TextField(context, configuration.options, collectElements.size) collectElement.setupField(input, options) collectElements.add(collectElement) @@ -40,61 +34,56 @@ fun Container.create( return collectElement } -fun Container.collect(callback: Callback, options: CollectOptions? = CollectOptions()){ +fun Container.collect(callback: Callback, options: CollectOptions? = CollectOptions()) { try { - Utils.checkVaultDetails(client.configuration) + validateVaultConfig() Logger.info(tag, Messages.VALIDATE_COLLECT_RECORDS.getMessage(), configuration.options.logLevel) validateElements() - post(callback,options) - } - catch (e:Exception) - { + post(callback, options) + } catch (e: Exception) { callback.onFailure(Utils.constructErrorResponse(e)) } } + +internal fun Container.validateVaultConfig() { + if (configuration.vaultID.isEmpty()) { + throw SkyflowInternalError(SkyflowErrorCode.EMPTY_VAULT_ID, tag, configuration.options.logLevel) + } + if (configuration.vaultURL.isEmpty()) { + throw SkyflowInternalError(SkyflowErrorCode.EMPTY_VAULT_URL, tag, configuration.options.logLevel) + } + if (!Utils.checkUrl(configuration.vaultURL)) { + throw SkyflowInternalError(SkyflowErrorCode.INVALID_VAULT_URL, tag, configuration.options.logLevel) + } +} + internal fun Container.validateElements() { var errors = "" for (element in this.collectElements) { - errors = validateElement(element,errors) + errors = validateElement(element, errors) } if (errors != "") { - throw SkyflowError(SkyflowErrorCode.INVALID_INPUT, tag, configuration.options.logLevel, arrayOf(errors)) + throw SkyflowInternalError(SkyflowErrorCode.INVALID_INPUT, tag, configuration.options.logLevel, arrayOf(errors)) } } -internal fun Container.validateElement(element: TextField,err:String) : String -{ +internal fun Container.validateElement(element: TextField, err: String): String { var errorOnElement = err if (!element.isAttachedToWindow()) { - throw SkyflowError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, - tag, - configuration.options.logLevel, - arrayOf(element.columnName)) + throw SkyflowInternalError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, configuration.options.logLevel, arrayOf(element.columnName)) } when { - element.collectInput.table.equals(null) -> { - throw SkyflowError(SkyflowErrorCode.MISSING_TABLE_IN_ELEMENT, - tag, - configuration.options.logLevel, - arrayOf(element.fieldType.toString())) + 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 SkyflowError(SkyflowErrorCode.MISSING_COLUMN, - tag, - configuration.options.logLevel, - arrayOf(element.fieldType.toString())) + throw SkyflowInternalError(SkyflowErrorCode.MISSING_COLUMN, tag, configuration.options.logLevel, arrayOf(element.fieldType.toString())) } - element.collectInput.table!!.isEmpty() -> { - throw SkyflowError(SkyflowErrorCode.ELEMENT_EMPTY_TABLE_NAME, - tag, - configuration.options.logLevel, - arrayOf(element.fieldType.toString())) + element.collectInput.tableName!!.isEmpty() -> { + throw SkyflowInternalError(SkyflowErrorCode.ELEMENT_EMPTY_TABLE_NAME, tag, configuration.options.logLevel, arrayOf(element.fieldType.toString())) } element.collectInput.column!!.isEmpty() -> { - throw SkyflowError(SkyflowErrorCode.EMPTY_COLUMN_NAME, - tag, - configuration.options.logLevel, - arrayOf(element.fieldType.toString())) + throw SkyflowInternalError(SkyflowErrorCode.EMPTY_COLUMN_NAME, tag, configuration.options.logLevel, arrayOf(element.fieldType.toString())) } else -> { val state = element.getState() @@ -107,45 +96,105 @@ internal fun Container.validateElement(element: TextField,err: } return errorOnElement } -internal fun Container.post(callback:Callback,options: CollectOptions?) -{ - // Separate insert and update elements/records - val (insertElements, insertAdditionalFields, updateRecords) = Skyflow.collect.client.CollectRequestBody.separateInsertAndUpdateRecords( + +internal fun Container.post(callback: Callback, options: CollectOptions?) { + val collectOptions = options ?: CollectOptions() + val updateElements = collectElements.filter { !it.skyflowId.isNullOrEmpty() } + val insertElements = collectElements.filter { it.skyflowId.isNullOrEmpty() } + + val additionalUpdates = collectOptions.additionalFields?.records + ?.filter { !it.skyflowId.isNullOrEmpty() } ?: emptyList() + val additionalInserts = collectOptions.additionalFields?.records + ?.filter { it.skyflowId.isNullOrEmpty() } ?: emptyList() + + if (updateElements.isNotEmpty() || additionalUpdates.isNotEmpty()) { + // Build ONE combined update body — tableName per record so all go in a single API call + val updateRecordsArray = org.json.JSONArray() + + updateElements.groupBy { it.skyflowId }.forEach { (_, elements) -> + val tableName = elements.first().tableName + val skyflowID = elements.first().skyflowId!! + val singleBody = FlowDBCollectRequestBody.buildUpdateRequestBody( + configuration.vaultID, tableName, elements.toMutableList(), + skyflowID, configuration.options.logLevel + ) + val rec = singleBody.getJSONArray("records").getJSONObject(0) + rec.put("tableName", tableName) + updateRecordsArray.put(rec) + } + + additionalUpdates.groupBy { it.skyflowId }.forEach { (_, records) -> + val merged = records.fold(mutableMapOf()) { acc, r -> acc.also { it.putAll(r.data) } } + val dataObj = org.json.JSONObject().apply { merged.forEach { (k, v) -> put(k, v) } } + updateRecordsArray.put( + org.json.JSONObject() + .put("skyflowID", records.first().skyflowId!!) + .put("data", dataObj) + .put("tableName", records.first().tableName) + ) + } + + val combinedUpdateBody = JSONObject() + .put("vaultID", configuration.vaultID) + .put("records", updateRecordsArray) + + val insertBody: JSONObject? = if (insertElements.isNotEmpty() || additionalInserts.isNotEmpty()) { + val insertOptions = CollectOptions( + upsert = collectOptions.upsert?.filter { opt -> insertElements.any { it.tableName == opt.tableName } }, + additionalFields = if (additionalInserts.isEmpty()) null else AdditionalFields(additionalInserts) + ) + FlowDBCollectRequestBody.buildRequestBody( + configuration.vaultID, insertElements.toMutableList(), + insertOptions, configuration.options.logLevel + ) + } else null + + val mixedCallback = FlowDBMixedAPICallback( + client.apiClient, combinedUpdateBody, insertBody, callback, collectOptions, + configuration.options.logLevel, CVVMap.capture(collectElements) + ) + client.apiClient.getAccessToken(mixedCallback) + return + } + + val insertOptions = CollectOptions( + upsert = collectOptions.upsert, + additionalFields = if (additionalInserts.isEmpty()) null else AdditionalFields(additionalInserts) + ) + val requestBody = FlowDBCollectRequestBody.buildRequestBody( + configuration.vaultID, this.collectElements, - options?.additionalFields, + insertOptions, configuration.options.logLevel ) - - val hasInsertData = insertElements.isNotEmpty() || insertAdditionalFields != null - val hasUpdateRecords = updateRecords.isNotEmpty() - - if (hasInsertData && hasUpdateRecords) { - // Mixed case: both insert and update - val insertRecordsJson = if (insertElements.isNotEmpty()) { - JSONObject(Skyflow.collect.client.CollectRequestBody.createRequestBody( - insertElements, - insertAdditionalFields, - configuration.options.logLevel - )) - } else { - insertAdditionalFields + this.client.apiClient.post(requestBody, callback, collectOptions, cvvMap = CVVMap.capture(this.collectElements)) +} + +fun Container.collect(callback: CollectCallback, options: CollectOptions? = CollectOptions()) { + val adapter = object : Callback { + override fun onSuccess(responseBody: Any) { + callback.onSuccess(CollectResponse.fromJson(responseBody.toString())) + } + override fun onFailure(exception: Any) { + callback.onFailure(SkyflowError.fromJson(exception.toString())) } - - val insertOptions = InsertOptions(options?.token ?: true, options?.upsert) - this.client.apiClient.postWithUpdate(insertRecordsJson, updateRecords, callback, insertOptions) - } else if (hasUpdateRecords) { - // Only update records - val insertOptions = InsertOptions(options?.token ?: true, options?.upsert) - this.client.apiClient.postWithUpdate(null, updateRecords, callback, insertOptions) - } else { - // Only insert records - val records = Skyflow.collect.client.CollectRequestBody.createRequestBody( - this.collectElements, - insertAdditionalFields, + } + collect(adapter, options) +} + +fun Container.update(tableName: String, skyflowID: String, callback: Callback, options: CollectOptions = CollectOptions()) { + try { + validateVaultConfig() + val requestBody = FlowDBCollectRequestBody.buildUpdateRequestBody( + configuration.vaultID, + tableName, + this.collectElements, + skyflowID, configuration.options.logLevel ) - val insertOptions = InsertOptions(options?.token ?: true, options?.upsert) - this.client.apiClient.post(JSONObject(records), callback, insertOptions) + 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/CollectElementInput.kt b/Skyflow/src/main/kotlin/Skyflow/CollectElementInput.kt index e2eef7d..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(), @@ -11,7 +11,7 @@ class CollectElementInput( internal var label: String = "", internal var placeholder: String = "", internal var validations: ValidationSet = ValidationSet(), - internal var skyflowID: String? = null + internal var skyflowId: String? = null ) { internal lateinit var type: SkyflowElementType @@ -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(), @@ -33,9 +33,9 @@ class CollectElementInput( placeholder: String = "", altText: String = "", validations: ValidationSet = ValidationSet(), - skyflowID: String? = null + skyflowId: String? = null ) : this( - table, + tableName, column, inputStyles, labelStyles, @@ -43,9 +43,9 @@ class CollectElementInput( label, placeholder, validations, - skyflowID + skyflowId ) { this.type = type this.altText = altText } -} \ No newline at end of file +} diff --git a/Skyflow/src/main/kotlin/Skyflow/CollectOptions.kt b/Skyflow/src/main/kotlin/Skyflow/CollectOptions.kt index a07417c..f46d8cb 100644 --- a/Skyflow/src/main/kotlin/Skyflow/CollectOptions.kt +++ b/Skyflow/src/main/kotlin/Skyflow/CollectOptions.kt @@ -1,7 +1,6 @@ package Skyflow -import org.json.JSONArray -import org.json.JSONObject - -class CollectOptions(val token:Boolean = true, val additionalFields: JSONObject? = null, val upsert : JSONArray? = null) { -} \ No newline at end of file +data class CollectOptions( + val upsert: List? = null, + val additionalFields: AdditionalFields? = null +) diff --git a/Skyflow/src/main/kotlin/Skyflow/CollectResponse.kt b/Skyflow/src/main/kotlin/Skyflow/CollectResponse.kt new file mode 100644 index 0000000..f459185 --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/CollectResponse.kt @@ -0,0 +1,127 @@ +package Skyflow + +import org.json.JSONArray +import org.json.JSONObject + +data class CollectRecord( + val tableName: String? = null, + val skyflowId: String? = null, + val error: String? = null, + val tokens: Map? = null, + val hashedData: Map? = null, + val httpCode: Int = 0 +) + +data class CollectResponse(val records: List = emptyList()) { + fun toJson(): JSONObject { + val arr = JSONArray() + records.forEach { r -> + val obj = JSONObject().put("httpCode", r.httpCode) + r.tableName?.let { obj.put("tableName", it) } + r.skyflowId?.let { obj.put("skyflowId", it) } + r.error?.let { obj.put("error", it) } + r.tokens?.let { tokens -> + val tokObj = JSONObject() + tokens.forEach { (col, v) -> + when (v) { + is List<*> -> { + val tokenArr = JSONArray() + v.filterIsInstance>().forEach { entry -> + val e = JSONObject() + entry.forEach { (k, ev) -> e.put(k.toString(), ev) } + tokenArr.put(e) + } + tokObj.put(col, tokenArr) + } + else -> tokObj.put(col, v) + } + } + obj.put("tokens", tokObj) + } + r.hashedData?.let { hd -> + val hdObj = JSONObject() + hd.forEach { (k, v) -> hdObj.put(k, v) } + obj.put("hashedData", hdObj) + } + arr.put(obj) + } + return JSONObject().put("records", arr) + } + + companion object { + fun fromJson(json: String): CollectResponse { + return try { + val root = JSONObject(json) + val records = root.optJSONArray("records")?.let { arr -> + (0 until arr.length()).mapNotNull { i -> + val r = arr.optJSONObject(i) ?: return@mapNotNull null + val httpCode = r.optInt("httpCode", 200) + if (r.has("error") && !r.isNull("error")) { + CollectRecord( + tableName = r.optString("tableName").ifEmpty { null }, + skyflowId = if (r.isNull("skyflowId")) null else r.optString("skyflowId").ifEmpty { null }, + error = r.optString("error"), + tokens = null, + hashedData = null, + httpCode = httpCode + ) + } else { + val fieldsObj = r.optJSONObject("tokens") + val tokens: Map? = fieldsObj?.let { obj -> + val map = mutableMapOf() + val keys = obj.keys() + while (keys.hasNext()) { + val key = keys.next() + val arr2 = obj.optJSONArray(key) + if (arr2 != null) { + map[key] = (0 until arr2.length()).mapNotNull { j -> + val e = arr2.optJSONObject(j) ?: return@mapNotNull null + e.keys().asSequence().associateWith { k -> e.opt(k) } + } + } else { + map[key] = obj.opt(key) + } + } + map + } + val hashedDataObj = r.optJSONObject("hashedData") + val hashedData: Map? = hashedDataObj?.let { obj -> + val map = mutableMapOf() + val keys = obj.keys() + while (keys.hasNext()) { + val key = keys.next() + val arr2 = obj.optJSONArray(key) + if (arr2 != null) { + map[key] = (0 until arr2.length()).mapNotNull { j -> + val e = arr2.optJSONObject(j) ?: return@mapNotNull null + e.keys().asSequence().associateWith { k -> e.opt(k) } + } + } else { + map[key] = obj.opt(key) + } + } + map + } + CollectRecord( + tableName = r.optString("tableName").ifEmpty { null }, + skyflowId = r.optString("skyflowId").ifEmpty { null }, + error = null, + tokens = tokens, + hashedData = hashedData, + httpCode = httpCode + ) + } + } + } ?: emptyList() + CollectResponse(records) + } catch (e: Exception) { + CollectResponse() + } + } + } +} + +interface CollectCallback { + fun onSuccess(response: CollectResponse) + fun onFailure(error: SkyflowError) +} diff --git a/Skyflow/src/main/kotlin/Skyflow/Configuration.kt b/Skyflow/src/main/kotlin/Skyflow/Configuration.kt index 9fb525a..36581ab 100644 --- a/Skyflow/src/main/kotlin/Skyflow/Configuration.kt +++ b/Skyflow/src/main/kotlin/Skyflow/Configuration.kt @@ -7,12 +7,5 @@ class Configuration( var vaultURL: String = "", val tokenProvider: TokenProvider, val options: Options = Options(), -){ - init { - if( vaultURL.endsWith("/")){ - vaultURL += "v1/vaults/" - } else{ - vaultURL += "/v1/vaults/" - } - } -} \ No newline at end of file + val okHttpClient: okhttp3.OkHttpClient = okhttp3.OkHttpClient(), +) \ No newline at end of file diff --git a/Skyflow/src/main/kotlin/Skyflow/Element.kt b/Skyflow/src/main/kotlin/Skyflow/Element.kt index e120f27..49513de 100644 --- a/Skyflow/src/main/kotlin/Skyflow/Element.kt +++ b/Skyflow/src/main/kotlin/Skyflow/Element.kt @@ -13,7 +13,7 @@ open class Element @JvmOverloads constructor( internal var isRequired: Boolean = false internal var columnName: String = "" internal var tableName: String = "" - internal var skyflowID: String? = null + internal var skyflowId: String? = null internal lateinit var collectInput : CollectElementInput internal lateinit var options : Skyflow.CollectElementOptions internal lateinit var fieldType: SkyflowElementType @@ -31,13 +31,12 @@ 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!! - if(!this.collectInput.skyflowID.equals(null)) - skyflowID = this.collectInput.skyflowID isRequired = this.options.required + skyflowId = this.collectInput.skyflowId state = State(columnName,isRequired) } diff --git a/Skyflow/src/main/kotlin/Skyflow/RevealContainer.kt b/Skyflow/src/main/kotlin/Skyflow/RevealContainer.kt index 5efb4e8..ef135a1 100644 --- a/Skyflow/src/main/kotlin/Skyflow/RevealContainer.kt +++ b/Skyflow/src/main/kotlin/Skyflow/RevealContainer.kt @@ -5,7 +5,7 @@ import Skyflow.core.Messages import Skyflow.core.getMessage import android.content.Context import com.Skyflow.core.container.ContainerProtocol -import Skyflow.reveal.RevealRequestBody +import Skyflow.reveal.FlowDBRevealRequestBody import Skyflow.reveal.RevealValueCallback import Skyflow.utils.Utils import Skyflow.utils.Utils.Companion.checkIfElementsMounted @@ -45,7 +45,6 @@ fun Container.reveal( options: RevealOptions? = RevealOptions() ) { try { - Utils.checkVaultDetails(client.configuration) validateElements() Logger.info( tag, @@ -62,22 +61,22 @@ internal fun Container.validateElements() { for (element in this.revealElements) { val token = element.revealInput.token if (!checkIfElementsMounted(element)) { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.ELEMENT_NOT_MOUNTED_REVEAL, tag, configuration.options.logLevel, arrayOf(element.revealInput.label) ) } if (element.isTokenNull) { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.TOKEN_KEY_NOT_FOUND_REVEAL, tag, configuration.options.logLevel, ) } else if (token!!.isEmpty()) { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.EMPTY_TOKEN_REVEAL, tag, configuration.options.logLevel ) } else if (element.isError) { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.ERROR_STATE_REVEAL, tag, configuration.options.logLevel, arrayOf("${element.error.text}") ) @@ -85,12 +84,28 @@ internal fun Container.validateElements() { } } +fun Container.reveal(callback: RevealCallback, options: RevealOptions? = RevealOptions()) { + val adapter = object : Callback { + override fun onSuccess(responseBody: Any) { + callback.onSuccess(RevealResponse.fromJson(responseBody.toString())) + } + override fun onFailure(exception: Any) { + callback.onFailure(SkyflowError.fromJson(exception.toString())) + } + } + reveal(adapter, options) +} + internal fun Container.get(callback: Callback, options: RevealOptions?) { val revealValueCallback = RevealValueCallback( callback, this.revealElements, configuration.options.logLevel ) - val records = RevealRequestBody.createRequestBody(this.revealElements) - this.client.apiClient.get(records, revealValueCallback) + val requestBody = FlowDBRevealRequestBody.buildRequestBody( + configuration.vaultID, + this.revealElements, + options + ) + this.client.apiClient.get(requestBody, revealValueCallback) } \ No newline at end of file diff --git a/Skyflow/src/main/kotlin/Skyflow/RevealElementInput.kt b/Skyflow/src/main/kotlin/Skyflow/RevealElementInput.kt index 1e132a1..14c173d 100644 --- a/Skyflow/src/main/kotlin/Skyflow/RevealElementInput.kt +++ b/Skyflow/src/main/kotlin/Skyflow/RevealElementInput.kt @@ -2,10 +2,9 @@ package Skyflow class RevealElementInput( internal var token: String? = null, - internal var redaction: RedactionType? = RedactionType.PLAIN_TEXT, internal var inputStyles: Styles = Styles(), internal var labelStyles: Styles = Styles(), internal var errorTextStyles: Styles = Styles(), internal var label: String = "", internal var altText: String = "" -) {} \ No newline at end of file +) {} diff --git a/Skyflow/src/main/kotlin/Skyflow/RevealOptions.kt b/Skyflow/src/main/kotlin/Skyflow/RevealOptions.kt index 9ae3cdb..90b8a1b 100644 --- a/Skyflow/src/main/kotlin/Skyflow/RevealOptions.kt +++ b/Skyflow/src/main/kotlin/Skyflow/RevealOptions.kt @@ -1,4 +1,10 @@ package Skyflow -class RevealOptions { -} \ No newline at end of file +data class TokenGroupRedaction( + val tokenGroupName: String, + val redaction: String +) + +data class RevealOptions( + val tokenGroupRedactions: List? = null +) diff --git a/Skyflow/src/main/kotlin/Skyflow/RevealResponse.kt b/Skyflow/src/main/kotlin/Skyflow/RevealResponse.kt new file mode 100644 index 0000000..c8bbda5 --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/RevealResponse.kt @@ -0,0 +1,73 @@ +package Skyflow + +import org.json.JSONArray +import org.json.JSONObject + +data class RevealRecord( + val token: String, + val error: String? = null, + val tokenGroupName: String? = null, + val metadata: Map? = null, + val httpCode: Int = 0 +) + +data class RevealResponse(val records: List = emptyList()) { + fun toJson(): JSONObject { + val arr = JSONArray() + records.forEach { r -> + val obj = JSONObject().put("token", r.token).put("httpCode", r.httpCode) + r.tokenGroupName?.let { obj.put("tokenGroupName", it) } + r.error?.let { obj.put("error", it) } + r.metadata?.let { m -> + val meta = JSONObject() + m.forEach { (k, v) -> meta.put(k, v) } + obj.put("metadata", meta) + } + arr.put(obj) + } + return JSONObject().put("records", arr) + } + + companion object { + fun fromJson(json: String): RevealResponse { + return try { + val root = JSONObject(json) + val records = root.optJSONArray("records")?.let { arr -> + (0 until arr.length()).mapNotNull { i -> + val r = arr.optJSONObject(i) ?: return@mapNotNull null + val httpCode = r.optInt("httpCode", 200) + if (r.has("error") && !r.isNull("error")) { + RevealRecord( + token = r.optString("token"), + error = r.optString("error"), + tokenGroupName = null, + metadata = null, + httpCode = httpCode + ) + } else { + val metaObj = r.optJSONObject("metadata") + val metadata: Map? = metaObj?.let { obj -> + obj.keys().asSequence().associateWith { k -> obj.opt(k) } + } + RevealRecord( + token = r.optString("token"), + error = null, + tokenGroupName = r.optString("tokenGroupName").ifEmpty { null }, + metadata = metadata, + httpCode = httpCode + ) + } + } + } ?: emptyList() + RevealResponse(records) + } catch (e: Exception) { + RevealResponse() + } + } + } +} + +interface RevealCallback { + fun onSuccess(response: RevealResponse) + fun onFailure(error: SkyflowError) +} diff --git a/Skyflow/src/main/kotlin/Skyflow/SkyflowError.kt b/Skyflow/src/main/kotlin/Skyflow/SkyflowError.kt index 2c763cf..4e83115 100644 --- a/Skyflow/src/main/kotlin/Skyflow/SkyflowError.kt +++ b/Skyflow/src/main/kotlin/Skyflow/SkyflowError.kt @@ -5,7 +5,7 @@ import Skyflow.utils.Utils -class SkyflowError(val skyflowErrorCode: SkyflowErrorCode = SkyflowErrorCode.UNKNOWN_ERROR, val tag : String? = "", logLevel: LogLevel? = null, params: Array = arrayOf()) : Exception(skyflowErrorCode.getMessage()) { +internal class SkyflowInternalError(val skyflowErrorCode: SkyflowErrorCode = SkyflowErrorCode.UNKNOWN_ERROR, val tag : String? = "", logLevel: LogLevel? = null, params: Array = arrayOf()) : Exception(skyflowErrorCode.getMessage()) { override var message = "" internal var internalMessage = "" diff --git a/Skyflow/src/main/kotlin/Skyflow/SkyflowException.kt b/Skyflow/src/main/kotlin/Skyflow/SkyflowException.kt new file mode 100644 index 0000000..73625db --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/SkyflowException.kt @@ -0,0 +1,68 @@ +package Skyflow + +import org.json.JSONArray +import org.json.JSONObject + +data class SkyflowError( + val grpcCode: Int?, + val httpCode: Int?, + override val message: String?, + val httpStatus: String?, + val details: List? +) : Error(message) { + companion object { + fun fromJson(json: String): SkyflowError { + return try { + val root = JSONObject(json) + + // Priority 1: flat format { "grpcCode": 13, "httpCode": 500, "message": "...", ... } + if (root.has("grpcCode") || (root.has("httpCode") && !root.has("error") && !root.has("errors"))) { + return SkyflowError( + grpcCode = if (root.has("grpcCode")) root.optInt("grpcCode") else null, + httpCode = if (root.has("httpCode")) root.optInt("httpCode") else null, + message = root.optString("message").ifEmpty { null }, + httpStatus = root.optString("httpStatus").ifEmpty { null }, + details = parseDetails(root.optJSONArray("details")) + ) + } + + // Priority 2: wrapped format { "error": { "grpcCode", "httpCode", "message", ... } } + if (root.has("error") && root.optJSONObject("error") != null) { + val err = root.getJSONObject("error") + return SkyflowError( + grpcCode = if (err.has("grpcCode")) err.optInt("grpcCode") else null, + httpCode = err.optInt("httpCode", err.optInt("code", 500)), + message = err.optString("message", err.optString("description", "Unknown error")).ifEmpty { null }, + httpStatus = err.optString("httpStatus").ifEmpty { null }, + details = parseDetails(err.optJSONArray("details")) + ) + } + + // Priority 3: legacy format { "errors": [{ "error": { "code", "description" } }] } + val errorsArr = root.optJSONArray("errors") + if (errorsArr != null && errorsArr.length() > 0) { + val first = errorsArr.optJSONObject(0) + val err = first?.optJSONObject("error") ?: first + if (err != null) { + return SkyflowError( + grpcCode = null, + httpCode = err.optInt("code", err.optInt("httpCode", 500)), + message = err.optString("description", err.optString("message", "Unknown error")).ifEmpty { null }, + httpStatus = "${err.optInt("code", 500)}", + details = emptyList() + ) + } + } + + SkyflowError(null, 500, json, null, emptyList()) + } catch (e: Exception) { + SkyflowError(null, 500, e.message ?: "Unknown error", null, emptyList()) + } + } + + private fun parseDetails(arr: JSONArray?): List { + if (arr == null) return emptyList() + return (0 until arr.length()).mapNotNull { arr.opt(it) } + } + } +} 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/UpdateType.kt b/Skyflow/src/main/kotlin/Skyflow/UpdateType.kt new file mode 100644 index 0000000..57989bd --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/UpdateType.kt @@ -0,0 +1,6 @@ +package Skyflow + +enum class UpdateType { + UPDATE, + REPLACE +} diff --git a/Skyflow/src/main/kotlin/Skyflow/UpsertOptions.kt b/Skyflow/src/main/kotlin/Skyflow/UpsertOptions.kt new file mode 100644 index 0000000..293eb9d --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/UpsertOptions.kt @@ -0,0 +1,7 @@ +package Skyflow + +data class UpsertOptions( + val tableName: String, + val updateType: UpdateType, + val uniqueColumns: List +) diff --git a/Skyflow/src/main/kotlin/Skyflow/collect/client/CollectAPICallback.kt b/Skyflow/src/main/kotlin/Skyflow/collect/client/CollectAPICallback.kt index 7c80d6b..3ef469f 100644 --- a/Skyflow/src/main/kotlin/Skyflow/collect/client/CollectAPICallback.kt +++ b/Skyflow/src/main/kotlin/Skyflow/collect/client/CollectAPICallback.kt @@ -61,26 +61,18 @@ internal class CollectAPICallback( val inputRecords = this.records["records"] as JSONArray val recordsArray = JSONArray() val responseObject = JSONObject() - if (this.options.tokens) { - for (i in responseJson.length() / 2 until responseJson.length()) { - val skyflowIDsObject = - JSONObject(responseJson[i - (responseJson.length() - responseJson.length() / 2)].toString()) - val skyflowIDs = skyflowIDsObject.getJSONArray("records") - val skyflowID = JSONObject(skyflowIDs[0].toString()).get("skyflow_id") - val record = JSONObject(responseJson[i].toString()) - val inputRecord = inputRecords.get(i - responseJson.length() / 2) as JSONObject - record.put("table", inputRecord["table"]) - val fields = JSONObject(record.get("fields").toString()) - fields.put("skyflow_id", skyflowID) - record.put("fields", fields) - recordsArray.put(record) - } - } else { - for (i in 0 until responseJson.length()) { - val inputRecord = inputRecords.get(i) as JSONObject - val record = (responseJson[i] as JSONObject)["records"] as JSONArray - recordsArray.put((record.get(0) as JSONObject).put("table", inputRecord["table"])) - } + for (i in responseJson.length() / 2 until responseJson.length()) { + val skyflowIDsObject = + JSONObject(responseJson[i - (responseJson.length() - responseJson.length() / 2)].toString()) + val skyflowIDs = skyflowIDsObject.getJSONArray("records") + val skyflowID = JSONObject(skyflowIDs[0].toString()).get("skyflow_id") + val record = JSONObject(responseJson[i].toString()) + val inputRecord = inputRecords.get(i - responseJson.length() / 2) as JSONObject + record.put("table", inputRecord["table"]) + val fields = JSONObject(record.get("fields").toString()) + fields.put("skyflow_id", skyflowID) + record.put("fields", fields) + recordsArray.put(record) } return responseObject.put("records", recordsArray) } diff --git a/Skyflow/src/main/kotlin/Skyflow/collect/client/CollectRequestBody.kt b/Skyflow/src/main/kotlin/Skyflow/collect/client/CollectRequestBody.kt index 8c083cb..305cd2c 100644 --- a/Skyflow/src/main/kotlin/Skyflow/collect/client/CollectRequestBody.kt +++ b/Skyflow/src/main/kotlin/Skyflow/collect/client/CollectRequestBody.kt @@ -19,9 +19,9 @@ internal class CollectRequestBody { // Process elements first for (element in elements) { - if (element.skyflowID != null && element.skyflowID!!.isNotEmpty()) { + if (element.skyflowId != null && element.skyflowId!!.isNotEmpty()) { // This is an update record - val key = "${element.tableName}_${element.skyflowID}" + val key = "${element.tableName}_${element.skyflowId}" if (updateRecordsMap.containsKey(key)) { // Add column to existing update record updateRecordsMap[key]!!.columns[element.columnName] = element.getValue() @@ -30,7 +30,7 @@ internal class CollectRequestBody { val columns = mutableMapOf(element.columnName to element.getValue()) updateRecordsMap[key] = UpdateRequestRecord( table = element.tableName, - skyflowID = element.skyflowID!!, + skyflowID = element.skyflowId!!, columns = columns ) } @@ -120,7 +120,7 @@ internal class CollectRequestBody { } } if (!hasElementValueMatchRule) - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.DUPLICATE_COLUMN_FOUND, tag, logLevel, arrayOf(element.tableName, element.columnName) ) @@ -141,12 +141,12 @@ internal class CollectRequestBody { if (additionalFields != null) { if (additionalFields.has("records")) { if (additionalFields.get("records") !is JSONArray) - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.ADDITIONAL_FIELDS_INVALID_RECORDS, tag, logLevel ) val records = additionalFields.getJSONArray("records") if (records.length() == 0) { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.ADDITIONAL_FIELDS_EMPTY_RECORDS, tag, logLevel ) } @@ -154,28 +154,28 @@ internal class CollectRequestBody { while (i < records.length()) { val jsonobj = records.getJSONObject(i) if (!jsonobj.has("table")) - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.ADDITIONAL_FIELDS_TABLE_KEY_NOT_FOUND, tag, logLevel, arrayOf("$i") ) else if (!jsonobj.has("fields")) - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.ADDITIONAL_FIELDS_FIELDS_KEY_NOT_FOUND, tag, logLevel, arrayOf("$i") ) else if (jsonobj.getJSONObject("fields").toString() == "{}") - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.ADDITIONAL_FIELDS_EMPTY_FIELDS, tag, logLevel, arrayOf("$i") ) val tableName = jsonobj.get("table") if (tableName !is String) - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.ADDITIONAL_FIELDS_INVALID_TABLE_NAME, tag, logLevel, arrayOf("$i") ) if (tableName.isEmpty()) - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.ADDITIONAL_FIELDS_EMPTY_TABLE_KEY, tag, logLevel, arrayOf("$i") ) @@ -185,7 +185,7 @@ internal class CollectRequestBody { val fieldList = mutableListOf() for (j in 0 until keys!!.length()) { if (keys.getString(j).isEmpty()) { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.EMPTY_COLUMN_NAME, tag, logLevel ) } @@ -197,7 +197,7 @@ internal class CollectRequestBody { if (tableMap[tableName] != null) { for (k in 0 until fieldList.size) { if (tableWithColumn.contains(tableName + fieldList[k].columnName)) - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.DUPLICATE_COLUMN_FOUND, tag, logLevel, arrayOf(tableName, fieldList[k].columnName) ) @@ -210,7 +210,7 @@ internal class CollectRequestBody { val tempArray = mutableListOf() for (k in 0 until fieldList.size) { if (tableWithColumn.contains(tableName + fieldList[k].columnName)) - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.DUPLICATE_COLUMN_FOUND, tag, logLevel, @@ -226,7 +226,7 @@ internal class CollectRequestBody { i++ } } else - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.ADDITIONAL_FIELDS_RECORDS_KEY_NOT_FOUND, tag, logLevel ) } diff --git a/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBCollectAPICallback.kt b/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBCollectAPICallback.kt new file mode 100644 index 0000000..f400098 --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBCollectAPICallback.kt @@ -0,0 +1,148 @@ +package Skyflow.collect.client + +import Skyflow.* +import Skyflow.core.FlowDBAPIClient +import Skyflow.core.Logger +import Skyflow.core.Messages +import Skyflow.core.getMessage +import Skyflow.utils.Utils +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import java.io.IOException + +internal class FlowDBCollectAPICallback( + private val apiClient: FlowDBAPIClient, + private val requestBody: JSONObject, + val callback: Skyflow.Callback, + private val options: CollectOptions, + val logLevel: LogLevel, + private val endpoint: String = "insert", + private val cvvMap: CVVMap = CVVMap.EMPTY +) : Skyflow.Callback { + private val okHttpClient = apiClient.okHttpClient + private val tag = FlowDBCollectAPICallback::class.qualifiedName + + override fun onSuccess(responseBody: Any) { + try { + Logger.info(tag, Messages.VALIDATE_RECORDS.getMessage(), logLevel) + val body = requestBody.toString().toRequestBody("application/json".toMediaTypeOrNull()) + val metrics = Utils.fetchMetrics() + val url = "${apiClient.vaultURL.trimEnd('/')}/v2/records/$endpoint" + val request = Request.Builder() + .method("POST", body) + .addHeader("Authorization", "$responseBody") + .addHeader("sky-metadata", "$metrics") + .url(url) + .build() + sendRequest(request) + } catch (e: Exception) { + callback.onFailure(Utils.constructErrorResponse(e)) + } + } + + override fun onFailure(exception: Any) { + callback.onFailure(exception) + } + + private fun sendRequest(request: Request) { + okHttpClient.newCall(request).enqueue(object : okhttp3.Callback { + override fun onFailure(call: okhttp3.Call, e: IOException) { + callback.onFailure(Utils.constructErrorResponse(e, 500)) + } + + override fun onResponse(call: okhttp3.Call, response: Response) { + verifyResponse(response) + } + }) + } + + private fun verifyResponse(response: Response) { + response.use { + try { + val bodyStr = response.body?.string() ?: "" + if (!response.isSuccessful) { + val parsed = try { JSONObject(bodyStr) } catch (e: Exception) { null } + if (parsed != null && parsed.has("records")) { + dispatchResponse(bodyStr) + return + } + val message = try { + parsed?.getJSONObject("error")?.getString("message") ?: bodyStr + } catch (e: JSONException) { bodyStr } + val requestId = response.headers["x-request-id"] ?: "" + callback.onFailure(Utils.constructErrorResponse(response.code, Utils.appendRequestId(message, requestId))) + return + } + dispatchResponse(bodyStr) + } catch (e: Exception) { + callback.onFailure(Utils.constructErrorResponse(e, 500)) + } + } + } + + private fun dispatchResponse(bodyStr: String) { + val responseJson = JSONObject(bodyStr) + val records = responseJson.optJSONArray("records") ?: JSONArray() + val allRecords = JSONArray() + val requestRecords = requestBody.optJSONArray("records") + val topLevelTableName = requestBody.optString("tableName", "") + val requestTableNames: List = if (topLevelTableName.isNotEmpty()) { + // All records in this request belong to one table + List(requestRecords?.length() ?: 1) { topLevelTableName } + } else { + (0 until (requestRecords?.length() ?: 0)).map { i -> + requestRecords?.optJSONObject(i)?.optString("tableName", "") ?: "" + } + } + + for (i in 0 until records.length()) { + val record = records.getJSONObject(i) + val httpCode = record.optInt("httpCode", 200) + val fallbackTableName = requestTableNames.getOrElse(i) { "" } + val tableName = record.optString("tableName", "").ifEmpty { fallbackTableName } + + if (httpCode != 200) { + allRecords.put( + JSONObject() + .put("error", record.optString("error", "")) + .put("skyflowId", record.opt("skyflowID") ?: record.opt("skyflowId")) + .put("tableName", tableName) + .put("httpCode", httpCode) + ) + continue + } + + val skyflowId = record.optString("skyflowID", "").ifEmpty { record.optString("skyflowId", "") } + val fieldsObject = JSONObject() + + val tokensObj = record.optJSONObject("tokens") + if (tokensObj != null) { + val fieldNames = tokensObj.keys() + while (fieldNames.hasNext()) { + val fieldName = fieldNames.next() + fieldsObject.put(fieldName, tokensObj.getJSONArray(fieldName)) + } + } + + // 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) + .put("tokens", fieldsObject) + .put("httpCode", httpCode) + val hashedData = record.optJSONObject("hashedData") + if (hashedData != null) resultRecord.put("hashedData", hashedData) + allRecords.put(resultRecord) + } + + callback.onSuccess(JSONObject().put("records", allRecords)) + } +} diff --git a/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBCollectRequestBody.kt b/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBCollectRequestBody.kt new file mode 100644 index 0000000..ffc5eba --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBCollectRequestBody.kt @@ -0,0 +1,138 @@ +package Skyflow.collect.client + +import Skyflow.* +import Skyflow.collect.elements.validations.ElementValueMatchRule +import org.json.JSONArray +import org.json.JSONObject + +internal class FlowDBCollectRequestBody { + companion object { + private val tag = FlowDBCollectRequestBody::class.qualifiedName + + internal fun buildRequestBody( + vaultID: String, + elements: MutableList, + options: CollectOptions, + logLevel: LogLevel + ): JSONObject { + val tableMap = groupByTable(elements, logLevel) + val upsertByTable = options.upsert?.associateBy { it.tableName } ?: emptyMap() + + // 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, anyToJsonValue(v))) } + } + + val recordsArray = JSONArray() + for ((tableName, columns) in tableMap) { + val dataObject = JSONObject() + for (record in columns) { + createJSONKey(dataObject, record.columnName, record.value) + } + val recordObj = JSONObject().put("data", dataObject).put("tableName", tableName) + val upsertOpt = upsertByTable[tableName] + if (upsertOpt != null) { + recordObj.put("upsert", JSONObject() + .put("updateType", upsertOpt.updateType.name) + .put("uniqueColumns", JSONArray(upsertOpt.uniqueColumns))) + } + recordsArray.put(recordObj) + } + return JSONObject().put("vaultID", vaultID).put("records", recordsArray) + } + + internal fun buildUpdateRequestBody( + vaultID: String, + tableName: String, + elements: MutableList, + skyflowID: String, + logLevel: LogLevel + ): JSONObject { + val tableMap = groupByTable(elements, logLevel) + val tableElements = tableMap[tableName] ?: mutableListOf() + val dataObject = JSONObject() + for (record in tableElements) { + createJSONKey(dataObject, record.columnName, record.value) + } + return JSONObject() + .put("vaultID", vaultID) + .put("tableName", tableName) + .put("records", JSONArray().put(JSONObject().put("skyflowID", skyflowID).put("data", dataObject))) + } + + internal fun buildUpdateRequestBodyFromMap( + vaultID: String, + tableName: String, + data: Map, + skyflowID: String + ): JSONObject { + val dataObject = JSONObject() + data.forEach { (k, v) -> dataObject.put(k, v) } + return JSONObject() + .put("vaultID", vaultID) + .put("tableName", tableName) + .put("records", JSONArray().put(JSONObject().put("skyflowID", skyflowID).put("data", dataObject))) + } + + private fun groupByTable( + elements: MutableList, + logLevel: LogLevel + ): LinkedHashMap> { + val tableMap = LinkedHashMap>() + val tableWithColumn = HashSet() + for (element in elements) { + val tableName = element.tableName + if (tableMap[tableName] != null) { + if (tableWithColumn.contains(tableName + element.columnName)) { + var hasElementValueMatchRule = false + for (validation in element.collectInput.validations.rules) { + if (validation is ElementValueMatchRule) { + hasElementValueMatchRule = true + break + } + } + if (!hasElementValueMatchRule) + throw SkyflowInternalError( + SkyflowErrorCode.DUPLICATE_COLUMN_FOUND, tag, logLevel, + arrayOf(tableName, element.columnName) + ) + continue + } + tableWithColumn.add(tableName + element.columnName) + tableMap[tableName]!!.add(CollectRequestRecord(element.columnName, element.getValue())) + } else { + tableWithColumn.add(tableName + element.columnName) + tableMap[tableName] = mutableListOf(CollectRequestRecord(element.columnName, element.getValue())) + } + } + 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])) { + if (keys.size > 1) { + createJSONKey(obj.get(keys[0]) as JSONObject, keys.drop(1).joinToString("."), value) + } + } else { + if (keys.size > 1) { + val tempObject = JSONObject() + obj.put(keys[0], tempObject) + createJSONKey(tempObject, keys.drop(1).joinToString("."), value) + } else { + obj.put(keys[0], value) + } + } + } + } +} diff --git a/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBMixedAPICallback.kt b/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBMixedAPICallback.kt new file mode 100644 index 0000000..7ab5db4 --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/collect/client/FlowDBMixedAPICallback.kt @@ -0,0 +1,63 @@ +package Skyflow.collect.client + +import Skyflow.Callback +import Skyflow.CollectOptions +import Skyflow.LogLevel +import Skyflow.core.FlowDBAPIClient +import org.json.JSONArray +import org.json.JSONObject + +internal class FlowDBMixedAPICallback( + private val apiClient: FlowDBAPIClient, + private val updateBody: JSONObject?, + private val insertBody: JSONObject?, + private val finalCallback: Callback, + private val options: CollectOptions, + 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) + private var completedCalls = 0 + private val allRecords = JSONArray() + + override fun onSuccess(responseBody: Any) { + val token = responseBody.toString() + updateBody?.let { body -> + FlowDBCollectAPICallback(apiClient, body, makeSubCallback(), options, logLevel, "update", cvvMap) + .onSuccess(token) + } + insertBody?.let { body -> + FlowDBCollectAPICallback(apiClient, body, makeSubCallback(), options, logLevel, "insert", cvvMap) + .onSuccess(token) + } + } + + override fun onFailure(exception: Any) { + finalCallback.onFailure(exception) + } + + private fun makeSubCallback(): Callback = object : Callback { + override fun onSuccess(responseBody: Any) { + synchronized(this@FlowDBMixedAPICallback) { + val json = JSONObject(responseBody.toString()) + mergeInto(allRecords, json.optJSONArray("records")) + completedCalls++ + if (completedCalls == totalCalls) dispatch() + } + } + + override fun onFailure(exception: Any) { + finalCallback.onFailure(exception) + } + } + + private fun mergeInto(target: JSONArray, source: JSONArray?) { + source ?: return + for (i in 0 until source.length()) target.put(source[i]) + } + + private fun dispatch() { + finalCallback.onSuccess(JSONObject().put("records", allRecords)) + } +} 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..5c6254d --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/collect/client/MockCVV.kt @@ -0,0 +1,126 @@ +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() + 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 + columns[element.columnName] = element.getValue() + } + 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. + * + * 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 + * 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 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 + 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/collect/client/UpdateAPICallback.kt b/Skyflow/src/main/kotlin/Skyflow/collect/client/UpdateAPICallback.kt index 8c3f9b5..dba8eb6 100644 --- a/Skyflow/src/main/kotlin/Skyflow/collect/client/UpdateAPICallback.kt +++ b/Skyflow/src/main/kotlin/Skyflow/collect/client/UpdateAPICallback.kt @@ -39,7 +39,7 @@ internal class UpdateAPICallback( if (e is SkyflowError) callback.onFailure(e) else { - val skyflowError = SkyflowError( + val skyflowError = SkyflowInternalError( SkyflowErrorCode.UNKNOWN_ERROR, tag = tag, logLevel = apiClient.logLevel, @@ -65,7 +65,7 @@ internal class UpdateAPICallback( val requestBody = JSONObject() requestBody.put("record", recordObject) - requestBody.put("tokenization", options.tokens) + requestBody.put("tokenization", true) val body: RequestBody = requestBody.toString().toRequestBody("application/json".toMediaTypeOrNull()) @@ -124,23 +124,17 @@ internal class UpdateAPICallback( val recordObject = JSONObject() recordObject.put("table", updateRecord.table) - if (options.tokens) { - val fieldsObject = JSONObject() - fieldsObject.put("skyflow_id", responseJson.getString("skyflow_id")) - - if (responseJson.has("tokens")) { - val tokens = responseJson.getJSONObject("tokens") - val tokenKeys = tokens.keys() - while (tokenKeys.hasNext()) { - val key = tokenKeys.next() - fieldsObject.put(key, tokens.getString(key)) - } + val fieldsObject = JSONObject() + fieldsObject.put("skyflow_id", responseJson.getString("skyflow_id")) + if (responseJson.has("tokens")) { + val tokens = responseJson.getJSONObject("tokens") + val tokenKeys = tokens.keys() + while (tokenKeys.hasNext()) { + val key = tokenKeys.next() + fieldsObject.put(key, tokens.getString(key)) } - - recordObject.put("fields", fieldsObject) - } else { - recordObject.put("skyflow_id", responseJson.getString("skyflow_id")) } + recordObject.put("fields", fieldsObject) responses.add(recordObject) } else { diff --git a/Skyflow/src/main/kotlin/Skyflow/composable/ComposableContainer.kt b/Skyflow/src/main/kotlin/Skyflow/composable/ComposableContainer.kt index 35d2b10..74609d6 100644 --- a/Skyflow/src/main/kotlin/Skyflow/composable/ComposableContainer.kt +++ b/Skyflow/src/main/kotlin/Skyflow/composable/ComposableContainer.kt @@ -1,7 +1,10 @@ package Skyflow.composable import Skyflow.* -import Skyflow.collect.client.CollectRequestBody +import Skyflow.collect.client.CVVMap +import Skyflow.collect.client.FlowDBCollectRequestBody +import Skyflow.collect.client.FlowDBMixedAPICallback +import org.json.JSONArray import Skyflow.core.Logger import Skyflow.core.Messages import Skyflow.core.getMessage @@ -75,7 +78,7 @@ fun Container.on(eventName: EventName, handler: (() -> Unit fun Container.getComposableLayout(): LinearLayout { if (collectElements.size != totalComposableElements) { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.MISMATCH_ELEMENT_COUNT_LAYOUT_SUM, tag, configuration.options.logLevel @@ -90,7 +93,7 @@ fun Container.collect( options: CollectOptions? = CollectOptions() ) { try { - Utils.checkVaultDetails(client.configuration) + validateVaultConfig() Logger.info( tag, Messages.VALIDATE_COLLECT_RECORDS.getMessage(), @@ -103,13 +106,37 @@ fun Container.collect( } } +fun Container.collect(callback: CollectCallback, options: CollectOptions? = CollectOptions()) { + val adapter = object : Callback { + override fun onSuccess(responseBody: Any) { + callback.onSuccess(CollectResponse.fromJson(responseBody.toString())) + } + override fun onFailure(exception: Any) { + callback.onFailure(SkyflowError.fromJson(exception.toString())) + } + } + collect(adapter, options) +} + +private fun Container.validateVaultConfig() { + if (configuration.vaultID.isEmpty()) { + throw SkyflowInternalError(SkyflowErrorCode.EMPTY_VAULT_ID, tag, configuration.options.logLevel) + } + if (configuration.vaultURL.isEmpty()) { + throw SkyflowInternalError(SkyflowErrorCode.EMPTY_VAULT_URL, tag, configuration.options.logLevel) + } + if (!Utils.checkUrl(configuration.vaultURL)) { + throw SkyflowInternalError(SkyflowErrorCode.INVALID_VAULT_URL, tag, configuration.options.logLevel) + } +} + private fun Container.validateElements() { var errors = "" for (element in this.collectElements) { errors = validateElement(element, errors) } if (errors != "") { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.INVALID_INPUT, tag, configuration.options.logLevel, arrayOf(errors) ) @@ -122,7 +149,7 @@ private fun Container.validateElement( ): String { var errorOnElement = err if (!element.isAttachedToWindow) { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, configuration.options.logLevel, @@ -130,8 +157,8 @@ private fun Container.validateElement( ) } when { - element.collectInput.table.equals(null) -> { - throw SkyflowError( + element.collectInput.tableName.equals(null) -> { + throw SkyflowInternalError( SkyflowErrorCode.MISSING_TABLE_IN_ELEMENT, tag, configuration.options.logLevel, @@ -139,15 +166,15 @@ private fun Container.validateElement( ) } element.collectInput.column.equals(null) -> { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.MISSING_COLUMN, tag, configuration.options.logLevel, arrayOf(element.fieldType.toString()) ) } - element.collectInput.table!!.isEmpty() -> { - throw SkyflowError( + element.collectInput.tableName!!.isEmpty() -> { + throw SkyflowInternalError( SkyflowErrorCode.ELEMENT_EMPTY_TABLE_NAME, tag, configuration.options.logLevel, @@ -155,7 +182,7 @@ private fun Container.validateElement( ) } element.collectInput.column!!.isEmpty() -> { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.EMPTY_COLUMN_NAME, tag, configuration.options.logLevel, @@ -175,43 +202,88 @@ private fun Container.validateElement( } private fun Container.post(callback: Callback, options: CollectOptions?) { - // Separate insert and update elements/records (including additionalFields) - val (insertElements, insertAdditionalFields, updateRecords) = CollectRequestBody.separateInsertAndUpdateRecords( + val collectOptions = options ?: CollectOptions() + val updateElements = collectElements.filter { !it.skyflowId.isNullOrEmpty() } + val insertElements = collectElements.filter { it.skyflowId.isNullOrEmpty() } + + val additionalUpdates = collectOptions.additionalFields?.records + ?.filter { !it.skyflowId.isNullOrEmpty() } ?: emptyList() + val additionalInserts = collectOptions.additionalFields?.records + ?.filter { it.skyflowId.isNullOrEmpty() } ?: emptyList() + + if (updateElements.isNotEmpty() || additionalUpdates.isNotEmpty()) { + val updateRecordsArray = org.json.JSONArray() + + updateElements.groupBy { it.skyflowId }.forEach { (_, elements) -> + val tableName = elements.first().tableName + val skyflowID = elements.first().skyflowId!! + val singleBody = FlowDBCollectRequestBody.buildUpdateRequestBody( + configuration.vaultID, tableName, elements.toMutableList(), + skyflowID, configuration.options.logLevel + ) + val rec = singleBody.getJSONArray("records").getJSONObject(0) + rec.put("tableName", tableName) + updateRecordsArray.put(rec) + } + + additionalUpdates.groupBy { it.skyflowId }.forEach { (_, records) -> + val merged = records.fold(mutableMapOf()) { acc, r -> acc.also { it.putAll(r.data) } } + val dataObj = org.json.JSONObject().apply { merged.forEach { (k, v) -> put(k, v) } } + updateRecordsArray.put( + org.json.JSONObject() + .put("skyflowID", records.first().skyflowId!!) + .put("data", dataObj) + .put("tableName", records.first().tableName) + ) + } + + val combinedUpdateBody = JSONObject() + .put("vaultID", configuration.vaultID) + .put("records", updateRecordsArray) + + val insertBody: JSONObject? = if (insertElements.isNotEmpty() || additionalInserts.isNotEmpty()) { + val insertOptions = CollectOptions( + upsert = collectOptions.upsert?.filter { opt -> insertElements.any { it.tableName == opt.tableName } }, + additionalFields = if (additionalInserts.isEmpty()) null else AdditionalFields(additionalInserts) + ) + FlowDBCollectRequestBody.buildRequestBody( + configuration.vaultID, insertElements.toMutableList(), + insertOptions, configuration.options.logLevel + ) + } else null + + val mixedCallback = FlowDBMixedAPICallback( + client.apiClient, combinedUpdateBody, insertBody, callback, collectOptions, + configuration.options.logLevel, CVVMap.capture(collectElements) + ) + client.apiClient.getAccessToken(mixedCallback) + return + } + + val insertOptions = CollectOptions( + upsert = collectOptions.upsert, + additionalFields = if (additionalInserts.isEmpty()) null else AdditionalFields(additionalInserts) + ) + val requestBody = FlowDBCollectRequestBody.buildRequestBody( + configuration.vaultID, this.collectElements, - options?.additionalFields, + insertOptions, configuration.options.logLevel ) - - val hasInsertData = insertElements.isNotEmpty() || insertAdditionalFields != null - val hasUpdateRecords = updateRecords.isNotEmpty() - - if (hasInsertData && hasUpdateRecords) { - // Mixed case: both insert and update - val insertRecordsJson = if (insertElements.isNotEmpty()) { - JSONObject(CollectRequestBody.createRequestBody( - insertElements, - insertAdditionalFields, - configuration.options.logLevel - )) - } else { - insertAdditionalFields - } - - val insertOptions = InsertOptions(options?.token ?: true, options?.upsert) - this.client.apiClient.postWithUpdate(insertRecordsJson, updateRecords, callback, insertOptions) - } else if (hasUpdateRecords) { - // Only update records - val insertOptions = InsertOptions(options?.token ?: true, options?.upsert) - this.client.apiClient.postWithUpdate(null, updateRecords, callback, insertOptions) - } else { - // Only insert records - val records = CollectRequestBody.createRequestBody( - this.collectElements, - insertAdditionalFields, + this.client.apiClient.post(requestBody, callback, collectOptions, cvvMap = CVVMap.capture(this.collectElements)) +} + +fun Container.update(tableName: String, skyflowID: String, callback: Callback, options: CollectOptions = CollectOptions()) { + try { + validateVaultConfig() + val requestBody = FlowDBCollectRequestBody.buildUpdateRequestBody( + configuration.vaultID, tableName, this.collectElements, skyflowID, configuration.options.logLevel ) - val insertOptions = InsertOptions(options?.token ?: true, options?.upsert) - this.client.apiClient.post(JSONObject(records), callback, insertOptions) + 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/APIClient.kt b/Skyflow/src/main/kotlin/Skyflow/core/APIClient.kt index f3b1e46..b1c213d 100644 --- a/Skyflow/src/main/kotlin/Skyflow/core/APIClient.kt +++ b/Skyflow/src/main/kotlin/Skyflow/core/APIClient.kt @@ -26,7 +26,6 @@ object JWTUtils { val split = JWTEncoded.split(".").toTypedArray() JSONObject(getJson(split[1])) } catch (e: UnsupportedEncodingException) { - println(e.toString()) JSONObject() } } @@ -70,7 +69,7 @@ internal class APIClient( Logger.info(tag, Messages.BEARER_TOKEN_RECEIVED.getMessage(), logLevel) if (!isValidToken(responseBody.toString())) { val error = - SkyflowError(SkyflowErrorCode.INVALID_BEARER_TOKEN, tag, logLevel) + SkyflowInternalError(SkyflowErrorCode.INVALID_BEARER_TOKEN, tag, logLevel) callback.onFailure(error) } else { token = "Bearer $responseBody" @@ -85,7 +84,7 @@ internal class APIClient( logLevel ) val error = - SkyflowError(SkyflowErrorCode.BEARER_TOKEN_REJECTED, tag, logLevel) + SkyflowInternalError(SkyflowErrorCode.BEARER_TOKEN_REJECTED, tag, logLevel) callback.onFailure(error) } }) @@ -93,7 +92,7 @@ internal class APIClient( callback.onSuccess(token) } } catch (e: Exception) { - val error = SkyflowError(SkyflowErrorCode.INVALID_BEARER_TOKEN, tag, logLevel) + val error = SkyflowInternalError(SkyflowErrorCode.INVALID_BEARER_TOKEN, tag, logLevel) callback.onFailure(error) } } @@ -181,13 +180,13 @@ internal class APIClient( fun constructBodyForDetokenize(records: JSONObject): MutableList { if (!records.has("records")) { - throw SkyflowError(SkyflowErrorCode.RECORDS_KEY_NOT_FOUND, tag, logLevel) + throw SkyflowInternalError(SkyflowErrorCode.RECORDS_KEY_NOT_FOUND, tag, logLevel) } if (records.get("records") !is JSONArray) { - throw SkyflowError(SkyflowErrorCode.INVALID_RECORDS, tag, logLevel) + throw SkyflowInternalError(SkyflowErrorCode.INVALID_RECORDS, tag, logLevel) } if (records.getJSONArray("records").length() == 0) { - throw SkyflowError(SkyflowErrorCode.EMPTY_RECORDS, tag, logLevel) + throw SkyflowInternalError(SkyflowErrorCode.EMPTY_RECORDS, tag, logLevel) } val jsonArray = records.getJSONArray("records") val list = mutableListOf() @@ -195,21 +194,21 @@ internal class APIClient( while (i < jsonArray.length()) { val recordObject = jsonArray.getJSONObject(i) if (!recordObject.has("token")) { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.TOKEN_KEY_NOT_FOUND, tag, logLevel, arrayOf("$i") ) } else if (recordObject.get("token").toString().isEmpty()) { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.EMPTY_TOKEN, tag, logLevel, arrayOf("$i") ) } else if (recordObject.has("redaction")) { val redaction = recordObject.get("redaction") if (redaction.toString().isEmpty()) { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.EMPTY_REDACTION_VALUE, tag, logLevel, arrayOf("$i") ) } else if (redaction !is RedactionType) { - throw SkyflowError( + throw SkyflowInternalError( SkyflowErrorCode.INVALID_REDACTION_TYPE, tag, logLevel, arrayOf("$i") ) } else { diff --git a/Skyflow/src/main/kotlin/Skyflow/core/ConnectionApiCallback.kt b/Skyflow/src/main/kotlin/Skyflow/core/ConnectionApiCallback.kt index 8481263..b89f3bd 100644 --- a/Skyflow/src/main/kotlin/Skyflow/core/ConnectionApiCallback.kt +++ b/Skyflow/src/main/kotlin/Skyflow/core/ConnectionApiCallback.kt @@ -73,7 +73,7 @@ internal class ConnectionApiCallback( sendRequest(requestBuild) } catch (e:Exception){ - callback.onFailure(Utils.constructError(SkyflowError(SkyflowErrorCode.UNKNOWN_ERROR, tag, logLevel, params = arrayOf(e.message)))) + callback.onFailure(Utils.constructError(SkyflowInternalError(SkyflowErrorCode.UNKNOWN_ERROR, tag, logLevel, params = arrayOf(e.message)))) } } override fun onFailure(exception: Any) { @@ -88,7 +88,7 @@ internal class ConnectionApiCallback( else tokens = error.getString("token") } - callback.onFailure(Utils.constructError(SkyflowError(SkyflowErrorCode.NOT_VALID_TOKENS, tag, logLevel, params = arrayOf(tokens)))) + callback.onFailure(Utils.constructError(SkyflowInternalError(SkyflowErrorCode.NOT_VALID_TOKENS, tag, logLevel, params = arrayOf(tokens)))) } catch (e:Exception){ callback.onFailure(exception) @@ -139,7 +139,7 @@ internal class ConnectionApiCallback( fun getRequestBuild(responseBody: Any, requestBody: JSONObject): Request? { //create requestBuild val requestUrlBuilder = connectionUrl.toHttpUrlOrNull()?.newBuilder() if(requestUrlBuilder == null){ - val error = SkyflowError(SkyflowErrorCode.INVALID_CONNECTION_URL, + val error = SkyflowInternalError(SkyflowErrorCode.INVALID_CONNECTION_URL, tag, logLevel, arrayOf(connectionConfig.connectionURL)) callback.onFailure(Utils.constructError(error)) return null @@ -203,13 +203,13 @@ internal class ConnectionApiCallback( } else { - val skyflowError = SkyflowError(SkyflowErrorCode.BAD_REQUEST, tag = tag, logLevel = logLevel) + val skyflowError = SkyflowInternalError(SkyflowErrorCode.BAD_REQUEST, tag = tag, logLevel = logLevel) callback.onFailure(Utils.constructError(skyflowError, response.code)) } } catch (e:Exception) { - val skyflowError = SkyflowError(SkyflowErrorCode.UNKNOWN_ERROR, tag = tag, logLevel = logLevel, arrayOf(e.message.toString())) + val skyflowError = SkyflowInternalError(SkyflowErrorCode.UNKNOWN_ERROR, tag = tag, logLevel = logLevel, arrayOf(e.message.toString())) skyflowError.setErrorCode(400) callback.onFailure(Utils.constructError(skyflowError, response.code)) } @@ -223,23 +223,23 @@ internal class ConnectionApiCallback( for (j in 0 until keys.length()) { if(keys.getString(j).isEmpty()) { - throw SkyflowError(SkyflowErrorCode.EMPTY_KEY_IN_REQUEST_BODY, + throw SkyflowInternalError(SkyflowErrorCode.EMPTY_KEY_IN_REQUEST_BODY, tag, logLevel) } var value: Any if (records.get(keys.getString(j)) is Element) { val element = (records.get(keys.getString(j)) as Element) if(!Utils.checkIfElementsMounted(element)) - throw SkyflowError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) + throw SkyflowInternalError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) checkForValidElement(element) value = (records.get(keys.getString(j)) as Element).getValue() } else if (records.get(keys.getString(j)) is Label) { if(!Utils.checkIfElementsMounted(records.get(keys.getString(j)) as Label)) - throw SkyflowError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) + throw SkyflowInternalError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) else if ((records.get(keys.getString(j)) as Label).isTokenNull) - throw SkyflowError(SkyflowErrorCode.MISSING_TOKEN_IN_CONNECTION_REQUEST, tag, logLevel, arrayOf(keys.getString(j))) + throw SkyflowInternalError(SkyflowErrorCode.MISSING_TOKEN_IN_CONNECTION_REQUEST, tag, logLevel, arrayOf(keys.getString(j))) else if ((records.get(keys.getString(j)) as Label).revealInput.token!!.isEmpty()) - throw SkyflowError(SkyflowErrorCode.EMPTY_TOKEN_ID, tag, logLevel) + throw SkyflowInternalError(SkyflowErrorCode.EMPTY_TOKEN_ID, tag, logLevel) value = Utils.getValueForLabel(records.get(keys.getString(j)) as Label,tokenValueMap,tokenIdMap,tokenLabelMap,tag,logLevel) } else if (records.get(keys.getString(j)) is JSONObject) { constructRequestBodyForConnection(records.get(keys.getString(j)) as JSONObject) @@ -258,18 +258,18 @@ internal class ConnectionApiCallback( { val element = (arrayValue.get(k) as Element) if(!Utils.checkIfElementsMounted(element)) - throw SkyflowError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) + throw SkyflowInternalError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) checkForValidElement(element) value = (arrayValue.get(k) as Element).getValue() } else if(arrayValue.get(k) is Label) { if(!Utils.checkIfElementsMounted(arrayValue.get(k) as Label)) - throw SkyflowError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) + throw SkyflowInternalError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) else if ((arrayValue.get(k) as Label).isTokenNull) - throw SkyflowError(SkyflowErrorCode.MISSING_TOKEN, tag, logLevel) + throw SkyflowInternalError(SkyflowErrorCode.MISSING_TOKEN, tag, logLevel) else if ((arrayValue.get(k) as Label).revealInput.token!!.isEmpty()) - throw SkyflowError(SkyflowErrorCode.EMPTY_TOKEN_ID, tag, logLevel) + throw SkyflowInternalError(SkyflowErrorCode.EMPTY_TOKEN_ID, tag, logLevel) value = Utils.getValueForLabel(arrayValue.get(k) as Label,tokenValueMap,tokenIdMap,tokenLabelMap,tag,logLevel) } else if(arrayValue.get(k) is JSONObject) @@ -280,7 +280,7 @@ internal class ConnectionApiCallback( else if(arrayValue.get(k) is String || arrayValue.get(k) is Number || arrayValue.get(k) is Boolean) value = arrayValue.get(k) .toString() else - throw SkyflowError(SkyflowErrorCode.INVALID_FIELD_IN_REQUEST_BODY, tag, logLevel, arrayOf(keys.getString(j))) + throw SkyflowInternalError(SkyflowErrorCode.INVALID_FIELD_IN_REQUEST_BODY, tag, logLevel, arrayOf(keys.getString(j))) arrayValue.put(k,value) } value = arrayValue @@ -294,7 +294,7 @@ internal class ConnectionApiCallback( { val element = (arrayValue[k] as Element) if(!Utils.checkIfElementsMounted(element)) - throw SkyflowError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) + throw SkyflowInternalError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) checkForValidElement(element) value = (arrayValue[k] as Element).getValue() } @@ -302,11 +302,11 @@ internal class ConnectionApiCallback( { if(!Utils.checkIfElementsMounted(arrayValue[k] as Label)) - throw SkyflowError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) + throw SkyflowInternalError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) else if ((arrayValue[k] as Label).isTokenNull) - throw SkyflowError(SkyflowErrorCode.MISSING_TOKEN, tag, logLevel) + throw SkyflowInternalError(SkyflowErrorCode.MISSING_TOKEN, tag, logLevel) else if ((arrayValue[k] as Label).revealInput.token!!.isEmpty()) - throw SkyflowError(SkyflowErrorCode.EMPTY_TOKEN_ID, tag, logLevel) + throw SkyflowInternalError(SkyflowErrorCode.EMPTY_TOKEN_ID, tag, logLevel) else value = Utils.getValueForLabel(arrayValue[k] as Label,tokenValueMap,tokenIdMap,tokenLabelMap,tag,logLevel) } @@ -318,7 +318,7 @@ internal class ConnectionApiCallback( else if(arrayValue[k] is String || arrayValue[k] is Number || arrayValue[k] is Boolean) value = arrayValue[k].toString() else - throw SkyflowError(SkyflowErrorCode.INVALID_FIELD_IN_REQUEST_BODY, tag, logLevel, arrayOf(keys.getString(j))) + throw SkyflowInternalError(SkyflowErrorCode.INVALID_FIELD_IN_REQUEST_BODY, tag, logLevel, arrayOf(keys.getString(j))) arrayInRequestBody.put(k,value) } value = arrayInRequestBody @@ -327,7 +327,7 @@ internal class ConnectionApiCallback( else if (records.get(keys.getString(j)) is String || records.get(keys.getString(j)) is Number || records.get(keys.getString(j)) is Boolean) value = records.get(keys.getString(j)).toString() else { - throw SkyflowError(SkyflowErrorCode.INVALID_FIELD_IN_REQUEST_BODY, tag, logLevel, arrayOf(keys.getString(j))) + throw SkyflowInternalError(SkyflowErrorCode.INVALID_FIELD_IN_REQUEST_BODY, tag, logLevel, arrayOf(keys.getString(j))) } records.put(keys.getString(j), value) } } @@ -342,7 +342,7 @@ internal class ConnectionApiCallback( if(headers.getString(i).isEmpty()) { - throw SkyflowError(SkyflowErrorCode.EMPTY_KEY_IN_REQUEST_HEADER_PARAMS) //empty key + throw SkyflowInternalError(SkyflowErrorCode.EMPTY_KEY_IN_REQUEST_HEADER_PARAMS) //empty key } if(headers.getString(i).lowercase(Locale.getDefault()).equals("content-type")) { if(connectionConfig.requestHeader.get(headers.getString(i)) is ContentType) { @@ -358,7 +358,7 @@ internal class ConnectionApiCallback( else { //callback.onFailure(Exception("invalid field \"${headers.getString(i)}\" present in requestHeader")) - val skyflowError = SkyflowError(SkyflowErrorCode.INVALID_FIELD_IN_REQUEST_HEADER_PARAMS, tag, logLevel, arrayOf(headers.getString(i))) + val skyflowError = SkyflowInternalError(SkyflowErrorCode.INVALID_FIELD_IN_REQUEST_HEADER_PARAMS, tag, logLevel, arrayOf(headers.getString(i))) throw skyflowError } } @@ -371,7 +371,7 @@ internal class ConnectionApiCallback( for (i in 0 until queryParams.length()) { if(queryParams.getString(i).isEmpty()) { - throw SkyflowError(SkyflowErrorCode.EMPTY_KEY_IN_QUERY_PARAMS)//empty key + throw SkyflowInternalError(SkyflowErrorCode.EMPTY_KEY_IN_QUERY_PARAMS)//empty key } val value = connectionConfig.queryParams.get(queryParams.getString(i)) if(value is Array<*>) @@ -396,7 +396,7 @@ internal class ConnectionApiCallback( { if(!Utils.checkIfElementsMounted(value)) { - throw SkyflowError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(key)) + throw SkyflowInternalError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(key)) } checkForValidElement(value) queryMap.put(key, value.getValue()) @@ -405,13 +405,13 @@ internal class ConnectionApiCallback( { if(!Utils.checkIfElementsMounted(value)) { - throw SkyflowError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(key)) + throw SkyflowInternalError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(key)) } else if (value.isTokenNull) { - val error = SkyflowError(SkyflowErrorCode.MISSING_TOKEN, tag, logLevel) + val error = SkyflowInternalError(SkyflowErrorCode.MISSING_TOKEN, tag, logLevel) throw error } else if (value.revealInput.token!!.isEmpty()) { - val error = SkyflowError(SkyflowErrorCode.EMPTY_TOKEN_ID, tag, logLevel) + val error = SkyflowInternalError(SkyflowErrorCode.EMPTY_TOKEN_ID, tag, logLevel) throw error } queryMap.put(key,Utils.getValueForLabel(value,tokenValueMap,tokenIdMap,tokenLabelMap,tag,logLevel)) @@ -422,7 +422,7 @@ internal class ConnectionApiCallback( } else { //callback.onFailure(Exception("invalid field \"${key}\" present in queryParams")) - val skyflowError = SkyflowError(SkyflowErrorCode.INVALID_FIELD_IN_QUERY_PARAMS, tag, logLevel, arrayOf(key)) + val skyflowError = SkyflowInternalError(SkyflowErrorCode.INVALID_FIELD_IN_QUERY_PARAMS, tag, logLevel, arrayOf(key)) throw skyflowError } } @@ -436,14 +436,14 @@ internal class ConnectionApiCallback( for (j in 0 until keys.length()) { if(keys.getString(j).isEmpty()) { - throw SkyflowError(SkyflowErrorCode.EMPTY_KEY_IN_PATH_PARAMS, tag, logLevel) //empty key + throw SkyflowInternalError(SkyflowErrorCode.EMPTY_KEY_IN_PATH_PARAMS, tag, logLevel) //empty key } var value = params.get(keys.getString(j)) if (value is Element) { val element = value if(!Utils.checkIfElementsMounted(element)) { - val error = SkyflowError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) + val error = SkyflowInternalError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) throw error } checkForValidElement(element) @@ -451,14 +451,14 @@ internal class ConnectionApiCallback( } else if (value is Label) { if(!Utils.checkIfElementsMounted(value)) { - val error = SkyflowError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) + val error = SkyflowInternalError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, tag, logLevel, arrayOf(keys.getString(j))) throw error } else if (value.isTokenNull) { - val error = SkyflowError(SkyflowErrorCode.MISSING_TOKEN, tag, logLevel) + val error = SkyflowInternalError(SkyflowErrorCode.MISSING_TOKEN, tag, logLevel) throw error } else if (value.revealInput.token!!.isEmpty()) { - val error = SkyflowError(SkyflowErrorCode.EMPTY_TOKEN_ID, tag, logLevel) + val error = SkyflowInternalError(SkyflowErrorCode.EMPTY_TOKEN_ID, tag, logLevel) throw error } else @@ -468,7 +468,7 @@ internal class ConnectionApiCallback( value = value.toString() newURL = newURL.replace("{" + keys.getString(j) + "}", value) } else { - val skyflowError = SkyflowError(SkyflowErrorCode.INVALID_FIELD_IN_PATH_PARAMS, tag, logLevel, arrayOf(keys.getString(j))) + val skyflowError = SkyflowInternalError(SkyflowErrorCode.INVALID_FIELD_IN_PATH_PARAMS, tag, logLevel, arrayOf(keys.getString(j))) throw skyflowError } } @@ -492,7 +492,7 @@ internal class ConnectionApiCallback( errors = "for " + labelName + " " + (state["validationError"] as String) + "\n" } if (errors != "") { - val error = SkyflowError(SkyflowErrorCode.INVALID_INPUT, tag, logLevel, arrayOf(errors)) + val error = SkyflowInternalError(SkyflowErrorCode.INVALID_INPUT, tag, logLevel, arrayOf(errors)) throw error } } @@ -510,13 +510,13 @@ internal class ConnectionApiCallback( { val element = (responseBody.get(keys.getString(j))) as Element if(!Utils.checkIfElementsMounted(element)) - throw SkyflowError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, Utils.tag, logLevel, arrayOf(keys.getString(j))) + throw SkyflowInternalError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, Utils.tag, logLevel, arrayOf(keys.getString(j))) } else if(responseBody.get(keys.getString(j)) is Label) { val element = (responseBody.get(keys.getString(j))) as Label if(!Utils.checkIfElementsMounted(element)) - throw SkyflowError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, Utils.tag, logLevel, arrayOf(keys.getString(j))) + throw SkyflowInternalError(SkyflowErrorCode.ELEMENT_NOT_MOUNTED, Utils.tag, logLevel, arrayOf(keys.getString(j))) } else if (responseBody.get(keys.getString(j)) is JSONObject) { validateResponseBody(responseBody.get(keys.getString(j)) as JSONObject, elementList) @@ -524,7 +524,7 @@ internal class ConnectionApiCallback( throw Exception("invalid field " + keys.getString(j) + " present in response body") if (responseBody.get(keys.getString(j)) is Element || responseBody.get(keys.getString(j)) is Label) { if (elementList.contains(responseBody.get(keys.getString(j)).hashCode().toString())) - throw SkyflowError(SkyflowErrorCode.DUPLICATE_ELEMENT_FOUND, Utils.tag,logLevel) + throw SkyflowInternalError(SkyflowErrorCode.DUPLICATE_ELEMENT_FOUND, Utils.tag,logLevel) else elementList.add(responseBody.get(keys.getString(j)).hashCode().toString()) } @@ -577,7 +577,7 @@ internal class ConnectionApiCallback( if(e is SkyflowError) throw e else - throw SkyflowError(SkyflowErrorCode.NOT_FOUND_IN_RESPONSE, + throw SkyflowInternalError(SkyflowErrorCode.NOT_FOUND_IN_RESPONSE, Utils.tag, logLevel, arrayOf(keys.getString(j))) } } } diff --git a/Skyflow/src/main/kotlin/Skyflow/core/FlowDBAPIClient.kt b/Skyflow/src/main/kotlin/Skyflow/core/FlowDBAPIClient.kt new file mode 100644 index 0000000..4e402f3 --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/core/FlowDBAPIClient.kt @@ -0,0 +1,73 @@ +package Skyflow.core + +import Skyflow.* +import Skyflow.collect.client.CVVMap +import Skyflow.collect.client.FlowDBCollectAPICallback +import Skyflow.reveal.FlowDBRevealApiCallback +import Skyflow.utils.Utils +import org.json.JSONObject + +internal class FlowDBAPIClient( + val vaultId: String, + val vaultURL: String, + private val tokenProvider: TokenProvider, + val logLevel: LogLevel, + private var token: String = "", + val okHttpClient: okhttp3.OkHttpClient = okhttp3.OkHttpClient() +) { + private val tag = FlowDBAPIClient::class.qualifiedName + + private fun isValidToken(token: String?): Boolean { + return if (token != "") !JWTUtils.isExpired(token!!) else false + } + + fun getAccessToken(callback: Callback) { + try { + if (!isValidToken(token)) { + Logger.info(tag, Messages.RETRIEVING_BEARER_TOKEN.getMessage(), logLevel) + tokenProvider.getBearerToken(object : Callback { + override fun onSuccess(responseBody: Any) { + Logger.info(tag, Messages.BEARER_TOKEN_RECEIVED.getMessage(), logLevel) + if (!isValidToken(responseBody.toString())) { + callback.onFailure(SkyflowInternalError(SkyflowErrorCode.INVALID_BEARER_TOKEN, tag, logLevel)) + } else { + token = "Bearer $responseBody" + callback.onSuccess(token) + } + } + + override fun onFailure(exception: Any) { + Logger.error(tag, Messages.RETRIEVING_BEARER_TOKEN_FAILED.getMessage(), logLevel) + callback.onFailure(SkyflowInternalError(SkyflowErrorCode.BEARER_TOKEN_REJECTED, tag, logLevel)) + } + }) + } else { + callback.onSuccess(token) + } + } catch (e: Exception) { + callback.onFailure(SkyflowInternalError(SkyflowErrorCode.INVALID_BEARER_TOKEN, tag, logLevel)) + } + } + + 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, cvvMap) + this.getAccessToken(collectApiCallback) + } catch (e: Exception) { + callback.onFailure(Utils.constructError(e)) + } + } + + fun get(requestBody: JSONObject, callback: Callback) { + try { + val tokensArray = requestBody.optJSONArray("tokens") + if (tokensArray == null || tokensArray.length() == 0) { + throw SkyflowInternalError(SkyflowErrorCode.EMPTY_RECORDS, tag, logLevel) + } + val revealApiCallback = FlowDBRevealApiCallback(callback, this, requestBody) + this.getAccessToken(revealApiCallback) + } catch (e: Exception) { + callback.onFailure(Utils.constructError(e)) + } + } +} diff --git a/Skyflow/src/main/kotlin/Skyflow/get/GetAPICallback.kt b/Skyflow/src/main/kotlin/Skyflow/get/GetAPICallback.kt index 5667cbb..707b6b7 100644 --- a/Skyflow/src/main/kotlin/Skyflow/get/GetAPICallback.kt +++ b/Skyflow/src/main/kotlin/Skyflow/get/GetAPICallback.kt @@ -3,6 +3,7 @@ package Skyflow.get import Skyflow.Callback import Skyflow.SkyflowError import Skyflow.SkyflowErrorCode +import Skyflow.SkyflowInternalError import Skyflow.core.APIClient import Skyflow.utils.Utils import okhttp3.Call @@ -39,7 +40,7 @@ internal class GetAPICallback( private fun buildRequest(responseBody: Any, record: GetRecord): Request { val url = "${apiClient.vaultURL}${apiClient.vaultId}/${record.table}" val requestUrlBuilder = url.toHttpUrlOrNull()?.newBuilder() - ?: throw SkyflowError( + ?: throw SkyflowInternalError( SkyflowErrorCode.INVALID_VAULT_URL, tag, apiClient.logLevel, arrayOf(apiClient.vaultURL) ) @@ -74,7 +75,7 @@ internal class GetAPICallback( private fun sendRequest(request: Request, record: GetRecord) { okHttpClient.newCall(request).enqueue(object : okhttp3.Callback { override fun onFailure(call: Call, e: IOException) { - val skyflowError = SkyflowError( + val skyflowError = SkyflowInternalError( SkyflowErrorCode.UNKNOWN_ERROR, tag = tag, logLevel = apiClient.logLevel, params = arrayOf(e.message.toString()) ) @@ -96,7 +97,7 @@ internal class GetAPICallback( try { val responseErrorBody = JSONObject(responseBody) val requestId = response.headers["x-request-id"].toString() - val skyflowError = SkyflowError( + val skyflowError = SkyflowInternalError( SkyflowErrorCode.SERVER_ERROR, tag, apiClient.logLevel, arrayOf( Utils.appendRequestId( @@ -109,7 +110,7 @@ internal class GetAPICallback( val responseObject = constructErrorResponseForGet(record, skyflowError) getResponse.insertResponse(JSONArray().put(responseObject), false) } catch (e: Exception) { - val skyflowError = SkyflowError( + val skyflowError = SkyflowInternalError( SkyflowErrorCode.SERVER_ERROR, tag, apiClient.logLevel, arrayOf(responseBody) ) @@ -132,14 +133,14 @@ internal class GetAPICallback( } getResponse.insertResponse(newJsonArray, true) } else { - val skyflowError = SkyflowError( + val skyflowError = SkyflowInternalError( SkyflowErrorCode.BAD_REQUEST, tag, apiClient.logLevel ) val responseObject = constructErrorResponseForGet(record, skyflowError) getResponse.insertResponse(JSONArray().put(responseObject), false) } } catch (e: Exception) { - val skyflowError = SkyflowError( + val skyflowError = SkyflowInternalError( SkyflowErrorCode.UNKNOWN_ERROR, tag, apiClient.logLevel, arrayOf(e.message.toString()) ) @@ -152,7 +153,7 @@ internal class GetAPICallback( private fun constructErrorResponseForGet( record: GetRecord, - skyflowError: SkyflowError + skyflowError: SkyflowInternalError ): JSONObject { val responseObject = JSONObject() responseObject.put("error", skyflowError) diff --git a/Skyflow/src/main/kotlin/Skyflow/get/GetResponse.kt b/Skyflow/src/main/kotlin/Skyflow/get/GetResponse.kt index 22bc0c7..5e59e36 100644 --- a/Skyflow/src/main/kotlin/Skyflow/get/GetResponse.kt +++ b/Skyflow/src/main/kotlin/Skyflow/get/GetResponse.kt @@ -4,6 +4,7 @@ import Skyflow.Callback import Skyflow.LogLevel import Skyflow.SkyflowError import Skyflow.SkyflowErrorCode +import Skyflow.SkyflowInternalError import Skyflow.utils.Utils import org.json.JSONArray import org.json.JSONObject @@ -39,7 +40,7 @@ internal class GetResponse( if (successResponses + failureResponses + emptyResponses == size) { if (successResponses + failureResponses == 0) { - val skyflowError = SkyflowError(SkyflowErrorCode.FAILED_TO_GET, tag, logLevel) + val skyflowError = SkyflowInternalError(SkyflowErrorCode.FAILED_TO_GET, tag, logLevel) callback.onFailure(Utils.constructError(skyflowError)) } else { if (failureResponses == 0) { diff --git a/Skyflow/src/main/kotlin/Skyflow/reveal/FlowDBRevealApiCallback.kt b/Skyflow/src/main/kotlin/Skyflow/reveal/FlowDBRevealApiCallback.kt new file mode 100644 index 0000000..e14c77b --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/reveal/FlowDBRevealApiCallback.kt @@ -0,0 +1,127 @@ +package Skyflow.reveal + +import Skyflow.Callback +import Skyflow.core.FlowDBAPIClient +import Skyflow.utils.Utils +import okhttp3.Call +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import java.io.IOException + +internal class FlowDBRevealApiCallback( + private val callback: Callback, + private val apiClient: FlowDBAPIClient, + private val requestBody: JSONObject +) : Callback { + private val tag = FlowDBRevealApiCallback::class.qualifiedName + private val okHttpClient = apiClient.okHttpClient + + override fun onSuccess(responseBody: Any) { + try { + val url = "${apiClient.vaultURL.trimEnd('/')}/v2/tokens/detokenize" + val body = requestBody.toString().toRequestBody("application/json".toMediaTypeOrNull()) + val metrics = Utils.fetchMetrics() + val request = Request.Builder() + .method("POST", body) + .addHeader("Authorization", "$responseBody") + .addHeader("sky-metadata", "$metrics") + .url(url) + .build() + sendRequest(request) + } catch (e: Exception) { + callback.onFailure(Utils.constructError(e)) + } + } + + override fun onFailure(exception: Any) { + callback.onFailure(exception) + } + + private fun sendRequest(request: Request) { + okHttpClient.newCall(request).enqueue(object : okhttp3.Callback { + override fun onFailure(call: Call, e: IOException) { + callback.onFailure(Utils.constructErrorResponse(e, 500)) + } + + override fun onResponse(call: Call, response: Response) { + verifyResponse(response) + } + }) + } + + private fun verifyResponse(response: Response) { + response.use { + try { + val bodyStr = response.body?.string() ?: "" + val responseJson = try { JSONObject(bodyStr) } catch (e: JSONException) { null } + + if (responseJson?.has("response") == true) { + parseAndDispatch(responseJson) + return + } + + // Whole-request failure (auth error, malformed request, etc.) + if (!response.isSuccessful) { + val message = try { + responseJson?.getJSONObject("error")?.getString("message") ?: bodyStr + } catch (e: JSONException) { bodyStr } + val requestId = response.headers["x-request-id"] ?: "" + callback.onFailure(Utils.constructErrorResponse( + response.code, + Utils.appendRequestId(message, requestId) + )) + return + } + + parseAndDispatch(responseJson ?: JSONObject()) + } catch (e: Exception) { + callback.onFailure(Utils.constructErrorResponse(e, 500)) + } + } + } + + private fun parseAndDispatch(responseJson: JSONObject) { + val responseArray = responseJson.optJSONArray("response") ?: JSONArray() + val allRecords = JSONArray() + + for (i in 0 until responseArray.length()) { + val entry = responseArray.getJSONObject(i) + val httpCode = entry.optInt("httpCode", 200) + val originalToken = entry.optString("token") + + if (httpCode == 200) { + val rawMeta = entry.optJSONObject("metadata") + val normalizedMeta = rawMeta?.let { m -> + JSONObject().also { out -> + m.keys().asSequence().forEach { k -> + val normalizedKey = if (k == "skyflowID") "skyflowId" else k + out.put(normalizedKey, m.opt(k)) + } + } + } + allRecords.put( + JSONObject() + .put("token", originalToken) + .put("value", entry.optString("value")) + .put("tokenGroupName", entry.optString("tokenGroupName")) + .put("httpCode", httpCode) + .put("metadata", normalizedMeta) + ) + } else { + allRecords.put( + JSONObject() + .put("token", originalToken) + .put("error", entry.optString("error", "Detokenize failed")) + .put("httpCode", httpCode) + ) + } + } + + callback.onSuccess(JSONObject().put("records", allRecords)) + } +} diff --git a/Skyflow/src/main/kotlin/Skyflow/reveal/FlowDBRevealRequestBody.kt b/Skyflow/src/main/kotlin/Skyflow/reveal/FlowDBRevealRequestBody.kt new file mode 100644 index 0000000..8222fc4 --- /dev/null +++ b/Skyflow/src/main/kotlin/Skyflow/reveal/FlowDBRevealRequestBody.kt @@ -0,0 +1,41 @@ +package Skyflow.reveal + +import Skyflow.Label +import Skyflow.RevealOptions +import org.json.JSONArray +import org.json.JSONObject + +internal class FlowDBRevealRequestBody { + companion object { + internal fun buildRequestBody( + vaultID: String, + elements: MutableList