Flow Action Handler¶
FlowActionHandler lets clients plug in customized logic into the flow runtime. For example, it provides logic to validate scanned QR codes and a parser to convert QR codes into app configurations.
open class FlowActionHandler {
open fun validatePasscode(code: String): Boolean = true
open suspend fun parseBarcode(barcode: String): AppConfig? = null
open fun getEffectiveOAuthClient(oAuthConfig: OAuthConfig): AbstractOAuthClient =
oAuthConfig.getAllOAuthClients()[0]
open fun getFlowCustomizationSteps(flow: BaseFlow, insertionPoint: CustomStepInsertionPoint) =
Unit
open suspend fun configureOkHttpClient(appConfig: AppConfig): (OkHttpClient.Builder.() -> Unit)? =
null
open suspend fun getUserPreferredLocale(user: DeviceUser?): Locale? = null
open fun tokenRenewAuthenticator(): (() -> OAuth2Token?)? = null
open fun getCustomAuthenticationFlow(context: Context): BaseFlow? = null
}
Provide Custom Steps¶
open fun getFlowCustomizationSteps(flow: BaseFlow, insertionPoint: CustomStepInsertionPoint) = Unit
Because Fragment is not used in the Jetpack compose world, the way to provide custom steps for the onboarding flow is also different. The preceding code is the signature of the function in FlowActionHandler that you can override to add custom steps. The flow argument is the running flow instance, and insertionPoint is the insertion point currently supported in the compose version of the flows component. Here is an example:
override fun getFlowCustomizationSteps(
flow: BaseFlow,
insertionPoint: CustomStepInsertionPoint
) {
if (flow.flowName == FlowType.Onboarding.name) {
when (insertionPoint) {
CustomStepInsertionPoint.BeforeEula -> {
flow.addSingleStep(step_welcome, secure = false) {
LaunchScreen(
primaryViewClickListener = {
flow.flowDone(step_welcome)
},
secondaryViewClickListener = {
flow.terminateFlow(Activity.RESULT_OK)
}
)
}
}
CustomStepInsertionPoint.BeforeActivation -> {
flow.addSingleStep("step_custom_activation", secure = false) {
//client code will provide its own activation logic to get the AppConfig instance
CustomActivationStep() { appConfig ->
flow.updateAppConfigBeforeActivation(appConfig)
flow.flowDone("step_custom_activation")
}
}
}
else -> Unit
}
}
}
The preceding sample code adds a welcome step before the EULA step and a custom activation step before the activation step for the onboarding flow. Currently, we provide the following insertion points in the SDK:
sealed class CustomStepInsertionPoint {
object BeforeEula : CustomStepInsertionPoint()
object BeforeActivation : CustomStepInsertionPoint()
object BeforeAuthentication : CustomStepInsertionPoint()
object BeforeSetPasscode : CustomStepInsertionPoint()
object BeforeConsents : CustomStepInsertionPoint()
object BeforeOnboardingFinish : CustomStepInsertionPoint()
}
Activation From MDM¶
open suspend fun activateFromManagedConfig(bundle: Bundle): AppConfig? = null
In the view-based flows component, the FlowActionHandler function has a default implementation. It tries to read the application configuration from a predefined property. In the compose version, client code handles the logic entirely. This approach offers more flexibility than the constrained former method. Below is an example of reading AppConfig from the managed configuration and adding mandatory validation headers.
the managed configuration then add mandatory validation headers.
override suspend fun activateFromManagedConfig(bundle: Bundle): AppConfig {
val appString = ... //read the app config string out from 'bundle'
return AppConfig.createAppConfigFromJsonString(appString).apply {
//read mandatory validation headers from bundle, then add them into 'AppConfig',
//so all requests to the mobile server will have these headers.
val headers = listOf("sf_header_1" to "sf_value_1")
headers.forEach { (n, v) ->
addMandatoryValidationHeader(n, v)
}
}
}
configureOkHttpClient¶
This function gives the client app the chance to build the okHttpClient when SDK builds it. For example:
override suspend fun configureOkHttpClient(appConfig: AppConfig): (OkHttpClient.Builder.() -> Unit)? =
{
addInterceptor(Interceptor { chain ->
val request = chain.request().newBuilder()
.addHeader("my_header", "my_header_value")
.build()
chain.proceed(request)
})
}
getEffectiveOAuthClient¶
For OAuth authentication applications, multiple OAuth clients might be defined in the mobile service cockpit. By default, the flows component uses the first client to authenticate users during the onboarding flow. This function allows the client app to select an OAuth client for authentication. For example:
override fun getEffectiveOAuthClient(oAuthConfig: OAuthConfig): AbstractOAuthClient {
return oAuthConfig.getAllOAuthClients().firstOrNull { client ->
val uri = client.redirectURL.toUri()
uri.scheme == "myscheme" && uri.host == "androidsdkqiangz.web.app"
} ?: super.getEffectiveOAuthClient(oAuthConfig)
}
tokenRenewAuthenticator¶
SAP SDK offers a feature to renew OAuth tokens when the current ones expire. By default, the token renewal process displays the IDP login page in a browser or a WebView, allowing users to enter their credentials. The tokenRenewAuthenticator function enables users to scan a QR code for this process, eliminating the need to input credentials.
Here is an example:
private fun myAuthenticator(): OAuth2Token? {
var token: OAuth2Token? = null
//The back flow context is necessary because the token renewal process typically runs on top of the restore process.
val flowContextBackup = FlowContextRegistry.flowContext.copy()
AppLifecycleCallbackHandler.getInstance().activity?.let { ui ->
val cd = CountDownLatch(1)
val oauthFlowContext = flowContextBackup.copy(
// Please make sure the following code is in place once the token is ready, so in the startFlow
// callback, it can be retrieved from 'data'.
//
// populateFinishData {
// putExtra("my_token", result.data?.toString())
// }
flow = MyTokenRenewFlow(ui)
)
FlowUtil.startFlow(ui, oauthFlowContext) { resultCode, data ->
//restore the FlowContext
FlowContextRegistry.flowContext = flowContextBackup.copy()
if (resultCode == Activity.RESULT_OK) {
data?.getStringExtra("my_token")
?.also { tokenString ->
token = OAuth2Token.createOAuth2TokenFromJsonString(tokenString)
}
}
cd.countDown()
}
//Ensure the function returns once the flow finishes.
cd.await()
}
return token ?: run {
//If failed to retrieve the token, it throws an IOException to terminate the token renewal process.
//process, otherwise, SDK will try IDP login with a browser or WebView.
throw IOException("Failed to renew token.")
}
}
override fun tokenRenewAuthenticator() = ::myAuthenticator
Please note the followings:
- This function runs in the IO scope. Make sure to block the processes before retrieving the token.
- The above example starts a new custom flow. The client code has full control over what to display in the flow.
- The function accepts only the OAuth token. The client code is responsible for converting the scanned QR code into the token. You can use the
retrieveCrossLoginOAuthTokenfunction ofOnboardingHelperfor the conversion. - Returning a null token will cause the SDK to fall back to the IDP(Identity Provider) login to retrieve the token. To stop the process, throw an
IOException.
Specify Locale for a User¶
open suspend fun getUserPreferredLocale(user: DeviceUser?): Locale? = null
The flows component of SAP SDK uses the system locale by default. However, the function in FlowActionHandler lets you specify a locale for a user or a group of users. If the function returns null, the system locale will be used.
Provide Custom Flow for Authentication¶
open fun getCustomAuthenticationFlow(context: Context): BaseFlow? = null
To support custom HTTP authentication types, the API allows client code to provide a custom authentication flow. In the custom flow, client code implements the logic to obtain token and save it in the user store.
Client code must also implement configureOkHttpClient to configure the okHttpClient by adding an interceptor. The interceptor reads the token from the user store and adds it to the request header. It must also handle token renewal logic if the existing token expires.
Here is an example:
override suspend fun configureOkHttpClient(appConfig: AppConfig): (OkHttpClient.Builder.() -> Unit) =
{
logger.debug("in configureOkHttpClient...")
if (appConfig.primaryAuthenticationConfig?.getAuthMethod() == AuthMethod.HTTP) {
logger.debug("Adding http OAuth interceptor...")
addInterceptor { chain ->
val req = chain.request()
val newReq =
UserSecureStoreDelegate.getInstance().getData<String>(KEY_HTTP_TOKEN)
?.let { token ->
req.newBuilder().header("authorization", "bearer $token").build()
} ?: req
chain.proceed(newReq)
}
}
}
override fun getCustomAuthenticationFlow(context: Context): BaseFlow =
object : BaseFlow(context, "http_oauth_flow") {
override fun initialize() {
addSingleStep("step_1") {
FioriTextButton(
modifier = Modifier
.fillMaxWidth()
.padding(48.dp),
buttonContent = FioriButtonContent(
title = "Get Token"
)
) {
scope.launch(IO) {
val url = "..."
val request: Request = Request.Builder()
.url(url).get().build()
OkHttpClient().newCall(request).execute().use { response ->
if (response.isSuccessful) {
val token = response.body.string()
UserSecureStoreDelegate.getInstance()
.saveData(KEY_HTTP_TOKEN, token)
logger.debug("Http OAuth token retrieved: {}", token)
}
}
}.invokeOnCompletion {
flowDone("step_1")
}
}
}
}
}