diff --git a/.gitignore b/.gitignore index a674bf8..a66c9a0 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ out/ # Gradle files .gradle/ build/ +.kotlin # Local configuration file (sdk path, etc) local.properties diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 65d4004..8b23461 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ androidx-activityCompose = "1.8.0" kotlinStdlibJdk8 = "1.7.20" lifecycleViewmodelKtx = "2.8.1" compose-plugin = "1.6.10" -multiplatformSettings = "1.0.0" +multiplatformSettings = "1.2.0" navigationCompose = "2.7.7" navigationComposeVersion = "2.7.0-alpha07" dagger-hilt = "2.48.1" @@ -32,6 +32,22 @@ testng = "6.9.6" monitor = "1.6.1" runnerVersion = "1.5.2" +kotlinxCoroutines = "1.9.0" +protobufPlugin = "0.9.4" +protobuf = "4.26.0" +kotlinxSerializationJson = "1.7.3" +composeJB = "1.7.0" +composeLifecycle = "2.8.3" +jbCoreBundle = "1.0.1" +jbSavedState = "1.2.2" +composeNavigation = "2.8.0-alpha10" +kotlinxImmutable = "0.3.8" +androidxLifecycle = "2.8.7" +androidxTracing = "1.3.0-alpha02" +koin = "4.0.1-RC1" +androidxNavigation = "2.8.5" +koinAnnotationsVersion = "1.4.0-RC4" + [libraries] android-maven-gradle-plugin = { module = "com.github.dcendents:android-maven-gradle-plugin", version.ref = "androidMavenGradlePlugin" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } @@ -68,6 +84,43 @@ testng = { group = "org.testng", name = "testng", version.ref = "testng" } androidx-monitor = { group = "androidx.test", name = "monitor", version.ref = "monitor" } runner = { group = "androidx.test", name = "runner", version.ref = "runnerVersion" } +kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" } +jb-kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" } +kotlin-reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "kotlin" } +protobuf-kotlin-lite = { group = "com.google.protobuf", name = "protobuf-kotlin-lite", version.ref = "protobuf" } +kotlinx-serialization-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-core", version.ref = "kotlinxSerializationJson" } +multiplatform-settings = { group = "com.russhwolf", name = "multiplatform-settings-no-arg", version.ref = "multiplatformSettings" } +multiplatform-settings-coroutines = { group = "com.russhwolf", name = "multiplatform-settings-coroutines", version.ref = "multiplatformSettings" } +multiplatform-settings-serialization = { group = "com.russhwolf", name = "multiplatform-settings-serialization", version.ref = "multiplatformSettings" } +kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } +kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinxCoroutines" } +jb-composeRuntime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "composeJB" } +jb-composeViewmodel = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "composeLifecycle" } +jb-lifecycleViewmodel = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel", version.ref = "composeLifecycle" } +jb-lifecycleViewmodelSavedState = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "composeLifecycle" } +jb-bundle = { module = "org.jetbrains.androidx.core:core-bundle", version.ref = "jbCoreBundle" } +jb-savedstate = { module = "org.jetbrains.androidx.savedstate:savedstate", version.ref = "jbSavedState" } +jb-composeNavigation = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "composeNavigation" } +kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinxImmutable" } +androidx-tracing-ktx = { group = "androidx.tracing", name = "tracing-ktx", version.ref = "androidxTracing" } +androidx-lifecycle-runtimeCompose-alt = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" } +androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" } +koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" } +koin-androidx-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin" } +koin-androidx-navigation = { group = "io.insert-koin", name = "koin-androidx-navigation", version.ref = "koin" } +koin-core-viewmodel = { group = "io.insert-koin", name = "koin-core-viewmodel", version.ref = "koin" } +koin-test-junit4 = { group = "io.insert-koin", name = "koin-test-junit4", version.ref = "koin" } +androidx-navigation-testing = { group = "androidx.navigation", name = "navigation-testing", version.ref = "androidxNavigation" } +androidx-compose-ui-test = { group = "androidx.compose.ui", name = "ui-test-junit4" } +androidx-lifecycle-runtimeTesting = { group = "androidx.lifecycle", name = "lifecycle-runtime-testing", version.ref = "androidxLifecycle" } +koin-compose-viewmodel = { group = "io.insert-koin", name = "koin-compose-viewmodel", version.ref = "koin" } +koin-compose = { group = "io.insert-koin", name = "koin-compose", version.ref = "koin" } +koin-bom = { group = "io.insert-koin", name = "koin-bom", version.ref = "koin" } +koin-core = { group = "io.insert-koin", name = "koin-core", version.ref = "koin" } +koin-annotations = { group = "io.insert-koin", name = "koin-annotations", version.ref = "koinAnnotationsVersion" } +koin-ksp-compiler = { group = "io.insert-koin", name = "koin-ksp-compiler", version.ref = "koinAnnotationsVersion" } +koin-test = { group = "io.insert-koin", name = "koin-test", version.ref = "koin" } + [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } androidLibrary = { id = "com.android.library", version.ref = "agp" } @@ -78,3 +131,13 @@ kotlinCocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", version.ref = compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } dagger-hilt = { id = "com.google.dagger.hilt.android", version.ref = "dagger-hilt" } jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } + + +mifospay-kmp-koin = { id = "mifospay.kmp.koin", version = "unspecified" } +mifos-detekt-plugin = { id = "mifos.detekt.plugin", version = "unspecified" } +mifos-spotless-plugin = { id = "mifos.spotless.plugin", version = "unspecified" } +mifospay-kmp-library = { id = "mifospay.kmp.library", version = "unspecified" } +mifospay-cmp-feature = { id = "mifospay.cmp.feature", version = "unspecified" } +kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" } diff --git a/mifos-passcode/build.gradle b/mifos-passcode/build.gradle index 2fc17de..2bae4c0 100644 --- a/mifos-passcode/build.gradle +++ b/mifos-passcode/build.gradle @@ -1,6 +1,19 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { alias(libs.plugins.androidLibrary) - alias(libs.plugins.kotlinAndroid) + alias(libs.plugins.kotlinMultiplatform) +// alias(libs.plugins.mifospay.kmp.library) +// alias(libs.plugins.mifospay.kmp.koin) +// alias(libs.plugins.mifos.detekt.plugin) +// alias(libs.plugins.mifos.spotless.plugin) +// alias(libs.plugins.mifospay.cmp.feature) +// alias(libs.plugins.kotlin.parcelize) +// alias(com.google.devtools.ksp) + alias(libs.plugins.devToolsKsp) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.protobuf) } ext { @@ -9,7 +22,7 @@ ext { publishedGroupId = 'com.mifos.mobile' libraryName = 'mifos-passcode' - artifact = 'mifos-passcode' // artifact name and library name should be same. + artifact = 'mifos-passcode' libraryDescription = 'A Library as feature of passcode' @@ -53,22 +66,102 @@ android { buildFeatures { viewBinding = true } - kotlinOptions { - jvmTarget = '17' - } } -dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) +kotlin { + androidTarget { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } + } + + sourceSets { + + commonTest.dependencies{ + implementation(libs.kotlinx.coroutines.test) + implementation(libs.kotlin.test) + implementation(libs.koin.test) + implementation libs.junit + } - implementation libs.androidx.appcompat - implementation libs.material + commonMain { + dependencies { + + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation libs.androidx.appcompat + implementation libs.material + implementation libs.androidx.core.ktx.v1131 + + // This Belongs To Koinlibrary + implementation(libs.koin.bom) + implementation(libs.koin.core) + implementation(libs.koin.annotations) + implementation(libs.koin.ksp.compiler) + implementation(libs.koin.compose.viewmodel) + implementation(libs.koin.compose) + implementation(libs.koin.core.viewmodel) + implementation(libs.koin.test.junit4) + + implementation(libs.jb.composeRuntime) + implementation(libs.jb.composeViewmodel) + implementation(libs.jb.lifecycleViewmodel) + implementation(libs.jb.lifecycleViewmodelSavedState) + implementation(libs.jb.savedstate) + implementation(libs.jb.bundle) + implementation(libs.jb.composeNavigation) + implementation(libs.kotlinx.collections.immutable) + + implementation(libs.jb.kotlin.stdlib) + implementation(libs.kotlin.reflect) + + api(libs.protobuf.kotlin.lite) + implementation(libs.kotlinx.serialization.core) + + implementation(libs.multiplatform.settings) + implementation(libs.multiplatform.settings.serialization) + implementation(libs.multiplatform.settings.coroutines) + + implementation(libs.kotlinx.coroutines.core) + +// implementation(compose.ui) +// implementation(compose.foundation) +// implementation(compose.material3) +// implementation(compose.materialIconsExtended) +// implementation(compose.components.resources) +// implementation(compose.components.uiToolingPreview) + } + } - testImplementation libs.junit - androidTestImplementation libs.runner - androidTestImplementation libs.androidx.espresso.core - implementation libs.androidx.core.ktx.v1131 - implementation platform(libs.kotlin.bom) + androidMain { + dependencies { + implementation(libs.androidx.appcompat) + implementation(libs.material) + implementation(libs.androidx.lifecycle.runtimeCompose.alt) + implementation(libs.androidx.lifecycle.viewModelCompose) + implementation(libs.androidx.tracing.ktx) + + implementation(libs.androidx.navigation.testing) + implementation(libs.androidx.compose.ui.test) + implementation(libs.androidx.lifecycle.runtimeTesting) + + implementation(libs.koin.android) + implementation(libs.koin.androidx.compose) + implementation(libs.koin.androidx.navigation) + } + } + + androidTest.dependencies{ + implementation(libs.runner) + implementation(libs.androidx.espresso.core) + } + + desktopMain { + dependencies { + implementation(libs.kotlinx.coroutines.swing) + } + } + } } tasks.withType(Javadoc) { @@ -81,10 +174,11 @@ tasks.withType(Javadoc) { repositories { mavenCentral() } + // Used this article to release the library // https://inthecheesefactory.com/blog/how-to-upload-library-to-jcenter-maven-central-as-dependency/en // Add these line in local.properties to direct release on bintray //bintray.user=YOUR_BINTRAY_USERNAME //bintray.apikey=YOUR_BINTRAY_API_KEY -//bintray.gpg.password=YOUR_GPG_PASSWORD \ No newline at end of file +//bintray.gpg.password=YOUR_GPG_PASSWORD diff --git a/mifos-passcode/src/androidMain/AndroidManifest.xml b/mifos-passcode/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000..b9b046d --- /dev/null +++ b/mifos-passcode/src/androidMain/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/mifos-passcode/src/commonMain/composeResources/drawable/lib_mifos_passcode_mifos_logo.jpg b/mifos-passcode/src/commonMain/composeResources/drawable/lib_mifos_passcode_mifos_logo.jpg new file mode 100644 index 0000000..a067a3c Binary files /dev/null and b/mifos-passcode/src/commonMain/composeResources/drawable/lib_mifos_passcode_mifos_logo.jpg differ diff --git a/mifos-passcode/src/commonMain/composeResources/font/lib_mifos_passcode_lato_black.ttf b/mifos-passcode/src/commonMain/composeResources/font/lib_mifos_passcode_lato_black.ttf new file mode 100644 index 0000000..4340502 Binary files /dev/null and b/mifos-passcode/src/commonMain/composeResources/font/lib_mifos_passcode_lato_black.ttf differ diff --git a/mifos-passcode/src/commonMain/composeResources/font/lib_mifos_passcode_lato_bold.ttf b/mifos-passcode/src/commonMain/composeResources/font/lib_mifos_passcode_lato_bold.ttf new file mode 100644 index 0000000..016068b Binary files /dev/null and b/mifos-passcode/src/commonMain/composeResources/font/lib_mifos_passcode_lato_bold.ttf differ diff --git a/mifos-passcode/src/commonMain/composeResources/font/lib_mifos_passcode_lato_regular.ttf b/mifos-passcode/src/commonMain/composeResources/font/lib_mifos_passcode_lato_regular.ttf new file mode 100644 index 0000000..bb2e887 Binary files /dev/null and b/mifos-passcode/src/commonMain/composeResources/font/lib_mifos_passcode_lato_regular.ttf differ diff --git a/mifos-passcode/src/commonMain/composeResources/values/strings.xml b/mifos-passcode/src/commonMain/composeResources/values/strings.xml new file mode 100644 index 0000000..4398748 --- /dev/null +++ b/mifos-passcode/src/commonMain/composeResources/values/strings.xml @@ -0,0 +1,34 @@ + + + + Passcode will be reset, are you sure? + Cancel + Yes + Passcode + hasPasscode + hasDragPasscode + passcode + drag_passcode + + Create Passcode + Confirm Passcode + Enter your Passcode + Forgot Passcode + Delete Passcode Key Button + Drag your finger here only in one direction. + Drag your Pattern + Exit + Skip + Forgot Passcode, Login Manually + Try again + Passcode do not match! + Are you sure you want to exit? + \ No newline at end of file diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt new file mode 100644 index 0000000..ba9cf7b --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt @@ -0,0 +1,248 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.Animatable +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +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.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.launch +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.koin.compose.viewmodel.koinViewModel +import org.mifos.library.passcode.component.MifosIcon +import org.mifos.library.passcode.component.PasscodeForgotButton +import org.mifos.library.passcode.component.PasscodeHeader +import org.mifos.library.passcode.component.PasscodeKeys +import org.mifos.library.passcode.component.PasscodeMismatchedDialog +import org.mifos.library.passcode.component.PasscodeSkipButton +import org.mifos.library.passcode.component.PasscodeToolbar +import org.mifos.library.passcode.theme.blueTint +import org.mifos.library.passcode.utility.Constants.PASSCODE_LENGTH +import org.mifos.library.passcode.utility.ShakeAnimation.performShakeAnimation +import org.mifos.library.passcode.viewmodels.PasscodeAction +import org.mifos.library.passcode.viewmodels.PasscodeEvent +import org.mifos.library.passcode.viewmodels.PasscodeViewModel +import org.mifospay.core.ui.utils.EventsEffect + +@Composable +internal fun PasscodeScreen( + onForgotButton: () -> Unit, + onSkipButton: () -> Unit, + onPasscodeConfirm: (String) -> Unit, + onPasscodeRejected: () -> Unit, + modifier: Modifier = Modifier, + viewModel: PasscodeViewModel = koinViewModel(), +) { + val scope = rememberCoroutineScope() + val state by viewModel.stateFlow.collectAsStateWithLifecycle() + + val xShake = remember { Animatable(initialValue = 0.0F) } + var passcodeRejectedDialogVisible by remember { mutableStateOf(false) } + + EventsEffect(viewModel) { event -> + when (event) { + is PasscodeEvent.PasscodeConfirmed -> { + onPasscodeConfirm(event.passcode) + } + + is PasscodeEvent.PasscodeRejected -> { + passcodeRejectedDialogVisible = true + scope.launch { + performShakeAnimation(xShake) + } + onPasscodeRejected() + } + } + } + + Scaffold( + modifier = modifier + .fillMaxSize(), + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + .padding(paddingValues), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + PasscodeToolbar(activeStep = state.activeStep, state.hasPasscode) + + PasscodeSkipButton( + hasPassCode = state.hasPasscode, + onSkipButton = onSkipButton, + ) + + MifosIcon(modifier = Modifier.fillMaxWidth()) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp, bottom = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + PasscodeHeader( + activeStep = state.activeStep, + isPasscodeAlreadySet = state.hasPasscode, + ) + PasscodeView( + restart = remember(viewModel) { + { viewModel.trySendAction(PasscodeAction.Restart) } + }, + togglePasscodeVisibility = remember(viewModel) { + { viewModel.trySendAction(PasscodeAction.TogglePasscodeVisibility) } + }, + filledDots = state.filledDots, + passcodeVisible = state.passcodeVisible, + currentPasscode = state.currentPasscodeInput, + passcodeRejectedDialogVisible = passcodeRejectedDialogVisible, + onDismissDialog = { passcodeRejectedDialogVisible = false }, + xShake = xShake, + ) + } + + Spacer(modifier = Modifier.height(6.dp)) + + PasscodeKeys( + enterKey = remember(viewModel) { + { viewModel.trySendAction(PasscodeAction.EnterKey(it)) } + }, + deleteKey = remember(viewModel) { + { viewModel.trySendAction(PasscodeAction.DeleteKey) } + }, + deleteAllKeys = remember(viewModel) { + { viewModel.trySendAction(PasscodeAction.DeleteAllKeys) } + }, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + PasscodeForgotButton( + hasPassCode = state.hasPasscode, + onForgotButton = onForgotButton, + ) + } + } +} + +@Composable +private fun PasscodeView( + restart: () -> Unit, + togglePasscodeVisibility: () -> Unit, + filledDots: Int, + passcodeVisible: Boolean, + currentPasscode: String, + passcodeRejectedDialogVisible: Boolean, + onDismissDialog: () -> Unit, + xShake: Animatable, + modifier: Modifier = Modifier, +) { + PasscodeMismatchedDialog( + visible = passcodeRejectedDialogVisible, + onDismiss = { + onDismissDialog.invoke() + restart() + }, + ) + + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Row( + modifier = Modifier.offset { IntOffset(xShake.value.toInt(), 0) }, + horizontalArrangement = Arrangement.spacedBy( + space = 26.dp, + alignment = Alignment.CenterHorizontally, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + repeat(PASSCODE_LENGTH) { dotIndex -> + if (passcodeVisible && dotIndex < currentPasscode.length) { + Text( + text = currentPasscode[dotIndex].toString(), + color = blueTint, + ) + } else { + val isFilledDot = dotIndex + 1 <= filledDots + val dotColor = animateColorAsState( + if (isFilledDot) blueTint else Color.Gray, + label = "", + ) + + Box( + modifier = Modifier + .size(14.dp) + .background( + color = dotColor.value, + shape = CircleShape, + ), + ) + } + } + } + + IconButton( + onClick = togglePasscodeVisibility, + modifier = Modifier.padding(start = 10.dp), + ) { + Icon( + imageVector = if (passcodeVisible) { + Icons.Filled.Visibility + } else { + Icons.Filled.VisibilityOff + }, + contentDescription = null, + ) + } + } +} + +@Preview +@Composable +private fun PasscodeScreenPreview() { + PasscodeScreen( + onForgotButton = {}, + onSkipButton = {}, + onPasscodeConfirm = {}, + onPasscodeRejected = {}, + ) +} diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PasscodeNavigation.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PasscodeNavigation.kt new file mode 100644 index 0000000..eab00ee --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PasscodeNavigation.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable + +const val PASSCODE_SCREEN = "passcode_screen" + +fun NavGraphBuilder.passcodeRoute( + onForgotButton: () -> Unit, + onSkipButton: () -> Unit, + onPasscodeConfirm: (String) -> Unit, + onPasscodeRejected: () -> Unit, +) { + composable( + route = PASSCODE_SCREEN, + ) { + PasscodeScreen( + onForgotButton = onForgotButton, + onSkipButton = onSkipButton, + onPasscodeConfirm = onPasscodeConfirm, + onPasscodeRejected = onPasscodeRejected, + ) + } +} + +fun NavController.navigateToPasscodeScreen(options: NavOptions? = null) { + navigate(PASSCODE_SCREEN, options) +} diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/MifosIcon.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/MifosIcon.kt new file mode 100644 index 0000000..986fa2e --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/MifosIcon.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import mobile_wallet.libs.mifos_passcode.generated.resources.Res +import mobile_wallet.libs.mifos_passcode.generated.resources.lib_mifos_passcode_mifos_logo +import org.jetbrains.compose.resources.painterResource + +@Composable +internal fun MifosIcon(modifier: Modifier = Modifier) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.Center, + ) { + Image( + modifier = Modifier.size(180.dp), + painter = painterResource(resource = Res.drawable.lib_mifos_passcode_mifos_logo), + contentDescription = null, + ) + } +} diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeButton.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeButton.kt new file mode 100644 index 0000000..bb7ecfd --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeButton.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import mobile_wallet.libs.mifos_passcode.generated.resources.Res +import mobile_wallet.libs.mifos_passcode.generated.resources.library_mifos_passcode_login_manually +import mobile_wallet.libs.mifos_passcode.generated.resources.library_mifos_passcode_skip +import org.jetbrains.compose.resources.stringResource +import org.mifos.library.passcode.theme.forgotButtonStyle +import org.mifos.library.passcode.theme.skipButtonStyle + +@Composable +internal fun PasscodeSkipButton( + hasPassCode: Boolean, + onSkipButton: () -> Unit, + modifier: Modifier = Modifier, +) { + if (!hasPassCode) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(end = 16.dp), + horizontalArrangement = Arrangement.End, + ) { + TextButton( + onClick = { onSkipButton.invoke() }, + ) { + Text( + text = stringResource(Res.string.library_mifos_passcode_skip), + style = skipButtonStyle, + ) + } + } + } +} + +@Composable +internal fun PasscodeForgotButton( + hasPassCode: Boolean, + onForgotButton: () -> Unit, + modifier: Modifier = Modifier, +) { + if (hasPassCode) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(end = 16.dp), + horizontalArrangement = Arrangement.Center, + ) { + TextButton( + onClick = { onForgotButton.invoke() }, + ) { + Text( + text = stringResource(Res.string.library_mifos_passcode_login_manually), + style = forgotButtonStyle, + ) + } + } + } +} diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeHeader.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeHeader.kt new file mode 100644 index 0000000..81eaa19 --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeHeader.kt @@ -0,0 +1,124 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.component + +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.core.Transition +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.animateOffset +import androidx.compose.animation.core.rememberTransition +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.scale +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import mobile_wallet.libs.mifos_passcode.generated.resources.Res +import mobile_wallet.libs.mifos_passcode.generated.resources.library_mifos_passcode_confirm_passcode +import mobile_wallet.libs.mifos_passcode.generated.resources.library_mifos_passcode_create_passcode +import mobile_wallet.libs.mifos_passcode.generated.resources.library_mifos_passcode_enter_your_passcode +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.mifos.library.passcode.utility.Step + +@Composable +internal fun PasscodeHeader( + activeStep: Step, + isPasscodeAlreadySet: Boolean, + modifier: Modifier = Modifier, +) { + val transitionState = remember { MutableTransitionState(activeStep) } + transitionState.targetState = activeStep + + val transition: Transition = rememberTransition( + transitionState = transitionState, + label = "Headers Transition", + ) + + val offset = 200.0F + val zeroOffset = Offset(x = 0.0F, y = 0.0F) + val negativeOffset = Offset(x = -offset, y = 0.0F) + val positiveOffset = Offset(x = offset, y = 0.0F) + + val xTransitionHeader1 by transition.animateOffset(label = "Transition Offset Header 1") { + if (it == Step.Create) zeroOffset else negativeOffset + } + val xTransitionHeader2 by transition.animateOffset(label = "Transition Offset Header 2") { + if (it == Step.Confirm) zeroOffset else positiveOffset + } + val alphaHeader1 by transition.animateFloat(label = "Transition Alpha Header 1") { + if (it == Step.Create) 1.0F else 0.0F + } + val alphaHeader2 by transition.animateFloat(label = "Transition Alpha Header 2") { + if (it == Step.Confirm) 1.0F else 0.0F + } + val scaleHeader1 by transition.animateFloat(label = "Transition Alpha Header 1") { + if (it == Step.Create) 1.0F else 0.5F + } + val scaleHeader2 by transition.animateFloat(label = "Transition Alpha Header 2") { + if (it == Step.Confirm) 1.0F else 0.5F + } + + Box( + modifier = modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + if (isPasscodeAlreadySet) { + Text( + modifier = Modifier + .offset(x = xTransitionHeader1.x.dp) + .alpha(alpha = alphaHeader1) + .scale(scale = scaleHeader1), + text = stringResource(Res.string.library_mifos_passcode_enter_your_passcode), + style = TextStyle(fontSize = 20.sp), + ) + } else { + if (activeStep == Step.Create) { + Text( + modifier = Modifier + .offset(x = xTransitionHeader1.x.dp) + .alpha(alpha = alphaHeader1) + .scale(scale = scaleHeader1), + text = stringResource(Res.string.library_mifos_passcode_create_passcode), + style = TextStyle(fontSize = 20.sp), + ) + } else if (activeStep == Step.Confirm) { + Text( + modifier = Modifier + .offset(x = xTransitionHeader2.x.dp) + .alpha(alpha = alphaHeader2) + .scale(scale = scaleHeader2), + text = stringResource(Res.string.library_mifos_passcode_confirm_passcode), + style = TextStyle(fontSize = 20.sp), + ) + } + } + } + } +} + +@Preview +@Composable +private fun PasscodeHeaderPreview() { + PasscodeHeader(activeStep = Step.Create, isPasscodeAlreadySet = true) +} diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeKeys.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeKeys.kt new file mode 100644 index 0000000..521b958 --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeKeys.kt @@ -0,0 +1,197 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.component + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Backspace +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.mifos.library.passcode.theme.PasscodeKeyButtonStyle +import org.mifos.library.passcode.theme.blueTint + +@Composable +internal fun PasscodeKeys( + enterKey: (String) -> Unit, + deleteKey: () -> Unit, + deleteAllKeys: () -> Unit, + modifier: Modifier = Modifier, +) { + val onEnterKeyClick = { keyTitle: String -> + enterKey(keyTitle) + } + Column( + modifier = modifier + .fillMaxWidth() + .padding(16.dp), + ) { + Row(modifier = Modifier.fillMaxWidth()) { + PasscodeKey( + modifier = Modifier.weight(weight = 1.0F), + keyTitle = "1", + onClick = onEnterKeyClick, + ) + PasscodeKey( + modifier = Modifier.weight(weight = 1.0F), + keyTitle = "2", + onClick = onEnterKeyClick, + ) + PasscodeKey( + modifier = Modifier.weight(weight = 1.0F), + keyTitle = "3", + onClick = onEnterKeyClick, + ) + } + Row(modifier = Modifier.fillMaxWidth()) { + PasscodeKey( + modifier = Modifier.weight(weight = 1.0F), + keyTitle = "4", + onClick = onEnterKeyClick, + ) + PasscodeKey( + modifier = Modifier.weight(weight = 1.0F), + keyTitle = "5", + onClick = onEnterKeyClick, + ) + PasscodeKey( + modifier = Modifier.weight(weight = 1.0F), + keyTitle = "6", + onClick = onEnterKeyClick, + ) + } + Row(modifier = Modifier.fillMaxWidth()) { + PasscodeKey( + modifier = Modifier.weight(weight = 1.0F), + keyTitle = "7", + onClick = onEnterKeyClick, + ) + PasscodeKey( + modifier = Modifier.weight(weight = 1.0F), + keyTitle = "8", + onClick = onEnterKeyClick, + ) + PasscodeKey( + modifier = Modifier.weight(weight = 1.0F), + keyTitle = "9", + onClick = onEnterKeyClick, + ) + } + Row(modifier = Modifier.fillMaxWidth()) { + PasscodeKey(modifier = Modifier.weight(weight = 1.0F)) + PasscodeKey( + modifier = Modifier.weight(weight = 1.0F), + keyTitle = "0", + onClick = onEnterKeyClick, + ) + PasscodeKey( + modifier = Modifier.weight(weight = 1.0F), + keyIcon = Icons.Default.Delete, + keyIconContentDescription = "Delete Passcode Key Button", + onClick = { + deleteKey() + }, + onLongClick = { + deleteAllKeys() + }, + ) + } + } +} + +@Composable +internal fun PasscodeKey( + modifier: Modifier = Modifier, + keyTitle: String = "", + keyIcon: ImageVector? = null, + keyIconContentDescription: String = "", + onClick: ((String) -> Unit)? = null, + onLongClick: (() -> Unit)? = null, +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.Center, + ) { + CombinedClickableIconButton( + onClick = { + onClick?.invoke(keyTitle) + }, + onLongClick = { + onLongClick?.invoke() + }, + modifier = Modifier + .padding(all = 4.dp), + ) { + if (keyIcon == null) { + Text( + text = keyTitle, + style = PasscodeKeyButtonStyle.copy(color = blueTint), + ) + } else { + Icon( + imageVector = Icons.AutoMirrored.Filled.Backspace, + contentDescription = keyIconContentDescription, + tint = blueTint, + ) + } + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +internal fun CombinedClickableIconButton( + onClick: () -> Unit, + onLongClick: () -> Unit, + modifier: Modifier = Modifier, + size: Dp = 48.dp, + enabled: Boolean = true, + content: @Composable () -> Unit, +) { + Column( + modifier = modifier + .size(size = size) + .combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + enabled = enabled, + role = Role.Button, + ), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + val contentAlpha = + if (enabled) LocalContentColor.current else LocalContentColor.current.copy(alpha = 0f) + CompositionLocalProvider(LocalContentColor provides contentAlpha, content = content) + } +} + +@Preview +@Composable +private fun PasscodeKeysPreview() { + PasscodeKeys({}, {}, {}) +} diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeMismatchedDialog.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeMismatchedDialog.kt new file mode 100644 index 0000000..6cfe56b --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeMismatchedDialog.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.component + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import mobile_wallet.libs.mifos_passcode.generated.resources.Res +import mobile_wallet.libs.mifos_passcode.generated.resources.library_mifos_passcode_do_not_match +import mobile_wallet.libs.mifos_passcode.generated.resources.library_mifos_passcode_try_again +import org.jetbrains.compose.resources.stringResource + +@Composable +internal fun PasscodeMismatchedDialog( + visible: Boolean, + onDismiss: () -> Unit, +) { + if (visible) { + AlertDialog( + shape = MaterialTheme.shapes.large, + containerColor = Color.White, + title = { + Text( + text = stringResource(Res.string.library_mifos_passcode_do_not_match), + color = Color.Black, + ) + }, + confirmButton = { + TextButton(onClick = onDismiss) { + Text( + text = stringResource(Res.string.library_mifos_passcode_try_again), + color = Color.Black, + ) + } + }, + onDismissRequest = onDismiss, + ) + } +} diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeStepIndicator.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeStepIndicator.kt new file mode 100644 index 0000000..43d582a --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeStepIndicator.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.component + +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import org.mifos.library.passcode.theme.blueTint +import org.mifos.library.passcode.utility.Constants.STEPS_COUNT +import org.mifos.library.passcode.utility.Step + +@Composable +internal fun PasscodeStepIndicator( + activeStep: Step, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy( + space = 6.dp, + alignment = Alignment.CenterHorizontally, + ), + ) { + repeat(STEPS_COUNT) { step -> + val isActiveStep = step <= activeStep.index + val stepColor = + animateColorAsState(if (isActiveStep) blueTint else Color.Gray, label = "") + + Box( + modifier = Modifier + .size( + width = 72.dp, + height = 4.dp, + ) + .background( + color = stepColor.value, + shape = MaterialTheme.shapes.medium, + ), + ) + } + } +} diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeToolbar.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeToolbar.kt new file mode 100644 index 0000000..eebeb1b --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeToolbar.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.component + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +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.graphics.Color +import androidx.compose.ui.unit.dp +import mobile_wallet.libs.mifos_passcode.generated.resources.Res +import mobile_wallet.libs.mifos_passcode.generated.resources.lib_mifos_passcode_cancel +import mobile_wallet.libs.mifos_passcode.generated.resources.library_mifos_passcode_exit +import mobile_wallet.libs.mifos_passcode.generated.resources.library_mifos_passcode_exit_message +import org.jetbrains.compose.resources.stringResource +import org.mifos.library.passcode.utility.Step + +@Composable +internal fun PasscodeToolbar( + activeStep: Step, + hasPasscode: Boolean, + modifier: Modifier = Modifier, +) { + var exitWarningDialogVisible by remember { mutableStateOf(false) } + + AnimatedVisibility(exitWarningDialogVisible) { + ExitWarningDialog( + visible = exitWarningDialogVisible, + onConfirm = {}, + onDismiss = { + exitWarningDialogVisible = false + }, + ) + } + + Row( + modifier = modifier + .fillMaxWidth() + .padding(top = 8.dp), + horizontalArrangement = Arrangement.Center, + ) { + if (!hasPasscode) { + PasscodeStepIndicator( + activeStep = activeStep, + ) + } + } +} + +@Composable +private fun ExitWarningDialog( + visible: Boolean, + onConfirm: () -> Unit, + onDismiss: () -> Unit, +) { + if (visible) { + AlertDialog( + shape = MaterialTheme.shapes.large, + containerColor = Color.White, + title = { + Text( + text = stringResource(Res.string.library_mifos_passcode_exit_message), + color = Color.Black, + ) + }, + confirmButton = { + TextButton(onClick = onConfirm) { + Text(text = stringResource(Res.string.library_mifos_passcode_exit)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(text = stringResource(Res.string.lib_mifos_passcode_cancel)) + } + }, + onDismissRequest = onDismiss, + ) + } +} diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/di/PasscodeModule.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/di/PasscodeModule.kt new file mode 100644 index 0000000..92ca27c --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/di/PasscodeModule.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.di + +import org.koin.core.module.dsl.viewModelOf +import org.koin.dsl.module +import org.mifos.library.passcode.viewmodels.PasscodeViewModel +import proto.org.mifos.library.passcode.di.PasscodePreferenceModule + +val PasscodeModule = module { + includes(PasscodePreferenceModule) + + viewModelOf(::PasscodeViewModel) +} diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/theme/Color.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/theme/Color.kt new file mode 100644 index 0000000..87e3798 --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/theme/Color.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.theme + +import androidx.compose.ui.graphics.Color + +internal val blueTint = Color(0xFF03A9F4) diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/theme/Font.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/theme/Font.kt new file mode 100644 index 0000000..33528be --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/theme/Font.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.theme + +import androidx.compose.material3.Typography +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp +import mobile_wallet.libs.mifos_passcode.generated.resources.Res +import mobile_wallet.libs.mifos_passcode.generated.resources.lib_mifos_passcode_lato_black +import mobile_wallet.libs.mifos_passcode.generated.resources.lib_mifos_passcode_lato_bold +import mobile_wallet.libs.mifos_passcode.generated.resources.lib_mifos_passcode_lato_regular +import org.jetbrains.compose.resources.Font + +@Composable +fun getFontFamily() = FontFamily( + Font(Res.font.lib_mifos_passcode_lato_regular, FontWeight.Normal, FontStyle.Normal), + Font(Res.font.lib_mifos_passcode_lato_bold, FontWeight.Bold, FontStyle.Normal), + Font(Res.font.lib_mifos_passcode_lato_black, FontWeight.Black, FontStyle.Normal), +) + +@Composable +fun getTypography(): Typography { + return Typography( + displayLarge = TextStyle( + fontFamily = getFontFamily(), + fontWeight = FontWeight.Black, + fontSize = 34.sp, + ), + ) +} diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/theme/Theme.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/theme/Theme.kt new file mode 100644 index 0000000..39b181f --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/theme/Theme.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Color.Companion.Blue + +private val DarkColorPalette = darkColorScheme( + primary = Color.Cyan, + onPrimary = Color.Cyan, + secondary = Color.Black.copy(alpha = 0.2f), + background = Color.Black, +) +private val LightColorPalette = lightColorScheme( + primary = Blue, + onPrimary = Blue, + secondary = Color.Blue.copy(alpha = 0.4f), + background = Color.White, +) + +@Composable +internal fun MifosPasscodeTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit, +) { + val colors = if (darkTheme) { + DarkColorPalette + } else { + LightColorPalette + } + + MaterialTheme( + colorScheme = colors, + typography = getTypography(), + content = content, + ) +} diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/theme/Type.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/theme/Type.kt new file mode 100644 index 0000000..1325f60 --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/theme/Type.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.theme + +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +internal val PasscodeKeyButtonStyle = TextStyle( + fontWeight = FontWeight.Bold, + fontSize = 24.sp, +) + +internal val skipButtonStyle = TextStyle( + color = blueTint, + fontSize = 20.sp, +) + +internal val forgotButtonStyle = TextStyle( + color = blueTint, + fontSize = 14.sp, +) diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/Constants.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/Constants.kt new file mode 100644 index 0000000..4cb95a4 --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/Constants.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.utility + +internal object Constants { + const val STEPS_COUNT = 2 + const val PASSCODE_LENGTH = 4 + const val VIBRATE_FEEDBACK_DURATION = 300L +} diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/ShakeAnimation.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/ShakeAnimation.kt new file mode 100644 index 0000000..88e75cf --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/ShakeAnimation.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.utility + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.keyframes +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +internal object ShakeAnimation { + + fun CoroutineScope.performShakeAnimation(xShake: Animatable) { + launch { + xShake.animateTo( + targetValue = 0f, + animationSpec = keyframes { + durationMillis = 280 + 0f at 0 using LinearOutSlowInEasing + 20f at 80 using LinearOutSlowInEasing + 20f at 120 using LinearOutSlowInEasing + 10f at 160 using LinearOutSlowInEasing + 10f at 200 using LinearOutSlowInEasing + 5f at 240 using LinearOutSlowInEasing + 0f at 280 + }, + ) + } + } +} diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/Step.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/Step.kt new file mode 100644 index 0000000..6c66457 --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/Step.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.utility + +enum class Step(var index: Int) { + Create(0), + Confirm(1), +} diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt new file mode 100644 index 0000000..b6c5cdd --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt @@ -0,0 +1,199 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.viewmodels + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.mifos.library.passcode.utility.Step +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize +import org.mifospay.core.ui.utils.BaseViewModel +import proto.org.mifos.library.passcode.data.PasscodeManager + +private const val KEY_STATE = "passcode_state" +private const val PASSCODE_LENGTH = 4 + +class PasscodeViewModel( + private val passcodeRepository: PasscodeManager, + savedStateHandle: SavedStateHandle, +) : BaseViewModel( + initialState = savedStateHandle[KEY_STATE] ?: PasscodeState(), +) { + private var createPasscode: StringBuilder = StringBuilder() + private var confirmPasscode: StringBuilder = StringBuilder() + + init { + observePasscodeRepository() + } + + private fun observePasscodeRepository() { + viewModelScope.launch { + passcodeRepository.hasPasscode.collect { hasPasscode -> + mutableStateFlow.update { + it.copy( + hasPasscode = hasPasscode, + isPasscodeAlreadySet = hasPasscode, + ) + } + } + } + } + + override fun handleAction(action: PasscodeAction) { + when (action) { + is PasscodeAction.EnterKey -> enterKey(action.key) + is PasscodeAction.DeleteKey -> deleteKey() + is PasscodeAction.DeleteAllKeys -> deleteAllKeys() + is PasscodeAction.TogglePasscodeVisibility -> togglePasscodeVisibility() + is PasscodeAction.Restart -> restart() + is PasscodeAction.Internal.ProcessCompletedPasscode -> processCompletedPasscode() + } + } + + private fun enterKey(key: String) { + if (state.filledDots >= PASSCODE_LENGTH) return + + val currentPasscode = + if (state.activeStep == Step.Create) createPasscode else confirmPasscode + currentPasscode.append(key) + + mutableStateFlow.update { + it.copy( + currentPasscodeInput = currentPasscode.toString(), + filledDots = currentPasscode.length, + ) + } + + if (state.filledDots == PASSCODE_LENGTH) { + viewModelScope.launch { + sendAction(PasscodeAction.Internal.ProcessCompletedPasscode) + } + } + } + + private fun deleteKey() { + val currentPasscode = + if (state.activeStep == Step.Create) createPasscode else confirmPasscode + if (currentPasscode.isNotEmpty()) { + currentPasscode.deleteAt(currentPasscode.length - 1) + mutableStateFlow.update { + it.copy( + currentPasscodeInput = currentPasscode.toString(), + filledDots = currentPasscode.length, + ) + } + } + } + + private fun deleteAllKeys() { + if (state.activeStep == Step.Create) { + createPasscode.clear() + } else { + confirmPasscode.clear() + } + mutableStateFlow.update { + it.copy( + currentPasscodeInput = "", + filledDots = 0, + ) + } + } + + private fun togglePasscodeVisibility() { + mutableStateFlow.update { it.copy(passcodeVisible = !it.passcodeVisible) } + } + + private fun restart() { + resetState() + } + + private fun processCompletedPasscode() { + viewModelScope.launch { + when { + state.isPasscodeAlreadySet -> validateExistingPasscode() + state.activeStep == Step.Create -> moveToConfirmStep() + else -> validateNewPasscode() + } + } + } + + private suspend fun validateExistingPasscode() { + val savedPasscode = passcodeRepository.getPasscode.first() + if (savedPasscode == createPasscode.toString()) { + sendEvent(PasscodeEvent.PasscodeConfirmed(createPasscode.toString())) + createPasscode.clear() + } else { + sendEvent(PasscodeEvent.PasscodeRejected) + } + mutableStateFlow.update { it.copy(currentPasscodeInput = "") } + } + + private fun moveToConfirmStep() { + mutableStateFlow.update { + it.copy( + activeStep = Step.Confirm, + filledDots = 0, + currentPasscodeInput = "", + ) + } + } + + private suspend fun validateNewPasscode() { + if (createPasscode.toString() == confirmPasscode.toString()) { + passcodeRepository.savePasscode(confirmPasscode.toString()) + sendEvent(PasscodeEvent.PasscodeConfirmed(confirmPasscode.toString())) + resetState() + } else { + sendEvent(PasscodeEvent.PasscodeRejected) + resetState() + } + } + + private fun resetState() { + mutableStateFlow.update { + PasscodeState( + hasPasscode = it.hasPasscode, + isPasscodeAlreadySet = it.isPasscodeAlreadySet, + ) + } + createPasscode.clear() + confirmPasscode.clear() + } +} + +@Parcelize +data class PasscodeState( + val hasPasscode: Boolean = false, + val activeStep: Step = Step.Create, + val filledDots: Int = 0, + val passcodeVisible: Boolean = false, + val currentPasscodeInput: String = "", + val isPasscodeAlreadySet: Boolean = false, +) : Parcelable + +sealed class PasscodeEvent { + data class PasscodeConfirmed(val passcode: String) : PasscodeEvent() + data object PasscodeRejected : PasscodeEvent() +} + +sealed class PasscodeAction { + data class EnterKey(val key: String) : PasscodeAction() + data object DeleteKey : PasscodeAction() + data object DeleteAllKeys : PasscodeAction() + data object TogglePasscodeVisibility : PasscodeAction() + data object Restart : PasscodeAction() + + sealed class Internal : PasscodeAction() { + data object ProcessCompletedPasscode : Internal() + } +} diff --git a/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodeManager.kt b/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodeManager.kt new file mode 100644 index 0000000..2d6a3dc --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodeManager.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package proto.org.mifos.library.passcode.data + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn +import proto.org.mifos.library.passcode.model.PasscodePreferencesProto + +class PasscodeManager( + private val source: PasscodePreferencesDataSource, + dispatcher: CoroutineDispatcher, +) { + private val coroutineScope = CoroutineScope(dispatcher) + + val getPasscode = source.passcode.stateIn( + scope = coroutineScope, + started = SharingStarted.Eagerly, + initialValue = "", + ) + + val hasPasscode = source.hasPasscode.stateIn( + scope = coroutineScope, + started = SharingStarted.Eagerly, + initialValue = false, + ) + + suspend fun savePasscode(passcode: String) { + source.updatePasscodeSettings( + PasscodePreferencesProto( + passcode = passcode, + hasPasscode = passcode.isNotEmpty(), + ), + ) + } + + suspend fun clearPasscode() { + source.clearInfo() + } +} diff --git a/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodePreferencesDataSource.kt b/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodePreferencesDataSource.kt new file mode 100644 index 0000000..9bf273f --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodePreferencesDataSource.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +@file:OptIn(ExperimentalSerializationApi::class, ExperimentalSettingsApi::class) + +package proto.org.mifos.library.passcode.data + +import com.russhwolf.settings.ExperimentalSettingsApi +import com.russhwolf.settings.Settings +import com.russhwolf.settings.serialization.decodeValue +import com.russhwolf.settings.serialization.decodeValueOrNull +import com.russhwolf.settings.serialization.encodeValue +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import kotlinx.serialization.ExperimentalSerializationApi +import proto.org.mifos.library.passcode.model.PasscodePreferencesProto + +private const val PASSCODE_INFO_KEY = "passcodeInfo" + +class PasscodePreferencesDataSource( + private val settings: Settings, + private val dispatcher: CoroutineDispatcher, +) { + private val passcodeSettings = MutableStateFlow( + settings.decodeValue( + key = PASSCODE_INFO_KEY, + serializer = PasscodePreferencesProto.serializer(), + defaultValue = settings.decodeValueOrNull( + key = PASSCODE_INFO_KEY, + serializer = PasscodePreferencesProto.serializer(), + ) ?: PasscodePreferencesProto.DEFAULT, + ), + ) + + val passcode = passcodeSettings.map { it.passcode } + val hasPasscode = passcodeSettings.map { it.hasPasscode } + + suspend fun updatePasscodeSettings(passcodePreferences: PasscodePreferencesProto) { + withContext(dispatcher) { + settings.putPasscodePreference(passcodePreferences) + passcodeSettings.value = passcodePreferences + } + } + + suspend fun clearInfo() { + withContext(dispatcher) { + settings.remove(PASSCODE_INFO_KEY) + } + } +} + +private fun Settings.putPasscodePreference(preference: PasscodePreferencesProto) { + encodeValue( + key = PASSCODE_INFO_KEY, + serializer = PasscodePreferencesProto.serializer(), + value = preference, + ) +} diff --git a/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/di/PreferenceModule.kt b/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/di/PreferenceModule.kt new file mode 100644 index 0000000..575121f --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/di/PreferenceModule.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package proto.org.mifos.library.passcode.di + +import com.russhwolf.settings.Settings +import org.koin.core.qualifier.named +import org.koin.dsl.module +import org.mifospay.core.common.MifosDispatchers +import proto.org.mifos.library.passcode.data.PasscodeManager +import proto.org.mifos.library.passcode.data.PasscodePreferencesDataSource + +val PasscodePreferenceModule = module { + factory { Settings() } + factory { PasscodePreferencesDataSource(get(), get(named(MifosDispatchers.IO.name))) } + factory { PasscodeManager(get(), get(named(MifosDispatchers.Unconfined.name))) } +} diff --git a/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/model/PasscodePreferencesProto.kt b/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/model/PasscodePreferencesProto.kt new file mode 100644 index 0000000..0f7b38b --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/model/PasscodePreferencesProto.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package proto.org.mifos.library.passcode.model + +import kotlinx.serialization.Serializable + +@Serializable +data class PasscodePreferencesProto( + val passcode: String, + val hasPasscode: Boolean, +) { + companion object { + val DEFAULT = PasscodePreferencesProto(passcode = "", hasPasscode = false) + } +} diff --git a/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/passcode_preferences.proto b/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/passcode_preferences.proto new file mode 100644 index 0000000..7ff555c --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/passcode_preferences.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; + +option java_package = "org.mifos.library.passcode.proto"; +option java_multiple_files = true; + +message PasscodePreferences { + string passcode = 1; +} \ No newline at end of file diff --git a/ter b/ter new file mode 100644 index 0000000..1250959 --- /dev/null +++ b/ter @@ -0,0 +1,314 @@ + + SSUUMMMMAARRYY OOFF LLEESSSS CCOOMMMMAANNDDSS + + Commands marked with * may be preceded by a number, _N. + Notes in parentheses indicate the behavior if _N is given. + A key preceded by a caret indicates the Ctrl key; thus ^K is ctrl-K. + + h H Display this help. + q :q Q :Q ZZ Exit. + --------------------------------------------------------------------------- + + MMOOVVIINNGG + + e ^E j ^N CR * Forward one line (or _N lines). + y ^Y k ^K ^P * Backward one line (or _N lines). + f ^F ^V SPACE * Forward one window (or _N lines). + b ^B ESC-v * Backward one window (or _N lines). + z * Forward one window (and set window to _N). + w * Backward one window (and set window to _N). + ESC-SPACE * Forward one window, but don't stop at end-of-file. + d ^D * Forward one half-window (and set half-window to _N). + u ^U * Backward one half-window (and set half-window to _N). + ESC-) RightArrow * Right one half screen width (or _N positions). + ESC-( LeftArrow * Left one half screen width (or _N positions). + ESC-} ^RightArrow Right to last column displayed. + ESC-{ ^LeftArrow Left to first column. + F Forward forever; like "tail -f". + ESC-F Like F but stop when search pattern is found. + r ^R ^L Repaint screen. + R Repaint screen, discarding buffered input. + --------------------------------------------------- + Default "window" is the screen height. + Default "half-window" is half of the screen height. + --------------------------------------------------------------------------- + + SSEEAARRCCHHIINNGG + + /_p_a_t_t_e_r_n * Search forward for (_N-th) matching line. + ?_p_a_t_t_e_r_n * Search backward for (_N-th) matching line. + n * Repeat previous search (for _N-th occurrence). + N * Repeat previous search in reverse direction. + ESC-n * Repeat previous search, spanning files. + ESC-N * Repeat previous search, reverse dir. & spanning files. + ^O^N ^On * Search forward for (_N-th) OSC8 hyperlink. + ^O^P ^Op * Search backward for (_N-th) OSC8 hyperlink. + ^O^L ^Ol Jump to the currently selected OSC8 hyperlink. + ESC-u Undo (toggle) search highlighting. + ESC-U Clear search highlighting. + &_p_a_t_t_e_r_n * Display only matching lines. + --------------------------------------------------- + A search pattern may begin with one or more of: + ^N or ! Search for NON-matching lines. + ^E or * Search multiple files (pass thru END OF FILE). + ^F or @ Start search at FIRST file (for /) or last file (for ?). + ^K Highlight matches, but don't move (KEEP position). + ^R Don't use REGULAR EXPRESSIONS. + ^S _n Search for match in _n-th parenthesized subpattern. + ^W WRAP search if no match found. + ^L Enter next character literally into pattern. + --------------------------------------------------------------------------- + + JJUUMMPPIINNGG + + g < ESC-< * Go to first line in file (or line _N). + G > ESC-> * Go to last line in file (or line _N). + p % * Go to beginning of file (or _N percent into file). + t * Go to the (_N-th) next tag. + T * Go to the (_N-th) previous tag. + { ( [ * Find close bracket } ) ]. + } ) ] * Find open bracket { ( [. + ESC-^F _<_c_1_> _<_c_2_> * Find close bracket _<_c_2_>. + ESC-^B _<_c_1_> _<_c_2_> * Find open bracket _<_c_1_>. + --------------------------------------------------- + Each "find close bracket" command goes forward to the close bracket + matching the (_N-th) open bracket in the top line. + Each "find open bracket" command goes backward to the open bracket + matching the (_N-th) close bracket in the bottom line. + + m_<_l_e_t_t_e_r_> Mark the current top line with . + M_<_l_e_t_t_e_r_> Mark the current bottom line with . + '_<_l_e_t_t_e_r_> Go to a previously marked position. + '' Go to the previous position. + ^X^X Same as '. + ESC-m_<_l_e_t_t_e_r_> Clear a mark. + --------------------------------------------------- + A mark is any upper-case or lower-case letter. + Certain marks are predefined: + ^ means beginning of the file + $ means end of the file + --------------------------------------------------------------------------- + + CCHHAANNGGIINNGG FFIILLEESS + + :e [_f_i_l_e] Examine a new file. + ^X^V Same as :e. + :n * Examine the (_N-th) next file from the command line. + :p * Examine the (_N-th) previous file from the command line. + :x * Examine the first (or _N-th) file from the command line. + ^O^O Open the currently selected OSC8 hyperlink. + :d Delete the current file from the command line list. + = ^G :f Print current file name. + --------------------------------------------------------------------------- + + MMIISSCCEELLLLAANNEEOOUUSS CCOOMMMMAANNDDSS + + -_<_f_l_a_g_> Toggle a command line option [see OPTIONS below]. + --_<_n_a_m_e_> Toggle a command line option, by name. + __<_f_l_a_g_> Display the setting of a command line option. + ___<_n_a_m_e_> Display the setting of an option, by name. + +_c_m_d Execute the less cmd each time a new file is examined. + + !_c_o_m_m_a_n_d Execute the shell command with $SHELL. + #_c_o_m_m_a_n_d Execute the shell command, expanded like a prompt. + |XX_c_o_m_m_a_n_d Pipe file between current pos & mark XX to shell command. + s _f_i_l_e Save input to a file. + v Edit the current file with $VISUAL or $EDITOR. + V Print version number of "less". + --------------------------------------------------------------------------- + + OOPPTTIIOONNSS + + Most options may be changed either on the command line, + or from within less by using the - or -- command. + Options may be given in one of two forms: either a single + character preceded by a -, or a name preceded by --. + + -? ........ --help + Display help (from command line). + -a ........ --search-skip-screen + Search skips current screen. + -A ........ --SEARCH-SKIP-SCREEN + Search starts just after target line. + -b [_N] .... --buffers=[_N] + Number of buffers. + -B ........ --auto-buffers + Don't automatically allocate buffers for pipes. + -c ........ --clear-screen + Repaint by clearing rather than scrolling. + -d ........ --dumb + Dumb terminal. + -D xx_c_o_l_o_r . --color=xx_c_o_l_o_r + Set screen colors. + -e -E .... --quit-at-eof --QUIT-AT-EOF + Quit at end of file. + -f ........ --force + Force open non-regular files. + -F ........ --quit-if-one-screen + Quit if entire file fits on first screen. + -g ........ --hilite-search + Highlight only last match for searches. + -G ........ --HILITE-SEARCH + Don't highlight any matches for searches. + -h [_N] .... --max-back-scroll=[_N] + Backward scroll limit. + -i ........ --ignore-case + Ignore case in searches that do not contain uppercase. + -I ........ --IGNORE-CASE + Ignore case in all searches. + -j [_N] .... --jump-target=[_N] + Screen position of target lines. + -J ........ --status-column + Display a status column at left edge of screen. + -k _f_i_l_e ... --lesskey-file=_f_i_l_e + Use a compiled lesskey file. + -K ........ --quit-on-intr + Exit less in response to ctrl-C. + -L ........ --no-lessopen + Ignore the LESSOPEN environment variable. + -m -M .... --long-prompt --LONG-PROMPT + Set prompt style. + -n ......... --line-numbers + Suppress line numbers in prompts and messages. + -N ......... --LINE-NUMBERS + Display line number at start of each line. + -o [_f_i_l_e] .. --log-file=[_f_i_l_e] + Copy to log file (standard input only). + -O [_f_i_l_e] .. --LOG-FILE=[_f_i_l_e] + Copy to log file (unconditionally overwrite). + -p _p_a_t_t_e_r_n . --pattern=[_p_a_t_t_e_r_n] + Start at pattern (from command line). + -P [_p_r_o_m_p_t] --prompt=[_p_r_o_m_p_t] + Define new prompt. + -q -Q .... --quiet --QUIET --silent --SILENT + Quiet the terminal bell. + -r -R .... --raw-control-chars --RAW-CONTROL-CHARS + Output "raw" control characters. + -s ........ --squeeze-blank-lines + Squeeze multiple blank lines. + -S ........ --chop-long-lines + Chop (truncate) long lines rather than wrapping. + -t _t_a_g .... --tag=[_t_a_g] + Find a tag. + -T [_t_a_g_s_f_i_l_e] --tag-file=[_t_a_g_s_f_i_l_e] + Use an alternate tags file. + -u -U .... --underline-special --UNDERLINE-SPECIAL + Change handling of backspaces, tabs and carriage returns. + -V ........ --version + Display the version number of "less". + -w ........ --hilite-unread + Highlight first new line after forward-screen. + -W ........ --HILITE-UNREAD + Highlight first new line after any forward movement. + -x [_N[,...]] --tabs=[_N[,...]] + Set tab stops. + -X ........ --no-init + Don't use termcap init/deinit strings. + -y [_N] .... --max-forw-scroll=[_N] + Forward scroll limit. + -z [_N] .... --window=[_N] + Set size of window. + -" [_c[_c]] . --quotes=[_c[_c]] + Set shell quote characters. + -~ ........ --tilde + Don't display tildes after end of file. + -# [_N] .... --shift=[_N] + Set horizontal scroll amount (0 = one half screen width). + + --exit-follow-on-close + Exit F command on a pipe when writer closes pipe. + --file-size + Automatically determine the size of the input file. + --follow-name + The F command changes files if the input file is renamed. + --header=[_L[,_C[,_N]]] + Use _L lines (starting at line _N) and _C columns as headers. + --incsearch + Search file as each pattern character is typed in. + --intr=[_C] + Use _C instead of ^X to interrupt a read. + --lesskey-context=_t_e_x_t + Use lesskey source file contents. + --lesskey-src=_f_i_l_e + Use a lesskey source file. + --line-num-width=[_N] + Set the width of the -N line number field to _N characters. + --match-shift=[_N] + Show at least _N characters to the left of a search match. + --modelines=[_N] + Read _N lines from the input file and look for vim modelines. + --mouse + Enable mouse input. + --no-keypad + Don't send termcap keypad init/deinit strings. + --no-histdups + Remove duplicates from command history. + --no-number-headers + Don't give line numbers to header lines. + --no-search-header-lines + Searches do not include header lines. + --no-search-header-columns + Searches do not include header columns. + --no-search-headers + Searches do not include header lines or columns. + --no-vbell + Disable the terminal's visual bell. + --redraw-on-quit + Redraw final screen when quitting. + --rscroll=[_C] + Set the character used to mark truncated lines. + --save-marks + Retain marks across invocations of less. + --search-options=[EFKNRW-] + Set default options for every search. + --show-preproc-errors + Display a message if preprocessor exits with an error status. + --proc-backspace + Process backspaces for bold/underline. + --PROC-BACKSPACE + Treat backspaces as control characters. + --proc-return + Delete carriage returns before newline. + --PROC-RETURN + Treat carriage returns as control characters. + --proc-tab + Expand tabs to spaces. + --PROC-TAB + Treat tabs as control characters. + --status-col-width=[_N] + Set the width of the -J status column to _N characters. + --status-line + Highlight or color the entire line containing a mark. + --use-backslash + Subsequent options use backslash as escape char. + --use-color + Enables colored text. + --wheel-lines=[_N] + Each click of the mouse wheel moves _N lines. + --wordwrap + Wrap lines at spaces. + + + --------------------------------------------------------------------------- + + LLIINNEE EEDDIITTIINNGG + + These keys can be used to edit text being entered + on the "command line" at the bottom of the screen. + + RightArrow ..................... ESC-l ... Move cursor right one character. + LeftArrow ...................... ESC-h ... Move cursor left one character. + ctrl-RightArrow ESC-RightArrow ESC-w ... Move cursor right one word. + ctrl-LeftArrow ESC-LeftArrow ESC-b ... Move cursor left one word. + HOME ........................... ESC-0 ... Move cursor to start of line. + END ............................ ESC-$ ... Move cursor to end of line. + BACKSPACE ................................ Delete char to left of cursor. + DELETE ......................... ESC-x ... Delete char under cursor. + ctrl-BACKSPACE ESC-BACKSPACE ........... Delete word to left of cursor. + ctrl-DELETE .... ESC-DELETE .... ESC-X ... Delete word under cursor. + ctrl-U ......... ESC (MS-DOS only) ....... Delete entire line. + UpArrow ........................ ESC-k ... Retrieve previous command line. + DownArrow ...................... ESC-j ... Retrieve next command line. + TAB ...................................... Complete filename & cycle. + SHIFT-TAB ...................... ESC-TAB Complete filename & reverse cycle. + ctrl-L ................................... Complete filename, list all.