Skip to content

Commit

Permalink
Merge branch 'main' into release/24.9.2
Browse files Browse the repository at this point in the history
  • Loading branch information
skyflow-vivek authored Sep 11, 2024
2 parents fa87529 + 18fedb9 commit d103345
Show file tree
Hide file tree
Showing 11 changed files with 377 additions and 17 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

All notable changes to this project will be documented in this file.

## [1.24.1] - 2024-09-09
### Added
- Fixed element state for card brand choice

## [1.24.0] - 2024-09-03
### Added
- Support for card brand choice for Card Number element.

## [1.23.0] - 2023-10-25
### Added
- Support for more styling in Collect Elements.
Expand Down
42 changes: 31 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ Alternatively you can also add the GPR_USER_NAME and GPR_PAT values to your envi
- Add the dependency to your application's build.gradle file

```java
implementation 'com.skyflowapi.android:skyflow-android-sdk:1.21.0'
implementation 'com.skyflowapi.android:skyflow-android-sdk:1.24.0'
```

#### Using maven
Expand Down Expand Up @@ -91,7 +91,7 @@ Alternatively you can also add the GPR_USER_NAME and GPR_PAT values to your envi
<dependency>
<groupId>com.skyflowapi.android</groupId>
<artifactId>skyflow-android-sdk</artifactId>
<version>1.21.0</version>
<version>1.24.0</version>
</dependency>
```

Expand Down Expand Up @@ -391,6 +391,7 @@ Skyflow.CollectElementOptions(
format: String, // Format for the element (currently applicable for "EXPIRATION_DATE", "CARD_NUMBER", "EXPIRATION_YEAR" and "INPUT_FIELD")
translation: HashMap<Char, String> // Indicates the allowed data type value for format.
enableCopy: Boolean, // Indicates whether to enable the copy icon in collect elements to copy text to clipboard. Defaults to 'false'
cardMetadata: Skyflow.CardMetadata, // Optional, metadata to control card number element behavior. (only applicable for CARD_NUMBER ElementType).
)
```

Expand All @@ -405,7 +406,25 @@ Skyflow.CollectElementOptions(

- `translation`: A hashmap of key/value pairs, where the key is a character that appears in `format` and the value is a regex pattern of acceptable inputs for that character. Each key can only appear once. Only applicable for `INPUT_FIELD` elements.

`enableCopy`: Indicates whether to enable the copy icon in collect elements to copy text to clipboard.
- `enableCopy`: Indicates whether to enable the copy icon in collect elements to copy text to clipboard.

- `cardMetadata`: An object of metadata keys to control card number element behavior. It supports an optional key called `scheme`, which accepts an array of Skyflow-supported card types and determines which brands display in the card number element's card brand choice dropdown. `Skyflow.CardType` is an enum with all Skyflow-supported card schemes.

```kotlin
class CardMetadata(var scheme: Array<CardType>) {}
```

#### Supported card types by Skyflow.CardType :
- `VISA`
- `MASTERCARD`
- `AMEX`
- `DINERS_CLUB`
- `DISCOVER`
- `JCB`
- `MAESTRO`
- `UNIONPAY`
- `HIPERCARD`
- `CARTES_BANCAIRES`

Accepted values by element type:

Expand Down Expand Up @@ -770,15 +789,17 @@ The handler ```(state: JSONObject) -> Unit``` is a callback function you provide
```kt
val state = {
"elementType": Skyflow.ElementType,
"isEmpty": Bool,
"isRequired": Bool,
"isFocused": Bool,
"isValid": Bool,
"value": String
"isEmpty": Boolean,
"isRequired": Boolean,
"isFocused": Boolean,
"isValid": Boolean,
"value": String,
"selectedCardScheme": Skyflow.CardType,
}
```
`Note:`
values of SkyflowElements will be returned in element state object only when `env` is `DEV`, else it is empty string i.e, '', but in case of CARD_NUMBER type element when the `env` is `PROD` for all the card types except AMEX, it will return first eight digits, for AMEX it will return first six digits and rest all digits in masked format.
`Notes:`
- values of SkyflowElements will be returned in element state object only when `env` is `DEV`, else it is empty string i.e, '', but in case of CARD_NUMBER type element when the `env` is `PROD` for all the card types except AMEX, it will return first eight digits, for AMEX it will return first six digits and rest all digits in masked format.
- `selectedCardScheme` is only populated for the `CARD_NUMBER` element states when a user chooses a card brand. By default, `selectedCardScheme` is an empty string.

##### Sample code snippet for using listeners
```kt
Expand Down Expand Up @@ -883,7 +904,6 @@ cardNumber.setError("custom error")

//reset custom error
cardNumber.resetError()
}
```


Expand Down
2 changes: 1 addition & 1 deletion Skyflow/src/main/kotlin/Skyflow/CardMetadata.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ package Skyflow

import Skyflow.collect.elements.utils.CardType

class CardMetadata(var scheme: Array<CardType>) {
class CardMetadata(var scheme: Array<CardType> = arrayOf()) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ class StateforText internal constructor(val tf: TextField) : State(tf.columnName
}

private fun getCardSchemeString(): String {
return if (tf.cardType === CardType.EMPTY) ""
else tf.cardType.toString()
return if (tf.cardType === CardType.EMPTY || !tf.isCustomCardBrandSelected) ""
else tf.cardType.defaultName.uppercase()
}

override fun show(): String {
Expand Down Expand Up @@ -69,13 +69,15 @@ class StateforText internal constructor(val tf: TextField) : State(tf.columnName
state.put("isRequired", isRequired)
state.put("isFocused", isFocused)
state.put("isValid", isValid)
state.put("selectedCardScheme", selectedCardScheme)
var value = ""
if (env == Env.DEV) {
value = tf.getValue()
} else if (env == Env.PROD && tf.fieldType == SkyflowElementType.CARD_NUMBER) {
value = CardType.getBin(tf.getValue())
}
if (tf.fieldType == SkyflowElementType.CARD_NUMBER) {
state.put("selectedCardScheme", selectedCardScheme)
}
state.put("value", value)
return state
}
Expand Down
4 changes: 2 additions & 2 deletions samples/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,6 @@ dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation "androidx.core:core-ktx:+"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"


implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0")
}
1 change: 1 addition & 0 deletions samples/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
android:name=".InputFormattingReveal"
android:label="@string/input_formatting_demo" />
<activity android:name=".ComposableActivity" />
<activity android:name=".CardBrandChoiceActivity" />
<activity
android:name=".MainActivity"
android:exported="true">
Expand Down
232 changes: 232 additions & 0 deletions samples/src/main/java/com/Skyflow/CardBrandChoiceActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
package com.Skyflow

import Skyflow.*
import Skyflow.LogLevel
import Skyflow.Options
import Skyflow.collect.elements.utils.CardType
import Skyflow.utils.EventName
import android.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.LinearLayout
import com.Skyflow.collect.elements.validations.LengthMatchRule
import com.Skyflow.collect.elements.validations.ValidationSet
import com.Skyflow.utils.CustomStyles
import kotlinx.android.synthetic.main.activity_collect.*
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONArray
import org.json.JSONObject
import java.io.IOException

class CardBrandChoiceActivity : AppCompatActivity() {

private val TAG = CollectActivity::class.qualifiedName

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_collect)
val tokenProvider = CollectActivity.DemoTokenProvider()
val skyflowConfiguration = Configuration(
"<VAULT_ID>",
"<VAULT_URL>",
tokenProvider,
Options(LogLevel.ERROR, Env.PROD)
)
val skyflowClient = init(skyflowConfiguration)
val collectContainer = skyflowClient.container(ContainerType.COLLECT)

val styles = CustomStyles.getInputStyles()
val labelStyles = CustomStyles.getLabelStyles()
val errorStyles = CustomStyles.getErrorStyles()

val validationSet = ValidationSet()
validationSet.add(LengthMatchRule(2, 20, "not valid"))

val cardNumberInput = CollectElementInput(
table = "<TABLE_NAME>",
column = "<COLUMN_NAME>",
SkyflowElementType.CARD_NUMBER,
inputStyles = styles,
labelStyles = labelStyles,
errorTextStyles = errorStyles,
label = "Card Number",
placeholder = "CardNumber"
)

val expiryDateInput = CollectElementInput(
table = "<TABLE_NAME>",
column = "<COLUMN_NAME>",
SkyflowElementType.EXPIRATION_DATE,
inputStyles = styles,
labelStyles = labelStyles,
errorTextStyles = errorStyles,
label = "expiry date",
placeholder = "expiry date"
)

val nameInput = CollectElementInput(
table = "<TABLE_NAME>",
column = "<COLUMN_NAME>",
SkyflowElementType.CARDHOLDER_NAME,
inputStyles = styles,
labelStyles = labelStyles,
errorTextStyles = errorStyles,
label = "Full Name",
placeholder = "Full Name",
validations = validationSet
)

val cvvInput = CollectElementInput(
table = "<TABLE_NAME>",
column = "<COLUMN_NAME>",
type = SkyflowElementType.CVV,
inputStyles = styles,
labelStyles = labelStyles,
errorTextStyles = errorStyles,
placeholder = "CVV",
altText = "CVV"
)

val options = CollectElementOptions(true)
val cardNumber = collectContainer.create(
this, cardNumberInput, CollectElementOptions(required = true, enableCopy = true)
)

val expirationDate = collectContainer.create(
this, expiryDateInput, CollectElementOptions(format = "yyyy/mm")
)

val name = collectContainer.create(this, nameInput, options)
val cvv = collectContainer.create(this, cvvInput)

cardNumber.on(EventName.FOCUS) { state ->
Log.d(TAG, "focus: sate $state")
}

cardNumber.on(EventName.BLUR) { state ->
Log.d(TAG, "blur: sate $state")
}

var calledUpdate = false
cardNumber.on(EventName.CHANGE) { state ->
Log.d(TAG, "change: sate $state")
val value = state.getString("value")
if (value.length < 8 && calledUpdate) {
calledUpdate = false
cardNumber.update(CollectElementOptions(cardMetadata = CardMetadata(arrayOf())))
} else if (value.length >= 8 && !calledUpdate) {
calledUpdate = true
binLookup(value, object : Callback {
override fun onSuccess(responseBody: Any) {
println(responseBody as JSONArray)
val scheme = getCardSchemes(responseBody)
runOnUiThread(kotlinx.coroutines.Runnable {
cardNumber.update(
CollectElementOptions(cardMetadata = CardMetadata(scheme))
)
})
}

override fun onFailure(exception: Any) {
println(exception)
}
})
}
}

cardNumber.on(EventName.READY) { state ->
Log.d(TAG, "ready: sate $state")
}

val parent = findViewById<LinearLayout>(R.id.parent)
val lp = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
lp.setMargins(20, -20, 20, 0)
cardNumber.layoutParams = lp
expirationDate.layoutParams = lp
name.layoutParams = lp
cvv.layoutParams = lp

parent.addView(name)
parent.addView(cardNumber)
parent.addView(expirationDate)
parent.addView(cvv)

submit.setOnClickListener {
val dialog = AlertDialog.Builder(this).create()
dialog.setMessage("please wait..")
dialog.show()
collectContainer.collect(object : Callback {
override fun onSuccess(responseBody: Any) {
dialog.dismiss()
Log.d(TAG, "collect success: $responseBody")
}

override fun onFailure(exception: Any) {
dialog.dismiss()
Log.d(TAG, "collect failure: ${(exception as Exception).message}")
}
}, CollectOptions(true))
}

clear.setOnClickListener {
clearFields(mutableListOf(cardNumber, cvv, name, expirationDate))
}

}

private fun binLookup(binValue: String, callback: Callback) {
val jsonBody = JSONObject().put("BIN", binValue)
val requestBody = jsonBody.toString().toRequestBody("application/json".toMediaTypeOrNull())
val url = "https://<VAULT_URL>/v1/card_lookup"
val request = okhttp3.Request.Builder()
.url(url)
.method("POST", requestBody)
.addHeader("X-skyflow-authorization", "<BEARER_TOKEN>")
.build()

val okHttpClient = OkHttpClient()
try {
val thread = Thread {
run {

okHttpClient.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
throw IOException("Request Failed $response")
}
val responseObject = JSONObject(response.body?.string().toString())
callback.onSuccess(responseObject.getJSONArray("cards_data"))
}
}
}
thread.start()
} catch (exception: Exception) {
exception.printStackTrace()
}
}

private fun getCardSchemes(cardData: JSONArray): Array<CardType> {
var cardSchemes: Array<CardType> = arrayOf()
for (i in 0 until cardData.length()) {
val cardObject = cardData.getJSONObject(i)
when (cardObject.getString("card_scheme")) {
"CARTES BANCAIRES" -> cardSchemes = cardSchemes.plus(CardType.CARTES_BANCAIRES)
"MASTERCARD" -> cardSchemes = cardSchemes.plus(CardType.MASTERCARD)
"VISA" -> cardSchemes = cardSchemes.plus(CardType.VISA)
}
}
return cardSchemes
}

//reset elements to initial state
private fun clearFields(elements: List<TextField>) {
for (element in elements) {
element.unmount()
}
}
}
Loading

0 comments on commit d103345

Please sign in to comment.