Skip to content

Commit

Permalink
Merge pull request #454 from ForgeRock/user-profile-sample
Browse files Browse the repository at this point in the history
Enhance Sample App to support IDM related Callbacks.
  • Loading branch information
spetrov authored Oct 1, 2024
2 parents c8af564 + 43db5cb commit 7fea231
Show file tree
Hide file tree
Showing 11 changed files with 444 additions and 2 deletions.
15 changes: 14 additions & 1 deletion samples/app/src/main/java/com/example/app/AppDrawer.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2023 ForgeRock. All rights reserved.
* Copyright (c) 2023 - 2024 ForgeRock. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
Expand All @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountBox
import androidx.compose.material.icons.filled.Fence
import androidx.compose.material.icons.filled.Fingerprint
import androidx.compose.material.icons.filled.GeneratingTokens
Expand Down Expand Up @@ -42,6 +43,7 @@ import com.example.app.Destinations.DEVICE_PROFILE
import com.example.app.Destinations.ENV_ROUTE
import com.example.app.Destinations.IG
import com.example.app.Destinations.LAUNCH_ROUTE
import com.example.app.Destinations.USER_PROFILE
import com.example.app.Destinations.MANAGE_USER_KEYS
import com.example.app.Destinations.MANAGE_WEBAUTHN_KEYS
import com.example.app.Destinations.SETTING
Expand Down Expand Up @@ -136,6 +138,17 @@ fun AppDrawer(
},
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
NavigationDrawerItem(
label = { Text("User Profile") },
selected = false,
icon = { Icon(Icons.Filled.AccountBox, null) },
onClick = {
navigateTo(USER_PROFILE);
closeDrawer()
},
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)

NavigationDrawerItem(
label = { Text("Setting") },
selected = false,
Expand Down
31 changes: 31 additions & 0 deletions samples/app/src/main/java/com/example/app/AppNavHost.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

package com.example.app

import android.net.Uri
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
Expand All @@ -22,6 +23,7 @@ import com.example.app.device.DeviceProfileRoute
import com.example.app.device.DeviceProfileViewModel
import com.example.app.env.EnvRoute
import com.example.app.env.EnvViewModel
import com.example.app.env.USER_PROFILE_JOURNEY
import com.example.app.ig.IGRoute
import com.example.app.ig.IGViewModel
import com.example.app.journey.Journey
Expand All @@ -37,6 +39,10 @@ import com.example.app.userprofile.UserProfile
import com.example.app.userprofile.UserProfileViewModel
import com.example.app.webauthn.WebAuthRoute
import com.example.app.webauthn.WebAuthnViewModel
import org.forgerock.android.auth.Action
import org.forgerock.android.auth.FRRequestInterceptor
import org.forgerock.android.auth.Request
import org.forgerock.android.auth.RequestInterceptorRegistry

@Composable
fun AppNavHost(navController: NavHostController,
Expand Down Expand Up @@ -97,6 +103,31 @@ fun AppNavHost(navController: NavHostController,
DeviceProfileRoute(viewModel)
}

composable(Destinations.USER_PROFILE) {

RequestInterceptorRegistry.getInstance()
.register(object : FRRequestInterceptor<Action> {
override fun intercept(request: Request, tag: Action?): Request {
return if (tag?.payload?.getString("tree").equals(USER_PROFILE_JOURNEY)) {
request.newBuilder()
.url(Uri.parse(request.url().toString())
.buildUpon()
.appendQueryParameter("ForceAuth", "true").toString())
.build()
} else request
}
})


val journeyViewModel = viewModel<JourneyViewModel<String>>(
factory = JourneyViewModel.factory(LocalContext.current, USER_PROFILE_JOURNEY)
)
Journey(journeyViewModel, onSuccess = {
navController.navigate(Destinations.USER_SESSION)
}) {
}
}

composable(Destinations.JOURNEY_ROUTE + "/{name}", arguments = listOf(
navArgument("name") { type = NavType.StringType }
)) {
Expand Down
1 change: 1 addition & 0 deletions samples/app/src/main/java/com/example/app/Destinations.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ object Destinations {
const val MANAGE_USER_KEYS = "User Keys"
const val IG = "IG protected endpoint"
const val DEVICE_PROFILE = "Device Profile"
const val USER_PROFILE = "User"
const val SETTING = "Setting"
const val CENTRALIZE_ROUTE = "Centralize Login"
const val USER_SESSION = "User Session"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright (c) 2024 ForgeRock. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/

package com.example.app.callback

import org.forgerock.android.auth.callback.AttributeInputCallback

val VALIDATION_ERROR = mutableMapOf(
"VALID_USERNAME" to "Invalid username",
"UNIQUE" to "Must be unique",
"MIN_LENGTH" to "{prompt} Must be at least {minLength} characters long",
"MAX_LENGTH" to "{prompt} Must be less than {maxLength} characters long",
"AT_LEAST_X_CAPITAL_LETTERS" to "{prompt} Must have at least {numCaps} capital letter(s)",
"AT_LEAST_X_NUMBERS" to "{prompt} Must have at least {numNums} number(s)",
"VALID_EMAIL_ADDRESS_FORMAT" to "Invalid email format (example@example.com)",
"CANNOT_CONTAIN_CHARACTERS" to "{prompt} Cannot contain characters: {forbiddenChars}",
"CANNOT_CONTAIN_DUPLICATES" to "Cannot contain duplicates: {duplicateValue}",
"REQUIRED" to "Cannot be blank",
"MATCH_REGEXP" to "Has to match pattern: {regexp}",
"VALID_TYPE" to "Must be one of the following types: {validTypes}. Cannot be {invalidType}",
"VALID_NUMBER" to "{prompt} Invalid number",
"MINIMUM_NUMBER_VALUE" to "{prompt} Less than minimum number value",
"MAXIMUM_NUMBER_VALUE" to "{prompt} Greater than maximum number value",
"VALID_NAME_FORMAT" to "Must have valid name characters",
"VALID_PHONE_FORMAT" to "Must be a valid phone number",
"CANNOT_CONTAIN_DUPLICATES" to "{prompt} must not contain duplicates {duplicateValue}",
"CANNOT_CONTAIN_OTHERS" to "{prompt} Must not share characters with {disallowedFields}",
"VALID_DATE" to "Must be a valid date",
"IS_NEW" to "Must not be the same as the previous {historyLength} password(s)",
"IS_NUMBER" to "Must be a number greater than or equal to zero.",
"IS_NUMBER_GREATER_ZERO" to "Must be a number greater than or equal to 1.",
"MIN_ITEMS" to "Must have at least {minItems} value(s)",
"MUST_CONTAIN_LOWERCASE_CHARACTERS" to "Must have at least {minItems} lowercase letter(s)",
"MUST_CONTAIN_SPECIAL_CHARACTERS" to "Must have at least {minItems} of these characters: {requiredChars}",
"VALID_BOOLEAN" to "Value must be of type boolean.",
"VALID_INT" to "Value must be of type integer.",
)


fun AttributeInputCallback.error(): String {
return failedPolicies.joinToString("\n") { policy ->
VALIDATION_ERROR[policy.policyRequirement]?.let { error ->
policy.format(prompt, error)
} ?: ""
}
}

fun AttributeInputCallback.hasError(): Boolean {
return failedPolicies.isNotEmpty()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2024 ForgeRock. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/

package com.example.app.callback

import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import org.forgerock.android.auth.callback.BooleanAttributeInputCallback

@Composable
fun BooleanAttributeInputCallback(callback: BooleanAttributeInputCallback) {

var input by remember {
mutableStateOf(callback.value)
}

Row(modifier = Modifier
.padding(4.dp)
.fillMaxWidth()) {
Text(text = callback.prompt)
Spacer(modifier = Modifier.weight(1f, true))
Switch(
checked = input,
onCheckedChange = {
input = it
callback.value = it
}
)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Copyright (c) 2024 ForgeRock. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/

package com.example.app.callback

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import org.forgerock.android.auth.callback.ChoiceCallback
import org.forgerock.android.auth.callback.KbaCreateCallback

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun KbaCreateCallback(callback: KbaCreateCallback) {
var expanded by remember { mutableStateOf(false) }
var selectedItem by remember {
mutableStateOf(callback.predefinedQuestions[0])
}

var answer by remember {
mutableStateOf("")
}

Column(modifier = Modifier
.padding(8.dp)
.fillMaxWidth()) {
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
expanded = !expanded
}
) {

// text field
TextField(
modifier = Modifier.menuAnchor(),
value = selectedItem,
onValueChange = {},
readOnly = true,
label = { Text(text = callback.prompt) },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded
)
},
colors = ExposedDropdownMenuDefaults.textFieldColors()
)

// menu
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
callback.predefinedQuestions.forEachIndexed { index, selectedOption ->
// menu item
DropdownMenuItem(text = {
Text(text = selectedOption)
}, onClick = {
selectedItem = selectedOption
expanded = false
callback.setSelectedQuestion(callback.predefinedQuestions[index])
})
}
}
}
OutlinedTextField(
modifier = Modifier,
value = answer,
onValueChange = { value ->
answer = value
callback.setSelectedAnswer(answer)
},
label = { Text(text = "Answer") },
)

}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright (c) 2024 ForgeRock. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/

package com.example.app.callback

import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.text.isDigitsOnly
import org.forgerock.android.auth.callback.NameCallback
import org.forgerock.android.auth.callback.NumberAttributeInputCallback
import org.json.JSONObject

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NumberAttributeInputCallback(callback: NumberAttributeInputCallback) {

var input by remember {
mutableStateOf(callback.value?.toString() ?: "")
}

Row(modifier = Modifier
.padding(4.dp)
.fillMaxWidth()) {
OutlinedTextField(
modifier = Modifier,
value = input,
onValueChange = { value ->
if (value.isDigitsOnly()) {
input = value
if (input.isNotEmpty()) {
callback.value = input.toDouble()
}
}
},
isError = callback.hasError(),
supportingText = if (callback.hasError()) {
@Composable {
Text(
text = callback.error(),
style = MaterialTheme.typography.bodySmall
)
}
} else null,
label = { Text(callback.prompt) },
)
}
}
Loading

0 comments on commit 7fea231

Please sign in to comment.