diff --git a/README.md b/README.md index 5129049..83a8fb3 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,46 @@ -## Ballerine KMP Integration example +## Ballerine Integration example -### Integration into Android +### Integration into Android version of KMP project -1. Create a Fragment or Activity which contains the WebView, it should load `https://[YOUR-SUBDOMAIN].dev.ballerine.app` (sanbox) or `https://[YOUR-SUBDOMAIN].ballerine.app` (prod) URL. -2. Set webViewSettings to the following WebView settings: +1. Generate JWT token in your backend which is required to access the Ballerine KYC flow APIs. Here is the link to the documentation on how to generate token. +2. Add gradle dependency for Ballerine webview in your app-level `build.gradle` file ```kt - webviewSettings.javaScriptEnabled = true - webviewSettings.domStorageEnabled = true - webviewSettings.allowFileAccess = true +dependencies { + implementation("com.github.gau4sar:Ballerine-android-webview:1.0.0") +} ``` -3. Setup the WebViewClient and WebChromeClient. In WebChromeClient override the method `onShowFileChooser` the same way as it is implemented in `UserRegistrationFlowActivity`. -4. Add `onActivityResultListener` or `registerForActivityResult` to listen to the callback from the camera application: + We need to add the maven dependency for jitpack in settings.gradle ```kt - val resultCode = result.resultCode - val data = result.data - - when (resultCode) { - Activity.RESULT_OK -> { - //Image Uri will not be null for RESULT_OK - val uri: Uri = data?.data!! - - // Use Uri object instead of File to avoid storage permissions - filePathCallback!!.onReceiveValue(arrayOf(uri)) - filePathCallback = null - } - ImagePicker.RESULT_ERROR -> { - Toast.makeText(this, ImagePicker.getError(data), Toast.LENGTH_SHORT).show() - } - else -> { - Toast.makeText(this, "Task Cancelled", Toast.LENGTH_SHORT).show() - } +allprojects { + repositories { + ... + maven("https://jitpack.io") + } } ``` -5. Create a method that is checking for the finished state of the registration flow and saves the received results, see `checkWebViewUrl` method for more details. +3. Add `BallerineKYCFlowWebview` composable to your Activity/Fragment to initiate the web KYC verification flow process. + Then we receive the result of the callback function `onVerificationComplete` in your Activity/Fragment. +#### MainActivity.kt +```kt +BallerineKYCFlowWebView( + outputFileDirectory = outputFileDirectory, + cameraExecutorService = cameraExecutorService, + url = "$BALLERINE_WEB_URL?/b_t=$BALLERINE_API_TOKEN", + onVerificationComplete = { verificationResult -> + + //Do something with the verification result + + // Here we are just displaying the verification result as a Toast message + val toastMessage = "Idv result : ${verificationResult.idvResult} \n" + + "Status : ${verificationResult.status} \n" + + "Code : ${verificationResult.code}" + + // Here we are just displaying the verification result as Text on the screen + Toast.makeText(this, toastMessage, Toast.LENGTH_LONG).show() + }) +``` +4. Once you have received the `VerificationResult` we can do further checks on the different values of the `VerificationResult` like `status`|`idvResult`|`code`|`isSync`. + (As shown above in Point 3) ### Integration into iOS @@ -43,7 +51,7 @@ ```swift webView.addObserver(self, forKeyPath: "URL", options: .new, context: nil) ``` -4. Implement observeValue forKeyPath method to handle URL updates: +4. Implement observeValue forKeyPath method to handle URL updates: ```swift override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { guard let key = change?[NSKeyValueChangeKey.newKey], let url = (key as? NSURL)?.absoluteString else { return } diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index 2a249c7..1256ce9 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -7,7 +7,7 @@ android { compileSdk = 32 defaultConfig { applicationId = "io.ballerine.kmp.example.android" - minSdk = 22 + minSdk = 23 targetSdk = 32 versionCode = 1 versionName = "1.0" @@ -17,12 +17,31 @@ android { isMinifyEnabled = false } } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.1.1" + } } dependencies { implementation(project(":shared")) - implementation("com.google.android.material:material:1.4.0") - implementation("androidx.appcompat:appcompat:1.3.1") - implementation("androidx.constraintlayout:constraintlayout:2.1.0") - implementation("com.github.dhaval2404:imagepicker:2.1") + + implementation("com.github.ballerine-io:ballerine-android-sdk:1.0.4") + + implementation("androidx.appcompat:appcompat:1.5.0") + + val compose_version = "1.1.1" + + implementation ("androidx.core:core-ktx:1.8.0") + implementation ("androidx.activity:activity-compose:1.5.1") + + implementation ("androidx.compose.ui:ui:$compose_version") + implementation ("androidx.compose.material:material:$compose_version") + implementation ("androidx.compose.ui:ui-tooling-preview:$compose_version") + androidTestImplementation ("androidx.compose.ui:ui-test-junit4:$compose_version") + debugImplementation ("androidx.compose.ui:ui-tooling:$compose_version") + + implementation("com.google.accompanist:accompanist-permissions:0.26.0-alpha") } \ No newline at end of file diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml index 330ea15..baa6cef 100644 --- a/androidApp/src/main/AndroidManifest.xml +++ b/androidApp/src/main/AndroidManifest.xml @@ -3,10 +3,14 @@ package="io.ballerine.kmp.example.android"> + + + + @@ -16,9 +20,5 @@ - - \ No newline at end of file diff --git a/androidApp/src/main/java/io/ballerine/kmp/example/android/BallerineKYCFlow.kt b/androidApp/src/main/java/io/ballerine/kmp/example/android/BallerineKYCFlow.kt deleted file mode 100644 index 4c0e5e1..0000000 --- a/androidApp/src/main/java/io/ballerine/kmp/example/android/BallerineKYCFlow.kt +++ /dev/null @@ -1,100 +0,0 @@ -package io.ballerine.kmp.example.android - -import android.app.Activity -import android.net.Uri -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.webkit.* -import android.widget.Toast -import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AppCompatActivity -import com.github.dhaval2404.imagepicker.ImagePicker -import io.ballerine.kmp.example.BallerineStorage - - -class BallerineKYCFlow : AppCompatActivity() { - - lateinit var webView: WebView - private var filePathCallback: ValueCallback>? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_user_registration_flow) - - webView = findViewById(R.id.web_view) - - webView.webViewClient = object : WebViewClient() {} - - setWebViewSettings(webView.settings) - webView.webChromeClient = object : WebChromeClient() { - - override fun onShowFileChooser( - webView: WebView?, - filePathCallback: ValueCallback>?, - fileChooserParams: FileChooserParams? - ): Boolean { - if (this@BallerineKYCFlow.filePathCallback != null) { - this@BallerineKYCFlow.filePathCallback!!.onReceiveValue(null) - } - this@BallerineKYCFlow.filePathCallback = filePathCallback - ImagePicker.with(this@BallerineKYCFlow) - .cameraOnly() - .createIntent { intent -> - startForProfileImageResult.launch(intent) - } - return true - } - } - - loadUrl() - checkWebViewUrl() - } - - private fun checkWebViewUrl(){ - Handler(Looper.getMainLooper()).postDelayed({ - if(webView.url?.contains("final") == true){ - BallerineStorage.saveSecret(Uri.parse(webView.url).query ?: "") - setResult(Activity.RESULT_OK) - finish() - return@postDelayed - } - //Log.d("TAG", "WebView url ${webView.url}") - checkWebViewUrl() - }, 2000) - } - - private fun setWebViewSettings(webviewSettings: WebSettings) { - webviewSettings.javaScriptEnabled = true - webviewSettings.domStorageEnabled = true - webviewSettings.allowFileAccess = true - } - - - private fun loadUrl() { - webView.loadUrl("https://2.dev.ballerine.app") - } - - private val startForProfileImageResult = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - val resultCode = result.resultCode - val data = result.data - - when (resultCode) { - Activity.RESULT_OK -> { - //Image Uri will not be null for RESULT_OK - val uri: Uri = data?.data!! - - // Use Uri object instead of File to avoid storage permissions - filePathCallback!!.onReceiveValue(arrayOf(uri)) - filePathCallback = null - } - ImagePicker.RESULT_ERROR -> { - Toast.makeText(this, ImagePicker.getError(data), Toast.LENGTH_SHORT).show() - } - else -> { - Toast.makeText(this, "Task Cancelled", Toast.LENGTH_SHORT).show() - } - } - } -} diff --git a/androidApp/src/main/java/io/ballerine/kmp/example/android/MainActivity.kt b/androidApp/src/main/java/io/ballerine/kmp/example/android/MainActivity.kt index cc5a28e..b1c8308 100644 --- a/androidApp/src/main/java/io/ballerine/kmp/example/android/MainActivity.kt +++ b/androidApp/src/main/java/io/ballerine/kmp/example/android/MainActivity.kt @@ -1,40 +1,135 @@ package io.ballerine.kmp.example.android -import android.app.Activity -import android.content.Intent -import androidx.appcompat.app.AppCompatActivity import android.os.Bundle -import android.widget.Button -import android.widget.TextView -import androidx.activity.result.contract.ActivityResultContracts -import io.ballerine.kmp.example.BallerineStorage +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.OutlinedButton +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import io.ballerine.android_sdk.BallerineKYCFlowWebView +import java.io.File +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors class MainActivity : AppCompatActivity() { + + companion object { + + const val BALLERINE_WEB_URL = "https://moneco.dev.ballerine.app" + + /** + * BALLERINE_API_TOKEN needs to be generated from the backend. Please follow the below link for more information on how to generate the tole + * https://www.notion.so/ballerine/Ballerine-s-Developers-Documentation-c9b93462384446ef98ffb69d16865981#228240bfef6f48f3971db07ef03368c3 + */ + const val BALLERINE_API_TOKEN = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbmRVc2VySWQiOiJhMzEyYzk1ZC03ODE4LTQyNDAtOTQ5YS1mMDRmNDEwMzRlYzEiLCJjbGllbnRJZCI6IjI2YTRmOTFiLWFhM2UtNGNlNS1hZDE1LWYzNTRiOTI1NmJmMCIsImlhdCI6MTY1OTYxNzM1NCwiZXhwIjoxNjkwMzc1NzU0LCJpc3MiOiIyNmE0ZjkxYi1hYTNlLTRjZTUtYWQxNS1mMzU0YjkyNTZiZjAifQ.Nm-j9jVh7ByHoo0WkqnIQeVR0mNWcV3TZUNknSLRtbc" + + const val MAIN_SCREEN = 0 + const val WEB_VIEW_SCREEN = 1 + } + + private lateinit var outputFileDirectory: File + private lateinit var cameraExecutorService: ExecutorService + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - val button = findViewById