diff --git a/samples/app/src/main/java/com/example/app/AppDrawer.kt b/samples/app/src/main/java/com/example/app/AppDrawer.kt index 143e3da8..0853f284 100644 --- a/samples/app/src/main/java/com/example/app/AppDrawer.kt +++ b/samples/app/src/main/java/com/example/app/AppDrawer.kt @@ -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. @@ -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 @@ -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 @@ -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, diff --git a/samples/app/src/main/java/com/example/app/AppNavHost.kt b/samples/app/src/main/java/com/example/app/AppNavHost.kt index 48c33086..cbd62c3c 100644 --- a/samples/app/src/main/java/com/example/app/AppNavHost.kt +++ b/samples/app/src/main/java/com/example/app/AppNavHost.kt @@ -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 @@ -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 @@ -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, @@ -97,6 +103,31 @@ fun AppNavHost(navController: NavHostController, DeviceProfileRoute(viewModel) } + composable(Destinations.USER_PROFILE) { + + RequestInterceptorRegistry.getInstance() + .register(object : FRRequestInterceptor { + 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>( + 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 } )) { diff --git a/samples/app/src/main/java/com/example/app/Destinations.kt b/samples/app/src/main/java/com/example/app/Destinations.kt index 910b1a34..65a3b05f 100644 --- a/samples/app/src/main/java/com/example/app/Destinations.kt +++ b/samples/app/src/main/java/com/example/app/Destinations.kt @@ -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" diff --git a/samples/app/src/main/java/com/example/app/callback/AttributeInput.kt b/samples/app/src/main/java/com/example/app/callback/AttributeInput.kt new file mode 100644 index 00000000..2d6d1a0d --- /dev/null +++ b/samples/app/src/main/java/com/example/app/callback/AttributeInput.kt @@ -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() +} \ No newline at end of file diff --git a/samples/app/src/main/java/com/example/app/callback/BooleanAttributeInputCallback.kt b/samples/app/src/main/java/com/example/app/callback/BooleanAttributeInputCallback.kt new file mode 100644 index 00000000..2227d348 --- /dev/null +++ b/samples/app/src/main/java/com/example/app/callback/BooleanAttributeInputCallback.kt @@ -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 + } + ) + } + +} \ No newline at end of file diff --git a/samples/app/src/main/java/com/example/app/callback/KbaCreateCallback.kt b/samples/app/src/main/java/com/example/app/callback/KbaCreateCallback.kt new file mode 100644 index 00000000..3130ab7d --- /dev/null +++ b/samples/app/src/main/java/com/example/app/callback/KbaCreateCallback.kt @@ -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") }, + ) + + } + +} + diff --git a/samples/app/src/main/java/com/example/app/callback/NumberAttributeInputCallback.kt b/samples/app/src/main/java/com/example/app/callback/NumberAttributeInputCallback.kt new file mode 100644 index 00000000..e1484946 --- /dev/null +++ b/samples/app/src/main/java/com/example/app/callback/NumberAttributeInputCallback.kt @@ -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) }, + ) + } +} \ No newline at end of file diff --git a/samples/app/src/main/java/com/example/app/callback/StringAttributeInputCallback.kt b/samples/app/src/main/java/com/example/app/callback/StringAttributeInputCallback.kt new file mode 100644 index 00000000..aac6b1ed --- /dev/null +++ b/samples/app/src/main/java/com/example/app/callback/StringAttributeInputCallback.kt @@ -0,0 +1,56 @@ +/* + * 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.unit.dp +import org.forgerock.android.auth.callback.StringAttributeInputCallback + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun StringAttributeInputCallback(callback: StringAttributeInputCallback) { + + var input by remember { + mutableStateOf(callback.value) + } + + Row(modifier = Modifier + .padding(4.dp) + .fillMaxWidth()) { + OutlinedTextField( + modifier = Modifier, + value = input, + onValueChange = { value -> + input = value + callback.value = input + }, + isError = callback.hasError(), + supportingText = if (callback.hasError()) { + @Composable { + Text( + text = callback.error(), + style = MaterialTheme.typography.bodySmall + ) + } + } else null, + label = { Text(callback.prompt) }, + ) + } +} \ No newline at end of file diff --git a/samples/app/src/main/java/com/example/app/callback/TermsAndConditionsCallback.kt b/samples/app/src/main/java/com/example/app/callback/TermsAndConditionsCallback.kt new file mode 100644 index 00000000..b77d7285 --- /dev/null +++ b/samples/app/src/main/java/com/example/app/callback/TermsAndConditionsCallback.kt @@ -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.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +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.TermsAndConditionsCallback + +@Composable +fun TermsAndConditionsCallback(callback: TermsAndConditionsCallback) { + + var input by remember { + mutableStateOf(false) + } + + Row(modifier = Modifier + .padding(16.dp) + .fillMaxWidth()) { + Text(text = callback.version, + Modifier + .weight(1f), + style = MaterialTheme.typography.titleMedium + ) + Spacer(Modifier.width(8.dp)) + Text(text = callback.createDate, + Modifier + .weight(1f), + style = MaterialTheme.typography.titleMedium + ) + Spacer(Modifier.width(8.dp)) + Text(text = callback.terms, + Modifier + .weight(1f), + style = MaterialTheme.typography.titleSmall + ) + Spacer(Modifier.width(8.dp)) + Switch( + checked = input, + onCheckedChange = { + input = it + callback.setAccept(it) + } + ) + } + +} \ No newline at end of file diff --git a/samples/app/src/main/java/com/example/app/env/EnvViewModel.kt b/samples/app/src/main/java/com/example/app/env/EnvViewModel.kt index 6e3fdfe9..8e3cb893 100644 --- a/samples/app/src/main/java/com/example/app/env/EnvViewModel.kt +++ b/samples/app/src/main/java/com/example/app/env/EnvViewModel.kt @@ -21,6 +21,8 @@ import org.forgerock.android.auth.Logger private val TAG = EnvViewModel::class.java.simpleName +const val USER_PROFILE_JOURNEY = "UserProfile" + class EnvViewModel : ViewModel() { val servers = mutableListOf() diff --git a/samples/app/src/main/java/com/example/app/journey/Journey.kt b/samples/app/src/main/java/com/example/app/journey/Journey.kt index 2a692a42..5196dcc7 100644 --- a/samples/app/src/main/java/com/example/app/journey/Journey.kt +++ b/samples/app/src/main/java/com/example/app/journey/Journey.kt @@ -25,13 +25,16 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.example.app.Error import com.example.app.callback.AppIntegrityCallback +import com.example.app.callback.BooleanAttributeInputCallback import com.example.app.callback.ChoiceCallback import com.example.app.callback.ConfirmationCallback import com.example.app.callback.DeviceBindingCallback import com.example.app.callback.DeviceProfileCallback import com.example.app.callback.DeviceSigningVerifierCallback import com.example.app.callback.IdPCallback +import com.example.app.callback.KbaCreateCallback import com.example.app.callback.NameCallback +import com.example.app.callback.NumberAttributeInputCallback import com.example.app.callback.PasswordCallback import com.example.app.callback.PingOneProtectEvaluationCallback import com.example.app.callback.PingOneProtectInitializeCallback @@ -53,11 +56,18 @@ import org.forgerock.android.auth.callback.NameCallback import org.forgerock.android.auth.callback.PasswordCallback import org.forgerock.android.auth.callback.PollingWaitCallback import com.example.app.callback.ReCaptchaCallback +import com.example.app.callback.StringAttributeInputCallback +import com.example.app.callback.TermsAndConditionsCallback import org.forgerock.android.auth.PingOneProtectEvaluationCallback import org.forgerock.android.auth.PingOneProtectInitializeCallback +import org.forgerock.android.auth.callback.BooleanAttributeInputCallback +import org.forgerock.android.auth.callback.KbaCreateCallback +import org.forgerock.android.auth.callback.NumberAttributeInputCallback import org.forgerock.android.auth.callback.ReCaptchaCallback import org.forgerock.android.auth.callback.ReCaptchaEnterpriseCallback import org.forgerock.android.auth.callback.SelectIdPCallback +import org.forgerock.android.auth.callback.StringAttributeInputCallback +import org.forgerock.android.auth.callback.TermsAndConditionsCallback import org.forgerock.android.auth.callback.TextInputCallback import org.forgerock.android.auth.callback.TextOutputCallback import org.forgerock.android.auth.callback.WebAuthnAuthenticationCallback @@ -177,7 +187,11 @@ fun Journey(state: JourneyState, } is TextInputCallback -> TextInputCallback(it) - + is KbaCreateCallback -> KbaCreateCallback(it) + is NumberAttributeInputCallback -> NumberAttributeInputCallback(it) + is StringAttributeInputCallback -> StringAttributeInputCallback(it) + is BooleanAttributeInputCallback -> BooleanAttributeInputCallback(it) + is TermsAndConditionsCallback -> TermsAndConditionsCallback(it) else -> { //Unsupported