Skip to content

Commit

Permalink
Merge pull request #21 from FinTecSystems/add-app2app-redirection
Browse files Browse the repository at this point in the history
Implement App2App redirection
  • Loading branch information
maik-mursall authored Aug 14, 2023
2 parents c907c6c + cef6cc1 commit 10b755c
Show file tree
Hide file tree
Showing 16 changed files with 216 additions and 11 deletions.
2 changes: 1 addition & 1 deletion .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/kotlinc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,52 @@ Pass your ViewModel, using the `viewModels` function, and pass it to the `XS2AWi
> It is possible to freely define the ViewModel-Scope.
> Please refer to [this answer](https://stackoverflow.com/a/68971296) for more information.
## App to App redirection (Beta)
Some banks support redirecting to their banking app.
Per default the SDK will not redirect to the banking app and opens the internal WebView instead.

If you'd like to make use of this feature you can configure the SDK the following way:

Modify your `AndroidManifest.xml` with the following:

```xml
<activity
android:exported="true" // Required
android:launchMode="singleInstance" // Required, other values might be used as well.
...>
...
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="<host>"
android:scheme="<scheme>" />
</intent-filter>
</activity>
```

Populate `host` and `scheme` with your the URL of your App.

After that just pass your URL to the SDK:

```kotlin
// Compose
XS2AWizard(
sessionKey = <your-session-key>,
redirectDeepLink = "<scheme>://<host>" // Insert your deep link
)

// Fragment
XS2AWizardFragment(
sessionKey = <your-session-key>,
redirectDeepLink = "<scheme>://<host>" // Insert your deep link
)
```

Now every time the SDK encounters an URL to a bank which is known by us to support App2App redirection, the user gets asked if they want
to perform the action within the WebView or the banking app.

#### Example

```kotlin
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
GROUP=com.fintecsystems
VERSION_NAME=5.1.4
VERSION_NAME=5.2.0
POM_URL=https://github.com/FinTecSystems/xs2a-android
POM_SCM_URL=[email protected]:FinTecSystems/xs2a-android.git
POM_SCM_CONNECTION=scm:git:[email protected]:FinTecSystems/xs2a-android.git
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ import com.fintecsystems.xs2awizard.form.components.textLine.TextLine
* @param enableAutomaticRetry - If true requests to the backend will be retried if the device is offline and goes online again.
* This also means that the loading indicator will stay during that time.
* @param callbackListener - Listener to all XS2A callbacks.
* @param redirectDeepLink - Deep Link of Host-App used for returning App2App redirection.
* Must match your scheme and host declared in your AndroidManifest.
* e.g "<scheme>://<host>".
* @param xs2aWizardViewModel - ViewModel of the Wizard-Instance.
*/
@Composable
Expand All @@ -66,6 +69,7 @@ fun XS2AWizard(
enableBackButton: Boolean = true,
enableAutomaticRetry: Boolean = true,
callbackListener: XS2AWizardCallbackListener? = null,
redirectDeepLink: String? = null,
xs2aWizardViewModel: XS2AWizardViewModel = viewModel()
) {
val form by xs2aWizardViewModel.form.observeAsState(null)
Expand All @@ -86,6 +90,7 @@ fun XS2AWizard(
enableScroll,
enableBackButton,
enableAutomaticRetry,
redirectDeepLink,
context as Activity
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ object XS2AWizardBundleKeys {
const val enableScroll = "enableScroll"
const val enableBackButton = "enableBackButton"
const val enableAutomaticRetry = "enableAutomaticRetry"
const val redirectDeepLink = "redirectDeepLink"

const val currentWebViewUrl = "currentWebViewUrl"
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,21 @@ package com.fintecsystems.xs2awizard.components
import android.app.Activity
import android.app.Application
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.Network
import android.net.Uri
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.ui.text.AnnotatedString
import androidx.core.net.toUri
import androidx.core.util.Consumer
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
Expand Down Expand Up @@ -64,6 +69,12 @@ class XS2AWizardViewModel(
*/
private var enableAutomaticRetry: Boolean = true

/**
* Used for App2App redirection.
* URL of Host-App to return to.
*/
private var redirectDeepLink: String? = null

internal val form = MutableLiveData<List<FormLineData>?>()

internal val loadingIndicatorLock = MutableLiveData(false)
Expand Down Expand Up @@ -98,6 +109,14 @@ class XS2AWizardViewModel(

private var currentState: String? = null

private val onNewIntentListener = Consumer<Intent> {
if (it.action != Intent.ACTION_VIEW || it.data == null) return@Consumer

if (isRedirectDeepLink(it.dataString)) {
redirectionCallback(true)
}
}

init {
val xs2aWizardBundle = savedStateHandle.get<Bundle>(XS2AWizardBundleKeys.bundleName)

Expand All @@ -120,12 +139,14 @@ class XS2AWizardViewModel(
enableScroll: Boolean,
enableBackButton: Boolean,
enableAutomaticRetry: Boolean,
redirectDeepLink: String?,
activity: Activity
) {
this.language = language
this.enableScroll = enableScroll
this.enableBackButton = enableBackButton
this.enableAutomaticRetry = enableAutomaticRetry
this.redirectDeepLink = redirectDeepLink
currentActivity = WeakReference(activity)

NetworkingInstance.getInstance(context).apply {
Expand All @@ -135,6 +156,8 @@ class XS2AWizardViewModel(

context.registerNetworkCallback(networkCallback)

(activity as? ComponentActivity)?.addOnNewIntentListener(onNewIntentListener)

initForm()
}

Expand All @@ -148,6 +171,10 @@ class XS2AWizardViewModel(
enableScroll = true
enableBackButton = true
enableAutomaticRetry = true
redirectDeepLink = null
(currentActivity.get() as? ComponentActivity)?.removeOnNewIntentListener(
onNewIntentListener
)
currentActivity = WeakReference(null)
connectionState.value = ConnectionState.UNKNOWN
context.unregisterNetworkCallback(networkCallback)
Expand All @@ -163,6 +190,10 @@ class XS2AWizardViewModel(
buildJsonObject {
put("version", JsonPrimitive(BuildConfig.VERSION))
put("client", JsonPrimitive(context.getString(R.string.xs2a_client_tag)))

if (redirectDeepLink != null) {
put("location", JsonPrimitive(redirectDeepLink))
}
},
true
)
Expand Down Expand Up @@ -344,6 +375,7 @@ class XS2AWizardViewModel(

submitForm(constructJsonBody("autosubmit", jsonBody))
}

else -> CustomTabsIntent.Builder().build().launchUrl(
activity, Uri.parse(annotation.item)
)
Expand Down Expand Up @@ -577,7 +609,7 @@ class XS2AWizardViewModel(
*
* @param url url to open.
*/
internal fun openWebView(url: String) {
private fun openWebView(url: String) {
currentWebViewUrl.value = url
}

Expand All @@ -588,6 +620,41 @@ class XS2AWizardViewModel(
currentWebViewUrl.value = null
}

/**
* Opens the provided [url] in an external Browser.
*
* @param url The URl to open.
*/
private fun openExternalUrl(url: String) {
val webIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))

currentActivity.get()!!.startActivity(webIntent)
}

/**
* Open supplied [url] in either an external Browser or internal Webview,
* depending if the current provider supports it.
*
* @param url the URL to open.
*/
internal fun openRedirectURL(url: String) {
if (urlSupportsAppRedirection(url)) {
AlertDialog.Builder(currentActivity.get()!!).apply {
setTitle(R.string.redirect_dialog_title)
setMessage(R.string.redirect_dialog_message)
setPositiveButton(R.string.redirect_dialog_banking_app_button_title) { _, _ ->
openExternalUrl(url)
}
setNegativeButton(R.string.redirect_dialog_website_button_title) { _, _ ->
openWebView(url)
}
show()
}
} else {
openWebView(url)
}
}

/**
* Checks if the current form is the bank search.
*/
Expand All @@ -600,12 +667,46 @@ class XS2AWizardViewModel(
@Suppress("unused")
fun isLogin() = currentState == "login"

/**
* Checks if provided [url] is known to support App2App redirection.
*
* @param url URL to check
* @return true if URL supports App2App redirection
*/
private fun urlSupportsAppRedirection(url: String) =
supportedAppRedirectionURLs.contains(url.toUri().host)

/**
* Checks if provided [url] is the [redirectDeepLink].
*
* @param url URL to check
* @return true if it's the [redirectDeepLink]
*/
internal fun isRedirectDeepLink(url: String?) = url == redirectDeepLink

/**
* Callback method for the redirection result.
*
* @param success True if redirection operation was successful.
*/
internal fun redirectionCallback(success: Boolean) {
closeWebView()

if (success)
submitForm("post-code")
}

companion object {
private const val rememberLoginName = "store_credentials"
private const val sharedPreferencesFileName = "xs2a_credentials"
private const val storedProvidersKey = "providers"
private const val masterKeyAlias = "xs2a_credentials_master_key"

private val supportedAppRedirectionURLs = listOf(
"manage.xs2a.com",
"myaccount.ing.com"
)

/**
* Delete all saved credentials.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,7 @@ fun URLBarWebView(viewModel: XS2AWizardViewModel) {
val callbackHandler = object : XS2AJavascriptInterfaceCallback {
override fun xS2AJavascriptInterfaceCallbackHandler(success: Boolean) {
coroutineScope.launch {
viewModel.closeWebView()

if (success)
viewModel.submitForm("post-code")
viewModel.redirectionCallback(success)
}
}
}
Expand Down Expand Up @@ -161,6 +158,11 @@ fun URLBarWebView(viewModel: XS2AWizardViewModel) {
webViewClient = object : WebViewClient() {
@Deprecated("Deprecated in Java")
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
if (viewModel.isRedirectDeepLink(url)) {
viewModel.redirectionCallback(true)
return true
}

currentUrl = url
view.loadUrl(url)
return true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ fun RedirectLine(formData: RedirectLineData, viewModel: XS2AWizardViewModel) {
verticalArrangement = Arrangement.spacedBy(5.dp)
) {
FormButton(label = formData.label!!, buttonStyle = XS2ATheme.CURRENT.redirectButtonStyle) {
viewModel.openWebView(formData.url!!)
viewModel.openRedirectURL(formData.url!!)
}

FormButton(
Expand Down
Loading

0 comments on commit 10b755c

Please sign in to comment.