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.