UI extensibility options

When following the guides such as HAAPI to OAuth, Theming and WidgetConfiguration.Builder options, the WidgetConfiguration, and the theming/resources XML files drive the UI elements for HAAPI. These configurations might be sufficient to align with the design of the main application. However, if these configurations are not enough, it is possible to have "advanced" customizations such as:

Providing custom fragments for any HAAPI states

The HAAPI UI.Widget uses HAAPI representations that are mapped into UIModel that generate fragments via a FragmentResolver.

The table below lists which fragments are handling the UIModels, by default.

UIModel Fragment that is a subclass of HaapiFlowFragment
ContinueSameModel FormFragment
FormModel FormFragment
GenericModel GenericFragment
PollingModel PollingFragment
ProblemModel ProblemFragment
SelectorModel SelectorFragment
WebAuthnOperationModel WebAuthnFragment

The FragmentResolver is an interface that is used by HaapiUIWidgetApplication and the definition is the following.

kotlin
interface FragmentResolver {
    /**
     * Resolves a Fragment instance for the provided model.
     *
     * @param uiModel An instance of [UIModel] with the current flow state information.
     */
    fun <T: UIModel> getFragment(uiModel: T): HaapiFlowFragment<T>
}

By default, HaapiUIWidgetApplication uses HaapiFlowFragmentResolver which can be customized by using the HaapiFlowFragmentResolver.Builder. The following snippet illustrates how to use the Builder:

kotlin
class ClientApplication : Application(), HaapiUIWidgetApplication {
    private val baseUri = URI.create("https://10.0.2.2:8443")

    override val fragmentResolver: FragmentResolver =
        HaapiFlowFragmentResolver.Builder()
  					// NewFormFragment is a subclass of FormFragment and uses the pattern `newInstance`
            .customize(FormModel::class.java, NewFormFragment::newInstance)
            .build()
  	...
}

Finally, HaapiFlowActivity uses the generated fragments.

There are different methods to customize fragments for any HAAPI states:

Customization Method What for
[Customizing an existing fragment using a different XML layout](#Customizing an existing fragment using a different XML layout) Redesigns the structure of the XML, adds/removes static UI elements, and keeps the default implementation. Only the XML layout file is changed.
[Customizing an existing fragment by subclassing default implementation](#Customizing an existing fragment by subclassing default implementation) Adds/removes UI elements to the fragment and/or redesigns the structure of the XML. An implementation is required and the layout may change.
[Providing a custom fragment that subclass HaapiFlowFragment](#Providing a custom fragment that subclass HaapiFlowFragment) Unsatisfied with the UI and default implementation. Creates a new fragment that conforms to HaapiFlowFragment by managing on your own the UI design that uses the UIModel.

Customizing an existing fragment using a different XML layout

An existing fragment can be customized by providing a different XML layout when :

To start, it is required to know which fragment to target.

To illustrate this method, FormFragment is customized with a different XML layout displaying the following structure.

instead of

The new XML that is named fragment_redesign_form looks like this:

xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/hui_fragment_form_layout"
    >

    <TextView
        android:id="@+id/a_custom_text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="A new redesign view"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/hui_fragment_form_links_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toBottomOf="@+id/a_custom_text_view"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        android:overScrollMode="never"/>

    <Space
        android:id="@+id/hui_fragment_form_bottom_space"
        android:layout_width="match_parent"
        android:layout_height="@dimen/hui_fragment_form_spacing"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/hui_fragment_form_links_recycler_view"
        app:layout_constraintEnd_toEndOf="parent"
        />

    <se.curity.identityserver.haapi.android.ui.widget.components.InfoView
        android:id="@+id/hui_fragment_form_info_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/hui_fragment_form_links_recycler_view"
        app:layout_constraintEnd_toEndOf="parent"
        />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/hui_fragment_form_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/hui_fragment_form_info_view"
        app:layout_constraintEnd_toEndOf="parent"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        android:overScrollMode="never"
        />
</androidx.constraintlayout.widget.ConstraintLayout>

Override HaapiUIWidgetApplication.fragmentResolver in your application by using the new FormFragment.

kotlin
class ClientApplication : Application(), HaapiUIWidgetApplication {
    private val baseUri = URI.create("https://10.0.2.2:8443")
    override val widgetConfiguration: WidgetConfiguration = 
  			WidgetConfiguration.Builder(
            clientId = "clientId",
            baseUri = baseUri,
            tokenEndpointUri = baseUri.resolve("/dev/oauth/token"),
            authorizationEndpointUri = baseUri.resolve("/dev/oauth/authorize"),
            appRedirect = "app://haapi"
        )
            .setKeyStoreAlias("keystore")
            .setHttpUrlConnectionProvider(UNCHECKED_CONNECTION_PROVIDER)
            .build()

    override val fragmentResolver: FragmentResolver = 
  			HaapiFlowFragmentResolver.Builder()
            .customize(FormModel::class.java) { model ->
              FormFragment.newInstance(uiModel, R.layout.fragment_redesign_form)
            }
            .build()
}

Customizing an existing fragment by subclassing default implementation

An existing fragment can be customized by subclassing its default implemenetation when:

To start, it is required to know which fragment to target including the naming requirements for the XML.

To illustrate this method, two examples are illustrated below:

Subclassing SelectorFragment to display an advertisement banner at the top of the screen

Follow the prerequisites to import the Mobile Ads SDK dependency and set up the mandatory configuration. The following AndroidManifest looks like that:

xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        android:name=".ClientApplication"
        ...>
			  ...
        <!-- Sample AdMob app ID: ca-app-pub-3940256099942544~3347511713 -->
        <meta-data
            android:name="com.google.android.gms.ads.APPLICATION_ID"
            android:value="ca-app-pub-3940256099942544~3347511713"/>
        ...
    </application>
</manifest>

Create a new XML (fragment_subclass_selector.xml) for the BannerAd by using the hui_fragment_selector.

xml
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.gms.ads.AdView
        android:id="@+id/adView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:adSize="BANNER"
        app:adUnitId="ca-app-pub-3940256099942544/9214589741"
        />

    <se.curity.identityserver.haapi.android.ui.widget.components.InfoView
        android:id="@+id/hui_fragment_selector_info_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/adView"
        app:layout_constraintEnd_toEndOf="parent" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/hui_fragment_selector_items_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/hui_fragment_selector_info_view"
        app:layout_constraintEnd_toEndOf="parent"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        android:overScrollMode="never" />

    <Space
        android:id="@+id/hui_fragment_selector_bottom_space"
        android:layout_width="match_parent"
        android:layout_height="@dimen/hui_fragment_form_spacing"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/hui_fragment_selector_items_recycler_view"
        app:layout_constraintEnd_toEndOf="parent" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/hui_fragment_selector_links_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/hui_fragment_selector_bottom_space"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        android:overScrollMode="never" />
</androidx.constraintlayout.widget.ConstraintLayout>

Create the implementation file that hooks fragment_subclass_selector.xml.

kotlin
class AdsBannerSelectorFragment : SelectorFragment(layoutId = R.layout.fragment_subclass_selector) {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // Configuring the advertisement banner
        MobileAds.initialize(requireContext())
        val adView = view.findViewById<AdView>(R.id.adView)
        adView.loadAd(AdRequest.Builder().build())
    }

    companion object {
        // It is the recommended way to instantiate a fragment.
        fun newInstance(uiModel: UIModel): AdsBannerSelectorFragment {
            return AdsBannerSelectorFragment().apply {
                arguments = uiModelBundle(uiModel)
            }
        }
    }
}

Override HaapiUIWidgetApplication.fragmentResolver in your application by using AdsBannerSelectorFragment.

kotlin
class ClientApplication : Application(), HaapiUIWidgetApplication {
    private val baseUri = URI.create("https://10.0.2.2:8443")
    override val widgetConfiguration: WidgetConfiguration = 
  			WidgetConfiguration.Builder(
            clientId = "clientId",
            baseUri = baseUri,
            tokenEndpointUri = baseUri.resolve("/dev/oauth/token"),
            authorizationEndpointUri = baseUri.resolve("/dev/oauth/authorize"),
            appRedirect = "app://haapi"
        )
            .setKeyStoreAlias("keystore")
            .setHttpUrlConnectionProvider(UNCHECKED_CONNECTION_PROVIDER)
            .build()

    override val fragmentResolver: FragmentResolver = 
  			HaapiFlowFragmentResolver.Builder()
            .customize(SelectorModel::class.java, AdsBannerSelectorFragment::newInstance)
            .build()
}

When starting the HAAPI flow and the SelectorModel is displayed, a banner view is presented at the top of the screen as demonstrated below.

Banner ad with subclass selectorFragment

Subclassing FormFragment to display a reCAPTCHA when a button is pushed.

Generate the reCAPTCHA site key and secret key here by using the mandatory configuration:

Import the following dependencies in your gradle file.

groovy
implementation 'com.google.android.gms:play-services-safetynet:18.0.1'
// Feel free to use a different http client.
implementation 'com.android.volley:volley:1.2.1'

Update the AndroidManifest to allow internet connection.

xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.INTERNET" />
    ...
</manifest>

Create a subclass of FormFragment that uses reCaptcha.

kotlin
class ReCaptchaFormFragment : FormFragment() {
    private lateinit var queue: RequestQueue

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        queue = Volley.newRequestQueue(requireContext())
    }

    override fun preSubmit(
        interactionAction: InteractionAction,
        keysValues: Map<String, Any>,
        block: (Boolean, Map<String, Any>) -> Unit
    ) {
        // The reCAPTCHA is triggered when the user pushes the submit button.
        // If the reCAPTCHA detects that the user is not a robot then the action of the submit button continues (see handleVerication method).
        // Otherwise, in this example, the action of the submit button is not propagated and an exception is thrown. 
        lifecycleScope.launch {
            SafetyNet.getClient(requireContext())
                .verifyWithRecaptcha("6LcGAr0pAAAAAMeLGV_SITE_KEY") // The site key when generating the reCAPTCHA - Please keep it secret
                .addOnSuccessListener {
                    if (it.tokenResult != null) {
                        handleVerification(it.tokenResult!!, block)
                    } else {
                        block(false, emptyMap())
                        throw IllegalStateException("tokenResult was null")
                    }
                }
                .addOnFailureListener {
                    throw it
                }
        }
    }

    private fun handleVerification(tokenResult: String, block: (Boolean, Map<String, Any>) -> Unit) {
        val siteVerifyUrl = "https://www.google.com/recaptcha/api/siteverify"
        val stringRequest = object : StringRequest(
            Method.POST,
            siteVerifyUrl,
            Response.Listener {
                try {
                    val jsonObject = JSONObject(it)
                    if (jsonObject.getBoolean("success")) {
                        // The user is not a robot then then the block can be invoked.
                        block(true, emptyMap())
                    }
                } catch (exception: Exception) {
                    block(false, emptyMap())
                    throw exception
                }
            },
            Response.ErrorListener {
                block(false, emptyMap())
                throw IllegalStateException(it.message)
            }
        ) {
            override fun getParams(): MutableMap<String, String>? {
                val params = mutableMapOf<String, String>()
                params["secret"] = "6LcGAr0pAAAAABrL8gDs5wi_SECRET_KEY" // The secret key when generating the reCAPTCHA - Please keep it secret
                params["response"] = tokenResult
                return params
            }
        }
        queue.add(stringRequest)
    }

    companion object {
        fun newInstance(uiModel: UIModel) : ReCaptchaFormFragment {
            return ReCaptchaFormFragment().apply {
                arguments = uiModelBundle(uiModel)
            }
        }
    }
}

Override HaapiUIWidgetApplication.fragmentResolver in your application by using ReCaptchaFormFragment.

kotlin
class ClientApplication : Application(), HaapiUIWidgetApplication {
    private val baseUri = URI.create("https://10.0.2.2:8443")
    override val widgetConfiguration: WidgetConfiguration = 
  			WidgetConfiguration.Builder(
            clientId = "clientId",
            baseUri = baseUri,
            tokenEndpointUri = baseUri.resolve("/dev/oauth/token"),
            authorizationEndpointUri = baseUri.resolve("/dev/oauth/authorize"),
            appRedirect = "app://haapi"
        )
            .setKeyStoreAlias("keystore")
            .setHttpUrlConnectionProvider(UNCHECKED_CONNECTION_PROVIDER)
            .build()

    override val fragmentResolver: FragmentResolver = 
  			HaapiFlowFragmentResolver.Builder()
            .customize(FormModel::class.java, ReCaptchaFormFragment::newInstance)
            .build()
}

When the button is pressed and before submiting the form, a reCAPTCHAT is displayed. When identifying successfully as a human, then the block of the submitting button is sent.

reCAPTCHA with subclass formFragment

Providing a custom fragment that subclass HaapiFlowFragment

An existing fragment can be customized by subclassing HaapiFlowFragment when:

With this customization method, it is required to fully implement the fragment behaviour according to the UIModel. More details on HaapiFlowFragment

The following snippet demonstrates a class that implements HaapiFlowFragment for a FormModel.

kotlin
class MyHaapiFlowFragment : HaapiFlowFragment<FormModel>(R.layout.custom_my_haapi_flow_fragment) {
    // Implement the whole behaviour
    // ...

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val formModel = extractUIModelFromArguments<FormModel>()
        // Handle the formModel
    }
    companion object {
        fun newInstance(formModel: FormModel): MyHaapiFlowFragment {
            return MyHaapiFlowFragment().apply {
                arguments = uiModelBundle(
                    model = formModel,
                    param = null
                )
            }
        }
    }
}

Override HaapiUIWidgetApplication.fragmentResolver in your application by using MyHaapiFlowFragment.

kotlin
class ClientApplication : Application(), HaapiUIWidgetApplication {
    private val baseUri = URI.create("https://10.0.2.2:8443")
    override val widgetConfiguration: WidgetConfiguration = 
  			WidgetConfiguration.Builder(
            clientId = "clientId",
            baseUri = baseUri,
            tokenEndpointUri = baseUri.resolve("/dev/oauth/token"),
            authorizationEndpointUri = baseUri.resolve("/dev/oauth/authorize"),
            appRedirect = "app://haapi"
        )
            .setKeyStoreAlias("keystore")
            .setHttpUrlConnectionProvider(UNCHECKED_CONNECTION_PROVIDER)
            .build()

    override val fragmentResolver: FragmentResolver = 
  			HaapiFlowFragmentResolver.Builder()
            .customize(FormModel::class.java, MyHaapiFlowFragment::newInstance)
            .build()
}

Providing custom objects and overriding the mappings of objects that are passed to the UI elements for HAAPI

The previous section focuses on the custom fragment via using different XML, subclassing a fragment or custom fragment of HaapiFlowFragment. This section highlights the possibility of providing custom/modified objects to the fragments. Reminder: HAAPI representations are mapped into UIModel and the latest generates fragments via a FragmentResolver

The requirements are:

The custom object must conform to a UIModel. The UIModel can be divided into 3 categories or types:

Category/Type Description
Interaction An interaction model that requires user interaction.
Operation An operation model requires to launch a service inside or outside the application.
Problem A problem informs something unexpected happened.

The following interfaces conform to UIModel/sub-model and are used by a fragment that is a subclass of HaapiFlowFragment.

UIModel Type Fragment that is a subclass of HaapiFlowFragment
ContinueSameModel Interaction FormFragment
FormModel Interaction FormFragment
GenericModel Interaction GenericFragment
PollingModel Operation PollingFragment
ProblemModel Problem ProblemFragment
SelectorModel Interaction SelectorFragment
WebAuthnOperationModel Operation WebAuthnFragment

The following snippet illustrates a class which adds a new field for FormModel:

kotlin
@Parcelize
data class MyFormModel(
    val userInfo: String, // New field
    override val interactionItems: List<InteractionItem>,
    override val linkItems: List<LinkItemModel>,
    override val messageItems: List<InfoMessageModel>,
    override val templateArea: String?,
    override val viewName: String?
) : FormModel

The following snippet illustrates creating a subclass of HaapiDataMappersFactory by using the new class.

kotlin
class CustomHaapiDataMappersFactory(
    redirectTo: String,
    autoPollingDuration: Duration,
    useDefaultExternalBrowser: Boolean
) : HaapiDataMappersFactory(redirectTo, autoPollingDuration, useDefaultExternalBrowser) {
    override val mapHaapiRepresentationToInteraction: (HaapiRepresentation) -> UIModel.Interaction
        get() = {
            val defaultMappedObject = super.mapHaapiRepresentationToInteraction(it)
            when (it) {
                is InteractiveFormStep -> {
                    (defaultMappedObject as? FormModel)?.let { formModel ->
                        MyFormModel(
                            userInfo = "NEW INFO",
                            interactionItems = formModel.interactionItems,
                            linkItems = formModel.linkItems,
                            messageItems = formModel.messageItems,
                            templateArea = formModel.templateArea,
                            formModel.viewName
                        )
                    } ?: defaultMappedObject
                }
                else -> defaultMappedObject
            }
        }
}

// Uses the CustomHaapiDataMappersFactory in your application that implements HaapiUIWidgetApplication
class ClientApplication : Application(), HaapiUIWidgetApplication {
    private val baseUri = URI.create("https://10.0.2.2:8443")

    override val dataMappersFactory: DataMappersFactory
        get() = CustomHaapiDataMappersFactory(
            redirectTo = widgetConfiguration.haapiConfiguration.appRedirect,
            autoPollingDuration = widgetConfiguration.autoPollingDuration,
            useDefaultExternalBrowser = widgetConfiguration.useDefaultExternalBrowser
        )
  ...
}

A fragment that uses the new MyFormModel can access the client added field as demonstrated below, and an existing fragment that uses the bundled FormModel can access the interface properties and maintain functionality

kotlin
class UsingMyFormModelFragment(layoutId: Int) : FormFragment(layoutId) {

    override fun preRender(uiModel: UIModel?, view: View, savedInstanceState: Bundle?) {
        super.preRender(uiModel, view, savedInstanceState)

        if (uiModel is MyFormModel) {
          	// To be implemented on the UI layout
            Log.d("TO BE IMPLEMENTED", uiModel.userInfo)
        }
    }

    companion object {
        fun newInstance(uiModel: UIModel, layoutId: Int): UsingMyFormModelFragment {
            return UsingMyFormModelFragment(layoutId).apply {
                arguments = uiModelBundle(uiModel)
            }
        }
    }
}

// Update the class that implements HaapiUIWidgetApplication
class ClientApplication : Application(), HaapiUIWidgetApplication {
  ...
    override val fragmentResolver: FragmentResolver = 
  			HaapiFlowFragmentResolver.Builder()
            .customize(FormModel::class.java { model ->
              UsingMyFormModelFragment.newInstance(uiModel, R.layout.fragment_redesign_form)
            }
            .build()
  ...
}