diff --git a/libs/cmp-mifos-passcode/build.gradle.kts b/libs/cmp-mifos-passcode/build.gradle.kts
new file mode 100644
index 000000000..f90e37229
--- /dev/null
+++ b/libs/cmp-mifos-passcode/build.gradle.kts
@@ -0,0 +1,86 @@
+plugins {
+ alias(libs.plugins.kotlinMultiplatform)
+// alias(libs.plugins.kotlinCocoapods)
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.jetbrainsCompose)
+ alias(libs.plugins.compose.compiler)
+}
+
+kotlin {
+ androidTarget {
+ compilations.all {
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+ }
+ }
+
+ jvm("desktop")
+
+ listOf(
+ iosX64(),
+ iosArm64(),
+ iosSimulatorArm64()
+ ).forEach {
+ it.binaries.framework {
+ baseName = "cmp-mifos-passcode"
+ isStatic = true
+ }
+ }
+
+// cocoapods {
+// summary = "Some description for the Shared Module"
+// homepage = "Link to the Shared Module homepage"
+// version = "1.0"
+// ios.deploymentTarget = "16.0"
+// framework {
+// baseName = "shared"
+// isStatic = true
+// }
+// }
+
+ sourceSets {
+ commonMain.dependencies {
+ implementation(libs.androidx.lifecycle.viewmodel.ktx)
+ implementation(compose.ui)
+ implementation(compose.runtime)
+ implementation(compose.foundation)
+ implementation(compose.material3)
+ implementation(compose.components.resources)
+ implementation(libs.navigation.compose)
+ implementation(libs.multiplatform.settings.no.arg)
+ }
+ commonTest.dependencies {
+ implementation(libs.kotlin.test)
+ }
+ androidMain.dependencies {
+ implementation (libs.androidx.biometric)
+ }
+ }
+ tasks.register("testClasses")
+}
+
+android {
+ namespace = "com.mifos.passcode"
+ compileSdk = 35
+ sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
+ sourceSets["main"].res.srcDirs("src/androidMain/res")
+ sourceSets["main"].resources.srcDirs("src/commonMain/resources")
+ defaultConfig {
+ minSdk = 24
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+}
+dependencies {
+ implementation(libs.androidx.compose.ui.tooling.preview)
+ implementation(libs.androidx.compose.ui)
+}
+
+compose.resources {
+ publicResClass = true
+ packageOfResClass = "com.mifos.passcode.resources"
+ generateResClass = always
+}
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/shared.podspec b/libs/cmp-mifos-passcode/shared.podspec
new file mode 100644
index 000000000..35c25eed8
--- /dev/null
+++ b/libs/cmp-mifos-passcode/shared.podspec
@@ -0,0 +1,54 @@
+Pod::Spec.new do |spec|
+ spec.name = 'shared'
+ spec.version = '1.0'
+ spec.homepage = 'Link to the Shared Module homepage'
+ spec.source = { :http=> ''}
+ spec.authors = ''
+ spec.license = ''
+ spec.summary = 'Some description for the Shared Module'
+ spec.vendored_frameworks = 'build/cocoapods/framework/shared.framework'
+ spec.libraries = 'c++'
+ spec.ios.deployment_target = '16.0'
+
+
+ if !Dir.exist?('build/cocoapods/framework/shared.framework') || Dir.empty?('build/cocoapods/framework/shared.framework')
+ raise "
+
+ Kotlin framework 'shared' doesn't exist yet, so a proper Xcode project can't be generated.
+ 'pod install' should be executed after running ':generateDummyFramework' Gradle task:
+
+ ./gradlew :shared:generateDummyFramework
+
+ Alternatively, proper pod installation is performed during Gradle sync in the IDE (if Podfile location is set)"
+ end
+
+ spec.xcconfig = {
+ 'ENABLE_USER_SCRIPT_SANDBOXING' => 'NO',
+ }
+
+ spec.pod_target_xcconfig = {
+ 'KOTLIN_PROJECT_PATH' => ':shared',
+ 'PRODUCT_MODULE_NAME' => 'shared',
+ }
+
+ spec.script_phases = [
+ {
+ :name => 'Build shared',
+ :execution_position => :before_compile,
+ :shell_path => '/bin/sh',
+ :script => <<-SCRIPT
+ if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then
+ echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\""
+ exit 0
+ fi
+ set -ev
+ REPO_ROOT="$PODS_TARGET_SRCROOT"
+ "$REPO_ROOT/../gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \
+ -Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \
+ -Pkotlin.native.cocoapods.archs="$ARCHS" \
+ -Pkotlin.native.cocoapods.configuration="$CONFIGURATION"
+ SCRIPT
+ }
+ ]
+ spec.resources = ['build/compose/cocoapods/compose-resources']
+end
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/androidMain/kotlin/com/mifos/passcode/BiometricUtilAndroidImpl.kt b/libs/cmp-mifos-passcode/src/androidMain/kotlin/com/mifos/passcode/BiometricUtilAndroidImpl.kt
new file mode 100644
index 000000000..1cadb17e0
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/androidMain/kotlin/com/mifos/passcode/BiometricUtilAndroidImpl.kt
@@ -0,0 +1,132 @@
+package com.mifos.passcode
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
+import androidx.biometric.BiometricPrompt
+import androidx.core.content.ContextCompat
+import androidx.fragment.app.FragmentActivity
+import com.mifos.passcode.utility.AuthenticationResult
+import com.mifos.passcode.utility.BioMetricUtil
+import java.util.Base64
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+
+class BiometricUtilAndroidImpl(
+ private val activity: FragmentActivity,
+ private val cipherUtil: com.mifos.passcode.ICipherUtil
+) : BioMetricUtil {
+
+ private val executor = ContextCompat.getMainExecutor(activity)
+ private var promptInfo: BiometricPrompt.PromptInfo? = null
+ private var biometricPrompt: BiometricPrompt? = null
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ override suspend fun setAndReturnPublicKey(): String? {
+ val authenticateResult = authenticate()
+ return when (authenticateResult) {
+ is AuthenticationResult.Success -> generatePublicKey()
+ else -> null
+ }
+ }
+
+ override fun canAuthenticate(): Boolean {
+ return BiometricManager.from(activity).canAuthenticate(BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ override fun generatePublicKey(): String? {
+ return cipherUtil.generateKeyPair().public?.encoded?.toBase64Encoded()?.toPemFormat()?.toBase64Encoded()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ override fun getPublicKey(): String? {
+ return cipherUtil.getPublicKey()?.encoded?.toBase64Encoded()?.toPemFormat()?.toBase64Encoded()
+ }
+
+ override fun isValidCrypto(): Boolean {
+ return try {
+ cipherUtil.getCrypto()
+ true
+ } catch (e: Exception){
+ false
+ }
+ }
+
+ override suspend fun authenticate(): AuthenticationResult = suspendCoroutine { continuation ->
+
+ biometricPrompt = BiometricPrompt(activity, executor, object :
+ BiometricPrompt.AuthenticationCallback() {
+ override fun onAuthenticationFailed() {
+ super.onAuthenticationFailed()
+ }
+
+ override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
+ super.onAuthenticationError(errorCode, errString)
+ when (errorCode) {
+ BiometricPrompt.ERROR_LOCKOUT, BiometricPrompt.ERROR_LOCKOUT_PERMANENT -> continuation.resume(
+ AuthenticationResult.AttemptExhausted)
+ BiometricPrompt.ERROR_NEGATIVE_BUTTON -> continuation.resume(
+ AuthenticationResult.NegativeButtonClick)
+ else -> continuation.resume(AuthenticationResult.Error(errString.toString()))
+ }
+ }
+
+ override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
+ super.onAuthenticationSucceeded(result)
+ continuation.resume(AuthenticationResult.Success)
+ }
+ })
+
+ promptInfo?.let {
+ biometricPrompt?.authenticate(it, cipherUtil.getCrypto())
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ override fun signUserId(ucc: String): String {
+ cipherUtil.getCrypto().signature?.update(ucc.toByteArray())
+ return cipherUtil.getCrypto().signature?.sign()?.toBase64Encoded() ?: ""
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ override fun isBiometricSet(): Boolean {
+ return !getPublicKey().isNullOrEmpty() && isValidCrypto()
+ }
+
+ fun preparePrompt(
+ title: String,
+ subtitle: String,
+ description: String,
+ ): BioMetricUtil {
+ promptInfo = BiometricPrompt.PromptInfo.Builder()
+ .setTitle(title)
+ .setSubtitle(subtitle)
+ .setDescription(description)
+ .setNegativeButtonText("Cancel")
+ .setAllowedAuthenticators(BIOMETRIC_STRONG)
+ .build()
+ return this
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.O)
+fun ByteArray.toBase64Encoded(): String? {
+ return Base64.getEncoder().encodeToString(this)
+}
+
+@RequiresApi(Build.VERSION_CODES.O)
+fun String.toBase64Encoded(): String? {
+ return Base64.getEncoder().encodeToString(this.toByteArray())
+}
+
+private fun String.toPemFormat(): String {
+ val stringBuilder = StringBuilder()
+ stringBuilder.append("-----BEGIN RSA PUBLIC KEY-----").append("\n")
+ chunked(64).forEach {
+ stringBuilder.append(it).append("\n")
+ }
+ stringBuilder.append("-----END RSA PUBLIC KEY-----")
+ return stringBuilder.toString()
+}
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/androidMain/kotlin/com/mifos/passcode/CipherUtilImpl.android.kt b/libs/cmp-mifos-passcode/src/androidMain/kotlin/com/mifos/passcode/CipherUtilImpl.android.kt
new file mode 100644
index 000000000..d0b9a382d
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/androidMain/kotlin/com/mifos/passcode/CipherUtilImpl.android.kt
@@ -0,0 +1,61 @@
+package com.mifos.passcode
+
+import android.security.keystore.KeyGenParameterSpec
+import android.security.keystore.KeyProperties
+import androidx.biometric.BiometricPrompt
+import com.mifos.passcode.ICipherUtil
+import java.security.KeyPair
+import java.security.KeyPairGenerator
+import java.security.KeyStore
+import java.security.PrivateKey
+import java.security.PublicKey
+import java.security.Signature
+
+class CipherUtilAndroidImpl: com.mifos.passcode.ICipherUtil {
+ private val KEY_NAME = "biometric_key"
+
+ override fun generateKeyPair(): KeyPair {
+ val keyPairGenerator: KeyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore")
+ val parameterSpec: KeyGenParameterSpec = KeyGenParameterSpec.Builder(KEY_NAME,
+ KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY).run {
+ setDigests(KeyProperties.DIGEST_SHA256)
+ setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1)
+ build()
+ }
+ keyPairGenerator.initialize(parameterSpec)
+ return keyPairGenerator.genKeyPair()
+ }
+
+ override fun getPublicKey(): PublicKey? = getKeyPair()?.public
+
+ private fun getKeyPair(): KeyPair? {
+ val keyStore = KeyStore.getInstance("AndroidKeyStore")
+ keyStore.load(null)
+ keyStore?.getCertificate(KEY_NAME).let { return KeyPair(it?.publicKey, null) }
+ }
+
+ override fun getCrypto(): Crypto {
+ val signature = Signature.getInstance("SHA256withRSA")
+ val keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore")
+ keyStore.load(null)
+ val key: PrivateKey = if(keyStore.containsAlias(KEY_NAME))
+ keyStore.getKey(KEY_NAME, null) as PrivateKey
+ else
+ generateKeyPair().private
+ signature.initSign(key)
+ return BiometricPrompt.CryptoObject(signature)
+ }
+
+ override suspend fun removePublicKey() {
+ val keyStore = KeyStore.getInstance("AndroidKeyStore")
+ keyStore.load(null)
+ keyStore?.deleteEntry(KEY_NAME)
+ }
+
+}
+
+actual typealias CommonKeyPair = KeyPair
+
+actual typealias CommonPublicKey = PublicKey
+
+actual typealias Crypto = BiometricPrompt.CryptoObject
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/androidMain/kotlin/com/mifos/passcode/Platform.android.kt b/libs/cmp-mifos-passcode/src/androidMain/kotlin/com/mifos/passcode/Platform.android.kt
new file mode 100644
index 000000000..c91875625
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/androidMain/kotlin/com/mifos/passcode/Platform.android.kt
@@ -0,0 +1,10 @@
+package com.mifos.passcode
+
+import com.mifos.passcode.Platform
+
+class AndroidPlatform : com.mifos.passcode.Platform {
+// override val name: String = "Android ${android.os.Build.VERSION.SDK_INT}"
+ override val name: String = "Android"
+}
+
+actual fun getPlatform(): com.mifos.passcode.Platform = AndroidPlatform()
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/appleMain/kotlin/com/mifos/passcode/getPlatform.kt b/libs/cmp-mifos-passcode/src/appleMain/kotlin/com/mifos/passcode/getPlatform.kt
new file mode 100644
index 000000000..650e482c6
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/appleMain/kotlin/com/mifos/passcode/getPlatform.kt
@@ -0,0 +1,2 @@
+package com.mifos.passcode
+
diff --git a/libs/cmp-mifos-passcode/src/commonMain/composeResources/drawable/ic_delete.xml b/libs/cmp-mifos-passcode/src/commonMain/composeResources/drawable/ic_delete.xml
new file mode 100644
index 000000000..4069f8377
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/composeResources/drawable/ic_delete.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/libs/cmp-mifos-passcode/src/commonMain/composeResources/drawable/mifos_logo.jpg b/libs/cmp-mifos-passcode/src/commonMain/composeResources/drawable/mifos_logo.jpg
new file mode 100644
index 000000000..a067a3c8e
Binary files /dev/null and b/libs/cmp-mifos-passcode/src/commonMain/composeResources/drawable/mifos_logo.jpg differ
diff --git a/libs/cmp-mifos-passcode/src/commonMain/composeResources/font/lato_black.ttf b/libs/cmp-mifos-passcode/src/commonMain/composeResources/font/lato_black.ttf
new file mode 100644
index 000000000..4340502d9
Binary files /dev/null and b/libs/cmp-mifos-passcode/src/commonMain/composeResources/font/lato_black.ttf differ
diff --git a/libs/cmp-mifos-passcode/src/commonMain/composeResources/font/lato_bold.ttf b/libs/cmp-mifos-passcode/src/commonMain/composeResources/font/lato_bold.ttf
new file mode 100644
index 000000000..016068b48
Binary files /dev/null and b/libs/cmp-mifos-passcode/src/commonMain/composeResources/font/lato_bold.ttf differ
diff --git a/libs/cmp-mifos-passcode/src/commonMain/composeResources/font/lato_regular.ttf b/libs/cmp-mifos-passcode/src/commonMain/composeResources/font/lato_regular.ttf
new file mode 100644
index 000000000..bb2e8875a
Binary files /dev/null and b/libs/cmp-mifos-passcode/src/commonMain/composeResources/font/lato_regular.ttf differ
diff --git a/libs/cmp-mifos-passcode/src/commonMain/composeResources/values/strings.xml b/libs/cmp-mifos-passcode/src/commonMain/composeResources/values/strings.xml
new file mode 100644
index 000000000..a4685cb21
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/composeResources/values/strings.xml
@@ -0,0 +1,34 @@
+
+
+ Biometric
+ 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
+ Cancel
+ Skip
+ Forgot Passcode, Login Manually
+ Try again
+ Passcode do not match!
+ Are you sure you want to exit?
+ Use TouchId
+ Use FaceIdh
+ Authentication failed
+ Authentication not set
+ Feature unavailable
+ Biometric Registration Successful !
+ Do you want to enable app lock ?
+ Use your existing PIN, pattern, face ID, or fingerprint to unlock this app.
+ Yes
+ No
+ Ok
+
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/CipherUtilImpl.kt b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/CipherUtilImpl.kt
new file mode 100644
index 000000000..c5bdaec31
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/CipherUtilImpl.kt
@@ -0,0 +1,18 @@
+package com.mifos.passcode
+
+interface ICipherUtil {
+ @Throws(Exception::class)
+ fun generateKeyPair(): com.mifos.passcode.CommonKeyPair
+
+ fun getPublicKey(): com.mifos.passcode.CommonPublicKey?
+
+ @Throws(Exception::class)
+ fun getCrypto(): com.mifos.passcode.Crypto
+
+ @Throws(Exception::class)
+ suspend fun removePublicKey()
+}
+
+expect class CommonKeyPair
+expect interface CommonPublicKey
+expect class Crypto
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/Greeting.kt b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/Greeting.kt
new file mode 100644
index 000000000..c8ce7120e
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/Greeting.kt
@@ -0,0 +1,11 @@
+package com.mifos.passcode
+
+import com.mifos.passcode.Platform
+
+class Greeting {
+ private val platform: com.mifos.passcode.Platform = com.mifos.passcode.getPlatform()
+
+ fun greet(): String {
+ return "Hello, ${platform.name}!"
+ }
+}
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/PasscodeNavigation.kt b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/PasscodeNavigation.kt
new file mode 100644
index 000000000..4be56d5b9
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/PasscodeNavigation.kt
@@ -0,0 +1,47 @@
+/*
+ * 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 com.mifos.passcode
+
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavOptions
+import androidx.navigation.compose.composable
+import com.mifos.passcode.component.PasscodeScreen
+import com.mifos.passcode.utility.BioMetricUtil
+
+const val PASSCODE_SCREEN = "passcode_screen"
+
+fun NavGraphBuilder.passcodeRoute(
+ onForgotButton: () -> Unit,
+ onSkipButton: () -> Unit,
+ onPasscodeConfirm: (String) -> Unit,
+ onPasscodeRejected: () -> Unit,
+ enableBiometric: Boolean,
+ bioMetricUtil: BioMetricUtil,
+ onBiometricAuthSucess: () -> Unit,
+) {
+ composable(
+ route = PASSCODE_SCREEN,
+ ) {
+ PasscodeScreen(
+ onForgotButton = onForgotButton,
+ onSkipButton = onSkipButton,
+ onPasscodeConfirm = onPasscodeConfirm,
+ onPasscodeRejected = onPasscodeRejected,
+ enableBiometric = enableBiometric,
+ bioMetricUtil = bioMetricUtil,
+ onBiometricAuthSuccess = onBiometricAuthSucess,
+ )
+ }
+}
+
+fun NavController.navigateToPasscodeScreen(options: NavOptions? = null) {
+ navigate(PASSCODE_SCREEN, options)
+}
diff --git a/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/Platform.kt b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/Platform.kt
new file mode 100644
index 000000000..c6af4a74a
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/Platform.kt
@@ -0,0 +1,7 @@
+package com.mifos.passcode
+
+interface Platform {
+ val name: String
+}
+
+expect fun getPlatform(): Platform
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/BackSpace.kt b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/BackSpace.kt
new file mode 100644
index 000000000..5cf67a44f
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/BackSpace.kt
@@ -0,0 +1,45 @@
+package com.mifos.passcode.component
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.materialIcon
+import androidx.compose.material.icons.materialPath
+import androidx.compose.ui.graphics.vector.ImageVector
+
+public val Icons.Filled.Backspace: ImageVector
+ get() {
+ if (com.mifos.passcode.component._backspace != null) {
+ return com.mifos.passcode.component._backspace!!
+ }
+ com.mifos.passcode.component._backspace = materialIcon(name = "Filled.Backspace") {
+ materialPath {
+ moveTo(22.0f, 3.0f)
+ lineTo(7.0f, 3.0f)
+ curveToRelative(-0.69f, 0.0f, -1.23f, 0.35f, -1.59f, 0.88f)
+ lineTo(0.0f, 12.0f)
+ lineToRelative(5.41f, 8.11f)
+ curveToRelative(0.36f, 0.53f, 0.9f, 0.89f, 1.59f, 0.89f)
+ horizontalLineToRelative(15.0f)
+ curveToRelative(1.1f, 0.0f, 2.0f, -0.9f, 2.0f, -2.0f)
+ lineTo(24.0f, 5.0f)
+ curveToRelative(0.0f, -1.1f, -0.9f, -2.0f, -2.0f, -2.0f)
+ close()
+ moveTo(19.0f, 15.59f)
+ lineTo(17.59f, 17.0f)
+ lineTo(14.0f, 13.41f)
+ lineTo(10.41f, 17.0f)
+ lineTo(9.0f, 15.59f)
+ lineTo(12.59f, 12.0f)
+ lineTo(9.0f, 8.41f)
+ lineTo(10.41f, 7.0f)
+ lineTo(14.0f, 10.59f)
+ lineTo(17.59f, 7.0f)
+ lineTo(19.0f, 8.41f)
+ lineTo(15.41f, 12.0f)
+ lineTo(19.0f, 15.59f)
+ close()
+ }
+ }
+ return com.mifos.passcode.component._backspace!!
+ }
+
+private var _backspace: ImageVector? = null
diff --git a/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/MifosIcon.kt b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/MifosIcon.kt
new file mode 100644
index 000000000..8926e9da9
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/MifosIcon.kt
@@ -0,0 +1,26 @@
+package com.mifos.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 com.mifos.passcode.resources.Res
+import com.mifos.passcode.resources.mifos_logo
+import org.jetbrains.compose.resources.painterResource
+
+@Composable
+fun MifosIcon(modifier: Modifier) {
+ Row(
+ modifier = modifier,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Image(
+ modifier = Modifier.size(180.dp),
+ painter = painterResource(resource= Res.drawable.mifos_logo),
+ contentDescription = null
+ )
+ }
+}
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/PassCodeScreen.kt b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/PassCodeScreen.kt
new file mode 100644
index 000000000..ff579754d
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/PassCodeScreen.kt
@@ -0,0 +1,309 @@
+package com.mifos.passcode.component
+
+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.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
+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.dp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.mifos.passcode.resources.Res
+import com.mifos.passcode.resources.biometric_registration_success
+import com.mifos.passcode.resources.ok
+import com.mifos.passcode.theme.blueTint
+import com.mifos.passcode.utility.BioMetricUtil
+import com.mifos.passcode.utility.Constants.PASSCODE_LENGTH
+import com.mifos.passcode.utility.PreferenceManager
+import com.mifos.passcode.utility.ShakeAnimation.performShakeAnimation
+import com.mifos.passcode.viewmodels.BiometricAuthorizationViewModel
+import com.mifos.passcode.viewmodels.BiometricEffect
+import com.mifos.passcode.viewmodels.PasscodeViewModel
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import org.jetbrains.compose.resources.getString
+
+/**
+ * @author pratyush
+ * @since 15/3/24
+ */
+
+@Composable
+fun PasscodeScreen(
+ viewModel: PasscodeViewModel = viewModel { PasscodeViewModel() },
+ onForgotButton: () -> Unit,
+ onSkipButton: () -> Unit,
+ onPasscodeConfirm: (String) -> Unit,
+ onPasscodeRejected: () -> Unit,
+ enableBiometric: Boolean = false,
+ onBiometricAuthSuccess: () -> Unit = {},
+ biometricAuthorizationViewModel: BiometricAuthorizationViewModel = viewModel(),
+ bioMetricUtil: BioMetricUtil? = null,
+) {
+ val preferenceManager = remember { PreferenceManager() }
+ val activeStep by viewModel.activeStep.collectAsState()
+ val filledDots by viewModel.filledDots.collectAsState()
+ val passcodeVisible by viewModel.passcodeVisible.collectAsState()
+ val currentPasscode by viewModel.currentPasscodeInput.collectAsState()
+ val xShake = remember { Animatable(initialValue = 0.0F) }
+ var passcodeRejectedDialogVisible by remember { mutableStateOf(false) }
+ val biometricState by biometricAuthorizationViewModel.state.collectAsState()
+ var biometricMessage by rememberSaveable { mutableStateOf("") }
+ val coroutineScope = rememberCoroutineScope()
+ var showBiometricDialog by rememberSaveable{ mutableStateOf(false) }
+
+
+ if(showBiometricDialog)
+ {
+ PasscodeBiometricConfirmDialog(
+ setBiometric = {
+ biometricAuthorizationViewModel.setBiometricAuthorization(bioMetricUtil!!)
+ },
+ cancelBiometric = {
+ showBiometricDialog = false
+ }
+ )
+ }
+
+ if(enableBiometric) {
+ biometricState.error?.let {
+ biometricMessage = it
+ }
+ }
+
+ LaunchedEffect(key1 = Unit) {
+ if(enableBiometric) {
+ biometricAuthorizationViewModel.effect.collectLatest {
+ when (it) {
+ BiometricEffect.BiometricAuthSuccess -> {
+ onBiometricAuthSuccess.invoke()
+ showBiometricDialog = false
+ }
+
+ BiometricEffect.BiometricSetSuccess -> {
+ biometricMessage = getString(Res.string.biometric_registration_success)
+ showBiometricDialog = false
+ }
+ }
+ }
+ }
+ }
+
+ LaunchedEffect(key1 = viewModel.onPasscodeConfirmed) {
+ viewModel.onPasscodeConfirmed.collect {
+ onPasscodeConfirm(it)
+ }
+ }
+ LaunchedEffect(key1 = viewModel.onPasscodeRejected) {
+ viewModel.onPasscodeRejected.collect {
+ passcodeRejectedDialogVisible = true
+// vibrateFeedback(context)
+ performShakeAnimation(xShake)
+ onPasscodeRejected()
+ }
+ }
+
+ LaunchedEffect(true) {
+ if(preferenceManager.hasPasscode && enableBiometric) {
+ if(bioMetricUtil!!.isBiometricSet())
+ biometricAuthorizationViewModel.authorizeBiometric(bioMetricUtil)
+ else
+ showBiometricDialog = true
+ }
+ }
+
+ val snackBarHostState = remember {
+ SnackbarHostState()
+ }
+
+ Scaffold(
+ snackbarHost = { SnackbarHost(snackBarHostState) }
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.White),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ PasscodeToolbar(activeStep = activeStep, preferenceManager.hasPasscode)
+ PasscodeSkipButton(
+ onSkipButton = { onSkipButton.invoke() },
+ hasPassCode = preferenceManager.hasPasscode
+ )
+ MifosIcon(modifier = Modifier.fillMaxWidth())
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 16.dp, bottom = 24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ PasscodeHeader(
+ activeStep = activeStep,
+ isPasscodeAlreadySet = preferenceManager.hasPasscode
+ )
+ PasscodeView(
+ filledDots = filledDots,
+ currentPasscode = currentPasscode,
+ passcodeVisible = passcodeVisible,
+ togglePasscodeVisibility = { viewModel.togglePasscodeVisibility() },
+ restart = { viewModel.restart() },
+ passcodeRejectedDialogVisible = passcodeRejectedDialogVisible,
+ onDismissDialog = { passcodeRejectedDialogVisible = false },
+ xShake = xShake
+ )
+ }
+ Spacer(modifier = Modifier.height(6.dp))
+ PasscodeKeys(
+ enterKey = { viewModel.enterKey(it) },
+ deleteKey = { viewModel.deleteKey() },
+ deleteAllKeys = { viewModel.deleteAllKeys() },
+ modifier = Modifier.padding(horizontal = 12.dp)
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ PasscodeForgotButton(
+ onForgotButton = { onForgotButton.invoke() },
+ hasPassCode = preferenceManager.hasPasscode
+ )
+
+ UseTouchIdButton(
+ onClick = {
+ if ( bioMetricUtil!!.isBiometricSet() )
+ biometricAuthorizationViewModel.authorizeBiometric(bioMetricUtil)
+ else
+ showBiometricDialog = true
+ },
+ hasPassCode = preferenceManager.hasPasscode,
+ enableBiometric = enableBiometric
+ )
+
+ LaunchedEffect ( biometricMessage ) {
+ if(biometricMessage.isNotEmpty()) {
+ coroutineScope.launch {
+ snackBarHostState.showSnackbar(
+ message = biometricMessage,
+ duration = SnackbarDuration.Short,
+ withDismissAction = false,
+ actionLabel = getString(Res.string.ok)
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun PasscodeView(
+ modifier: Modifier = Modifier,
+ restart: () -> Unit,
+ togglePasscodeVisibility: () -> Unit,
+ filledDots: Int,
+ passcodeVisible: Boolean,
+ currentPasscode: String,
+ passcodeRejectedDialogVisible: Boolean,
+ onDismissDialog: () -> Unit,
+ xShake: Animatable
+) {
+ PasscodeMismatchedDialog(
+ visible = passcodeRejectedDialogVisible,
+ onDismiss = {
+ onDismissDialog.invoke()
+ restart()
+ }
+ )
+
+ Row(
+ modifier = modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Row(
+ modifier = modifier.offset(x = xShake.value.dp),
+ 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.invoke() },
+ modifier = Modifier.padding(start = 10.dp)
+ ) {
+ Icon(
+ imageVector = if (passcodeVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff,
+ contentDescription = null
+ )
+ }
+ }
+}
+
+//@Preview(showBackground = true)
+//@Composable
+//fun PasscodeScreenPreview() {
+// PasscodeScreen(
+// viewModel = PasscodeViewModel(object : PasscodeRepository {
+// override fun getSavedPasscode(): String {
+// return ""
+// }
+//
+// override val hasPasscode: Boolean
+// get() = false
+//
+// override fun savePasscode(passcode: String) {}
+//
+// }),
+// {}, {}, {}, {}
+// )
+//}
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/PasscodeBiometricConfirmDialog.kt b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/PasscodeBiometricConfirmDialog.kt
new file mode 100644
index 000000000..4875a8ec1
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/PasscodeBiometricConfirmDialog.kt
@@ -0,0 +1,116 @@
+package com.mifos.passcode.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Color.Companion.White
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.window.Dialog
+import com.mifos.passcode.resources.Res
+import com.mifos.passcode.resources.enable_biometric_dialog_description
+import com.mifos.passcode.resources.enable_biometric_dialog_title
+import com.mifos.passcode.resources.no
+import com.mifos.passcode.resources.yes
+import com.mifos.passcode.theme.blueTint
+import org.jetbrains.compose.resources.stringResource
+
+@Composable
+fun PasscodeBiometricConfirmDialog(
+ cancelBiometric: () -> Unit,
+ setBiometric: () -> Unit
+) {
+
+ Dialog(onDismissRequest = { cancelBiometric.invoke() }) {
+
+ Box(
+ modifier = Modifier
+ .clip(RoundedCornerShape(16.dp))
+ .background(Color.White)
+ .padding(16.dp)
+ ) {
+
+ Column {
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ Text(
+ text = stringResource(resource = Res.string.enable_biometric_dialog_title),
+ modifier = Modifier
+ .padding(8.dp),
+ fontSize = 20.sp
+ )
+
+ Spacer(modifier = Modifier.height(4.dp))
+
+ Text(
+ text = stringResource(resource = Res.string.enable_biometric_dialog_description),
+ modifier = Modifier
+ .padding(8.dp),
+ fontSize = 12.sp
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 16.dp)
+ ) {
+ DialogButton(
+ onClick = { cancelBiometric.invoke() },
+ modifier = Modifier
+ .padding(end = 8.dp)
+ .weight(1f),
+ text = stringResource(resource = Res.string.no)
+ )
+
+ DialogButton(
+ onClick = { setBiometric.invoke() },
+ modifier = Modifier
+ .padding(start = 8.dp)
+ .weight(1f),
+ text = stringResource(resource = Res.string.yes)
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun DialogButton(
+ text: String,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Button(
+ onClick = onClick,
+ modifier = modifier
+ .fillMaxWidth()
+ .height(36.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = blueTint,
+ contentColor = White,
+ disabledContainerColor = Color.DarkGray,
+ disabledContentColor = White
+ )
+ ) {
+ Text(text = text)
+ }
+}
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/PasscodeButton.kt b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/PasscodeButton.kt
new file mode 100644
index 000000000..c799d72f4
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/PasscodeButton.kt
@@ -0,0 +1,91 @@
+package com.mifos.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 com.mifos.passcode.resources.Res
+import com.mifos.passcode.resources.forgot_passcode_login_manually
+import com.mifos.passcode.resources.skip
+import com.mifos.passcode.resources.use_faceId
+import com.mifos.passcode.resources.use_touchId
+import com.mifos.passcode.theme.forgotButtonStyle
+import com.mifos.passcode.theme.skipButtonStyle
+import com.mifos.passcode.theme.useTouchIdButtonStyle
+import org.jetbrains.compose.resources.stringResource
+
+@Composable
+fun PasscodeSkipButton(
+ onSkipButton: () -> Unit,
+ hasPassCode: Boolean
+) {
+ if (!hasPassCode) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(end = 16.dp),
+ horizontalArrangement = Arrangement.End
+ ) {
+ TextButton(
+ onClick = { onSkipButton.invoke() }
+ ) {
+ Text(text = stringResource(Res.string.skip), style = skipButtonStyle())
+ }
+ }
+ }
+
+}
+
+@Composable
+fun PasscodeForgotButton(
+ onForgotButton: () -> Unit,
+ hasPassCode: Boolean
+) {
+ if (hasPassCode) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(end = 16.dp),
+ horizontalArrangement = Arrangement.Center
+ ) {
+ TextButton(
+ onClick = { onForgotButton.invoke() }
+ ) {
+ Text(
+ text = stringResource(Res.string.forgot_passcode_login_manually),
+ style = forgotButtonStyle()
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun UseTouchIdButton(
+ onClick: () -> Unit,
+ hasPassCode: Boolean,
+ enableBiometric: Boolean
+) {
+ if (hasPassCode && enableBiometric) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(end = 16.dp),
+ horizontalArrangement = Arrangement.Center
+ ) {
+ TextButton(
+ onClick = onClick
+ ) {
+ if(com.mifos.passcode.getPlatform().name == "Android")
+ Text(text = stringResource(Res.string.use_touchId), style = useTouchIdButtonStyle())
+ else
+ Text(text = stringResource(Res.string.use_faceId), style = useTouchIdButtonStyle())
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/PasscodeHeader.kt b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/PasscodeHeader.kt
new file mode 100644
index 000000000..d676ca027
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/PasscodeHeader.kt
@@ -0,0 +1,114 @@
+package com.mifos.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.updateTransition
+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 com.mifos.passcode.resources.Res
+import com.mifos.passcode.resources.confirm_passcode
+import com.mifos.passcode.resources.create_passcode
+import com.mifos.passcode.resources.enter_your_passcode
+import com.mifos.passcode.utility.Step
+import org.jetbrains.compose.resources.stringResource
+
+@Composable
+fun PasscodeHeader(
+ modifier: Modifier = Modifier,
+ activeStep: Step,
+ isPasscodeAlreadySet: Boolean,
+) {
+ val transitionState = remember { MutableTransitionState(activeStep) }
+ transitionState.targetState = activeStep
+
+ val transition: Transition = updateTransition(
+ 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(resource = Res.string.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(resource = Res.string.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(resource = Res.string.confirm_passcode),
+ style = TextStyle(fontSize = 20.sp)
+ )
+ }
+ }
+ }
+ }
+}
+
+//@Preview
+//@Composable
+//fun PasscodeHeaderPreview() {
+// PasscodeHeader(activeStep = Step.Create, isPasscodeAlreadySet = true)
+//}
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/PasscodeKeys.kt b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/PasscodeKeys.kt
new file mode 100644
index 000000000..d2c9bb03a
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/PasscodeKeys.kt
@@ -0,0 +1,199 @@
+package com.mifos.passcode.component
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+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.ripple.rememberRipple
+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.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+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 com.mifos.passcode.theme.PasscodeKeyButtonStyle
+import com.mifos.passcode.theme.blueTint
+
+@Composable
+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.Filled.Backspace,
+ keyIconContentDescription = "Delete Passcode Key Button",
+ onClick = {
+ deleteKey()
+ },
+ onLongClick = {
+ deleteAllKeys()
+ }
+ )
+ }
+ }
+}
+
+@Composable
+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(
+ modifier = Modifier
+ .padding(all = 4.dp),
+ onClick = {
+ onClick?.invoke(keyTitle)
+ },
+ onLongClick = {
+ onLongClick?.invoke()
+ }
+ ) {
+ if (keyIcon == null) {
+ Text(
+ text = keyTitle,
+ style = PasscodeKeyButtonStyle().copy(color = blueTint)
+ )
+ } else {
+ Icon(
+ imageVector = Icons.Default.Backspace,
+ contentDescription = keyIconContentDescription,
+ tint = blueTint
+ )
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun CombinedClickableIconButton(
+ onClick: () -> Unit,
+ onLongClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ size: Dp = 48.dp,
+ rippleRadius: Dp = 36.dp,
+ enabled: Boolean = true,
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ content: @Composable () -> Unit
+) {
+ Column(
+ modifier = modifier
+ .size(size = size)
+ .combinedClickable(
+ onClick = onClick,
+ onLongClick = onLongClick,
+ enabled = enabled,
+ role = Role.Button,
+ interactionSource = interactionSource,
+ indication = rememberRipple(
+ bounded = false,
+ radius = rippleRadius,
+ color = Color.Cyan
+ )
+ ),
+ 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
+//fun PasscodeKeysPreview() {
+// PasscodeKeys({}, {}, {})
+//}
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/PasscodeMismatchedDialog.kt b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/PasscodeMismatchedDialog.kt
new file mode 100644
index 000000000..a116d7043
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/PasscodeMismatchedDialog.kt
@@ -0,0 +1,37 @@
+package com.mifos.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 com.mifos.passcode.resources.Res
+import com.mifos.passcode.resources.passcode_do_not_match
+import com.mifos.passcode.resources.try_again
+import org.jetbrains.compose.resources.stringResource
+
+@Composable
+fun PasscodeMismatchedDialog(
+ visible: Boolean,
+ onDismiss: () -> Unit
+) {
+ if (visible) {
+ AlertDialog(
+ shape = MaterialTheme.shapes.large,
+ containerColor = Color.White,
+ title = {
+ Text(
+ text = stringResource(Res.string.passcode_do_not_match),
+ color = Color.Black
+ )
+ },
+ confirmButton = {
+ TextButton(onClick = onDismiss) {
+ Text(text = stringResource(Res.string.try_again), color = Color.Black)
+ }
+ },
+ onDismissRequest = onDismiss
+ )
+ }
+}
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/PasscodeStepIndicator.kt b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/PasscodeStepIndicator.kt
new file mode 100644
index 000000000..9789c1466
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/PasscodeStepIndicator.kt
@@ -0,0 +1,50 @@
+package com.mifos.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 com.mifos.passcode.theme.blueTint
+import com.mifos.passcode.utility.Constants.STEPS_COUNT
+import com.mifos.passcode.utility.Step
+
+@Composable
+fun PasscodeStepIndicator(
+ modifier: Modifier = Modifier,
+ activeStep: Step
+) {
+ 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
+ )
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/PasscodeToolbar.kt b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/PasscodeToolbar.kt
new file mode 100644
index 000000000..a5c83860f
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/PasscodeToolbar.kt
@@ -0,0 +1,80 @@
+package com.mifos.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.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 com.mifos.passcode.resources.Res
+import com.mifos.passcode.resources.are_you_sure_you_want_to_exit
+import com.mifos.passcode.resources.cancel
+import com.mifos.passcode.resources.exit
+import com.mifos.passcode.utility.Step
+import org.jetbrains.compose.resources.stringResource
+
+@Composable
+fun PasscodeToolbar(activeStep: Step, hasPasscode: Boolean) {
+ var exitWarningDialogVisible by remember { mutableStateOf(false) }
+ ExitWarningDialog(
+ visible = exitWarningDialogVisible,
+ onConfirm = {},
+ onDismiss = {
+ exitWarningDialogVisible = false
+ }
+ )
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 40.dp),
+ horizontalArrangement = Arrangement.Center
+ ) {
+ if (!hasPasscode) {
+ PasscodeStepIndicator(
+ activeStep = activeStep
+ )
+ }
+ }
+}
+
+@Composable
+fun ExitWarningDialog(
+ visible: Boolean,
+ onConfirm: () -> Unit,
+ onDismiss: () -> Unit
+) {
+ if (visible) {
+ AlertDialog(
+ shape = MaterialTheme.shapes.large,
+ containerColor = Color.White,
+ title = {
+ Text(
+ text = stringResource(Res.string.are_you_sure_you_want_to_exit),
+ color = Color.Black
+ )
+ },
+ confirmButton = {
+ TextButton(onClick = onConfirm) {
+ Text(text = stringResource(Res.string.exit))
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = onDismiss) {
+ Text(text = stringResource(Res.string.cancel))
+ }
+ },
+ onDismissRequest = onDismiss
+ )
+ }
+}
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/Visibility.kt b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/Visibility.kt
new file mode 100644
index 000000000..272999b3d
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/Visibility.kt
@@ -0,0 +1,58 @@
+package com.mifos.passcode.component
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.materialIcon
+import androidx.compose.material.icons.materialPath
+import androidx.compose.ui.graphics.vector.ImageVector
+
+public val Icons.Filled.VisibilityOff: ImageVector
+ get() {
+ if (_visibilityOff != null) {
+ return _visibilityOff!!
+ }
+ _visibilityOff = materialIcon(name = "Filled.VisibilityOff") {
+ materialPath {
+ moveTo(12.0f, 7.0f)
+ curveToRelative(2.76f, 0.0f, 5.0f, 2.24f, 5.0f, 5.0f)
+ curveToRelative(0.0f, 0.65f, -0.13f, 1.26f, -0.36f, 1.83f)
+ lineToRelative(2.92f, 2.92f)
+ curveToRelative(1.51f, -1.26f, 2.7f, -2.89f, 3.43f, -4.75f)
+ curveToRelative(-1.73f, -4.39f, -6.0f, -7.5f, -11.0f, -7.5f)
+ curveToRelative(-1.4f, 0.0f, -2.74f, 0.25f, -3.98f, 0.7f)
+ lineToRelative(2.16f, 2.16f)
+ curveTo(10.74f, 7.13f, 11.35f, 7.0f, 12.0f, 7.0f)
+ close()
+ moveTo(2.0f, 4.27f)
+ lineToRelative(2.28f, 2.28f)
+ lineToRelative(0.46f, 0.46f)
+ curveTo(3.08f, 8.3f, 1.78f, 10.02f, 1.0f, 12.0f)
+ curveToRelative(1.73f, 4.39f, 6.0f, 7.5f, 11.0f, 7.5f)
+ curveToRelative(1.55f, 0.0f, 3.03f, -0.3f, 4.38f, -0.84f)
+ lineToRelative(0.42f, 0.42f)
+ lineTo(19.73f, 22.0f)
+ lineTo(21.0f, 20.73f)
+ lineTo(3.27f, 3.0f)
+ lineTo(2.0f, 4.27f)
+ close()
+ moveTo(7.53f, 9.8f)
+ lineToRelative(1.55f, 1.55f)
+ curveToRelative(-0.05f, 0.21f, -0.08f, 0.43f, -0.08f, 0.65f)
+ curveToRelative(0.0f, 1.66f, 1.34f, 3.0f, 3.0f, 3.0f)
+ curveToRelative(0.22f, 0.0f, 0.44f, -0.03f, 0.65f, -0.08f)
+ lineToRelative(1.55f, 1.55f)
+ curveToRelative(-0.67f, 0.33f, -1.41f, 0.53f, -2.2f, 0.53f)
+ curveToRelative(-2.76f, 0.0f, -5.0f, -2.24f, -5.0f, -5.0f)
+ curveToRelative(0.0f, -0.79f, 0.2f, -1.53f, 0.53f, -2.2f)
+ close()
+ moveTo(11.84f, 9.02f)
+ lineToRelative(3.15f, 3.15f)
+ lineToRelative(0.02f, -0.16f)
+ curveToRelative(0.0f, -1.66f, -1.34f, -3.0f, -3.0f, -3.0f)
+ lineToRelative(-0.17f, 0.01f)
+ close()
+ }
+ }
+ return _visibilityOff!!
+ }
+
+private var _visibilityOff: ImageVector? = null
diff --git a/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/VisibilityOff.kt b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/VisibilityOff.kt
new file mode 100644
index 000000000..d6a7704ed
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/component/VisibilityOff.kt
@@ -0,0 +1,38 @@
+package com.mifos.passcode.component
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.materialIcon
+import androidx.compose.material.icons.materialPath
+import androidx.compose.ui.graphics.vector.ImageVector
+
+public val Icons.Filled.Visibility: ImageVector
+ get() {
+ if (_visibility != null) {
+ return _visibility!!
+ }
+ _visibility = materialIcon(name = "Filled.Visibility") {
+ materialPath {
+ moveTo(12.0f, 4.5f)
+ curveTo(7.0f, 4.5f, 2.73f, 7.61f, 1.0f, 12.0f)
+ curveToRelative(1.73f, 4.39f, 6.0f, 7.5f, 11.0f, 7.5f)
+ reflectiveCurveToRelative(9.27f, -3.11f, 11.0f, -7.5f)
+ curveToRelative(-1.73f, -4.39f, -6.0f, -7.5f, -11.0f, -7.5f)
+ close()
+ moveTo(12.0f, 17.0f)
+ curveToRelative(-2.76f, 0.0f, -5.0f, -2.24f, -5.0f, -5.0f)
+ reflectiveCurveToRelative(2.24f, -5.0f, 5.0f, -5.0f)
+ reflectiveCurveToRelative(5.0f, 2.24f, 5.0f, 5.0f)
+ reflectiveCurveToRelative(-2.24f, 5.0f, -5.0f, 5.0f)
+ close()
+ moveTo(12.0f, 9.0f)
+ curveToRelative(-1.66f, 0.0f, -3.0f, 1.34f, -3.0f, 3.0f)
+ reflectiveCurveToRelative(1.34f, 3.0f, 3.0f, 3.0f)
+ reflectiveCurveToRelative(3.0f, -1.34f, 3.0f, -3.0f)
+ reflectiveCurveToRelative(-1.34f, -3.0f, -3.0f, -3.0f)
+ close()
+ }
+ }
+ return _visibility!!
+ }
+
+private var _visibility: ImageVector? = null
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/data/PasscodeRepository.kt b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/data/PasscodeRepository.kt
new file mode 100644
index 000000000..ab7ebc5b7
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/data/PasscodeRepository.kt
@@ -0,0 +1,8 @@
+package com.mifos.passcode.data
+
+interface PasscodeRepository {
+ fun getSavedPasscode(): String
+ val hasPasscode: Boolean
+ fun savePasscode(passcode: String)
+ fun clearPasscode()
+}
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/data/PasscodeRepositoryImpl.kt b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/data/PasscodeRepositoryImpl.kt
new file mode 100644
index 000000000..d16945592
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/data/PasscodeRepositoryImpl.kt
@@ -0,0 +1,23 @@
+package com.mifos.passcode.data
+
+import com.mifos.passcode.utility.PreferenceManager
+
+
+class PasscodeRepositoryImpl constructor(private val preferenceManager: PreferenceManager) :
+ PasscodeRepository {
+
+ override fun getSavedPasscode(): String {
+ return preferenceManager.getSavedPasscode()
+ }
+
+ override val hasPasscode: Boolean
+ get() = preferenceManager.hasPasscode
+
+ override fun savePasscode(passcode: String) {
+ preferenceManager.savePasscode(passcode)
+ }
+
+ override fun clearPasscode() {
+ preferenceManager.clearPasscode()
+ }
+}
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/data/SetBiometricPublicKeyRepository.kt b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/data/SetBiometricPublicKeyRepository.kt
new file mode 100644
index 000000000..0c9c39690
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/data/SetBiometricPublicKeyRepository.kt
@@ -0,0 +1,17 @@
+package com.mifos.passcode.data
+
+import kotlinx.coroutines.delay
+
+private var publicKeyOnServer = ""
+class SetBiometricPublicKeyRepository {
+ suspend fun set(publicKey: String) {
+ delay(500)
+ publicKeyOnServer = publicKey
+ }
+}
+
+class VerifyBiometric {
+ suspend fun verify(signedUserId: String): Result {
+ return Result.success(Unit)
+ }
+}
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/theme/Color.kt b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/theme/Color.kt
new file mode 100644
index 000000000..0c4d15f8f
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/theme/Color.kt
@@ -0,0 +1,5 @@
+package com.mifos.passcode.theme
+
+import androidx.compose.ui.graphics.Color
+
+val blueTint = Color(0xFF03A9F4)
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/theme/Font.kt b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/theme/Font.kt
new file mode 100644
index 000000000..dae6c8fd7
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/theme/Font.kt
@@ -0,0 +1,30 @@
+package com.mifos.passcode.theme
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+import com.mifos.passcode.resources.Res
+import com.mifos.passcode.resources.lato_black
+import com.mifos.passcode.resources.lato_bold
+import com.mifos.passcode.resources.lato_regular
+import org.jetbrains.compose.resources.Font
+
+@Composable
+fun LatoFonts() = FontFamily(
+ Font(
+ resource = Res.font.lato_regular,
+ weight = FontWeight.Normal,
+ style = FontStyle.Normal
+ ),
+ Font(
+ resource = Res.font.lato_bold,
+ weight = FontWeight.Bold,
+ style = FontStyle.Normal
+ ),
+ Font(
+ resource = Res.font.lato_black,
+ weight = FontWeight.Black,
+ style = FontStyle.Normal
+ )
+)
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/theme/Theme.kt b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/theme/Theme.kt
new file mode 100644
index 000000000..92ce16c22
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/theme/Theme.kt
@@ -0,0 +1,40 @@
+package com.mifos.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
+fun MifosPasscodeTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ content: @Composable () -> Unit
+) {
+ val colors = if (darkTheme) {
+ DarkColorPalette
+ } else {
+ LightColorPalette
+ }
+
+ MaterialTheme(
+ colorScheme = colors,
+ typography = Typography(),
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/theme/Type.kt b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/theme/Type.kt
new file mode 100644
index 000000000..1fe3a2e11
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/theme/Type.kt
@@ -0,0 +1,57 @@
+package com.mifos.passcode.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+@Composable
+fun Typography() = Typography().run {
+ val fontFamily = LatoFonts()
+ copy(
+ displayLarge = displayLarge.copy(fontFamily = fontFamily),
+ displayMedium = displayMedium.copy(fontFamily = fontFamily),
+ displaySmall = displaySmall.copy(fontFamily = fontFamily),
+ headlineLarge = headlineLarge.copy(fontFamily = fontFamily),
+ headlineMedium = headlineMedium.copy(fontFamily = fontFamily),
+ headlineSmall = headlineSmall.copy(fontFamily = fontFamily),
+ titleLarge = titleLarge.copy(fontFamily = fontFamily),
+ titleMedium = titleMedium.copy(fontFamily = fontFamily),
+ titleSmall = titleSmall.copy(fontFamily = fontFamily),
+ bodyLarge = bodyLarge.copy(fontFamily = fontFamily),
+ bodyMedium = bodyMedium.copy(fontFamily = fontFamily),
+ bodySmall = bodySmall.copy(fontFamily = fontFamily),
+ labelLarge = labelLarge.copy(fontFamily = fontFamily),
+ labelMedium = labelMedium.copy(fontFamily = fontFamily),
+ labelSmall = labelSmall.copy(fontFamily = fontFamily)
+ )
+}
+
+@Composable
+fun PasscodeKeyButtonStyle() = TextStyle(
+ fontFamily = LatoFonts(),
+ fontWeight = FontWeight.Bold,
+ fontSize = 24.sp
+)
+
+@Composable
+fun skipButtonStyle() = TextStyle(
+ color = blueTint,
+ fontSize = 20.sp,
+ fontFamily = LatoFonts()
+)
+
+@Composable
+fun forgotButtonStyle() = TextStyle(
+ color = blueTint,
+ fontSize = 14.sp,
+ fontFamily = LatoFonts()
+)
+
+@Composable
+fun useTouchIdButtonStyle() = TextStyle(
+ color = blueTint,
+ fontSize = 14.sp,
+ fontFamily = LatoFonts()
+)
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/utility/BioMetricUtil.kt b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/utility/BioMetricUtil.kt
new file mode 100644
index 000000000..c1c4eb7bb
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/utility/BioMetricUtil.kt
@@ -0,0 +1,21 @@
+package com.mifos.passcode.utility
+
+interface BioMetricUtil {
+
+ suspend fun setAndReturnPublicKey(): String?
+ suspend fun authenticate(): AuthenticationResult
+ fun canAuthenticate(): Boolean
+ fun generatePublicKey(): String?
+ fun signUserId(ucc: String): String
+ fun isBiometricSet(): Boolean
+ fun getPublicKey(): String?
+ fun isValidCrypto(): Boolean
+}
+
+sealed class AuthenticationResult {
+ data object Success: AuthenticationResult()
+ data object Failed: AuthenticationResult()
+ data object AttemptExhausted: AuthenticationResult()
+ data object NegativeButtonClick: AuthenticationResult()
+ data class Error(val error: String): AuthenticationResult()
+}
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/utility/Constants.kt b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/utility/Constants.kt
new file mode 100644
index 000000000..42932b28f
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/utility/Constants.kt
@@ -0,0 +1,7 @@
+package com.mifos.passcode.utility
+
+object Constants {
+ const val STEPS_COUNT = 2
+ const val PASSCODE_LENGTH = 4
+ const val VIBRATE_FEEDBACK_DURATION = 300L
+}
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/utility/PreferenceManager.kt b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/utility/PreferenceManager.kt
new file mode 100644
index 000000000..a1a9ee4d4
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/utility/PreferenceManager.kt
@@ -0,0 +1,36 @@
+package com.mifos.passcode.utility
+
+import com.mifos.passcode.resources.Res
+import com.mifos.passcode.resources.has_passcode
+import com.mifos.passcode.resources.passcode
+import com.russhwolf.settings.Settings
+
+/**
+ * @author pratyush
+ * @since 15/3/24
+ */
+
+class PreferenceManager()
+{
+ private val settings : Settings by lazy {
+ Settings()
+ }
+
+ var hasPasscode: Boolean
+ get() = settings.getBoolean(Res.string.has_passcode.toString(), false)
+ set(value) = settings.putBoolean(Res.string.has_passcode.toString(), value)
+
+ fun savePasscode(passcode: String) {
+ settings.putString(Res.string.passcode.toString(), passcode)
+ hasPasscode = true
+ }
+
+ fun getSavedPasscode(): String {
+ return settings.getString(Res.string.passcode.toString(), "")
+ }
+
+ fun clearPasscode() {
+ settings.clear()
+ hasPasscode = false
+ }
+}
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/utility/ShakeAnimation.kt b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/utility/ShakeAnimation.kt
new file mode 100644
index 000000000..c9b483bed
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/utility/ShakeAnimation.kt
@@ -0,0 +1,28 @@
+package com.mifos.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
+
+object ShakeAnimation {
+
+ fun CoroutineScope.performShakeAnimation(xShake: Animatable) {
+ launch {
+ xShake.animateTo(
+ targetValue = 0f, // This resets the position after the shake
+ animationSpec = keyframes {
+ durationMillis = 280 // Total animation duration
+ 0f at 0 with LinearOutSlowInEasing // Start position
+ 20f at 80 with LinearOutSlowInEasing // Move right
+ -20f at 120 with LinearOutSlowInEasing // Move left
+ 10f at 160 with LinearOutSlowInEasing // Move right
+ -10f at 200 with LinearOutSlowInEasing // Move left
+ 5f at 240 with LinearOutSlowInEasing // Move right
+ 0f at 280 // End at the original position
+ }
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/utility/Step.kt b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/utility/Step.kt
new file mode 100644
index 000000000..a3de5330a
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/utility/Step.kt
@@ -0,0 +1,6 @@
+package com.mifos.passcode.utility
+
+enum class Step(var index: Int) {
+ Create(0),
+ Confirm(1)
+}
diff --git a/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/viewmodels/BiometricAuthorizationViewModel.kt b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/viewmodels/BiometricAuthorizationViewModel.kt
new file mode 100644
index 000000000..fae0e3db2
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/viewmodels/BiometricAuthorizationViewModel.kt
@@ -0,0 +1,85 @@
+package com.mifos.passcode.viewmodels
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.mifos.passcode.data.SetBiometricPublicKeyRepository
+import com.mifos.passcode.data.VerifyBiometric
+import com.mifos.passcode.utility.AuthenticationResult
+import com.mifos.passcode.utility.BioMetricUtil
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+
+class BiometricAuthorizationViewModel: ViewModel() {
+ private val setBiometricPublicKeyRepository =
+ SetBiometricPublicKeyRepository()
+ private val verifyBiometric = VerifyBiometric()
+
+ private val _state: MutableStateFlow =
+ MutableStateFlow(BiometricState(false, null))
+ private val _effect: MutableSharedFlow = MutableSharedFlow(replay = 0)
+
+ val state: StateFlow
+ get() = _state
+
+ val effect: SharedFlow
+ get() = _effect
+
+ fun setBiometricAuthorization(bioMetricUtil: BioMetricUtil) {
+ viewModelScope.launch {
+ _state.value = BiometricState(isLoading = true, error = null)
+ if (!bioMetricUtil.canAuthenticate()) {
+ _state.value = BiometricState(isLoading = true, error = "Biometric not available")
+ return@launch
+ }
+ val publicKey = bioMetricUtil.setAndReturnPublicKey() ?: ""
+ setBiometricPublicKeyRepository.set(publicKey)
+ _state.value = BiometricState(isLoading = false, error = null)
+ _effect.emit(BiometricEffect.BiometricSetSuccess)
+
+ }
+ }
+
+ fun authorizeBiometric(bioMetricUtil: BioMetricUtil) {
+ viewModelScope.launch {
+ when(val biometricResult = bioMetricUtil.authenticate()) {
+ AuthenticationResult.AttemptExhausted -> {
+ _state.value = BiometricState(isLoading = false, error = "Attempt Exhausted")
+ }
+ is AuthenticationResult.Error -> {
+ _state.value = BiometricState(isLoading = false, error = biometricResult.error)
+ }
+ AuthenticationResult.Failed -> {
+ _state.value = BiometricState(isLoading = false, error = "Biometric Failed")
+ }
+ AuthenticationResult.NegativeButtonClick -> {
+ _state.value = BiometricState(isLoading = false, error = "Biometric Canceled")
+ }
+ AuthenticationResult.Success -> {
+ _state.value = BiometricState(isLoading = true, error = null)
+ val signedUserId = bioMetricUtil.signUserId("userId")
+ val result = verifyBiometric.verify(signedUserId)
+ if (result.isSuccess) {
+ _state.value = BiometricState(isLoading = false, error = null)
+ _effect.emit(BiometricEffect.BiometricAuthSuccess)
+ } else {
+ _state.value = BiometricState(isLoading = false, error = result.exceptionOrNull()!!.message)
+ }
+ }
+ }
+
+ }
+ }
+}
+
+data class BiometricState(
+ val isLoading: Boolean,
+ val error: String?
+)
+
+sealed class BiometricEffect {
+ data object BiometricSetSuccess: BiometricEffect()
+ data object BiometricAuthSuccess: BiometricEffect()
+}
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/viewmodels/PasscodeViewModel.kt b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/viewmodels/PasscodeViewModel.kt
new file mode 100644
index 000000000..961fbc33d
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/commonMain/kotlin/com/mifos/passcode/viewmodels/PasscodeViewModel.kt
@@ -0,0 +1,146 @@
+package com.mifos.passcode.viewmodels
+
+import androidx.compose.runtime.mutableStateOf
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.mifos.passcode.data.PasscodeRepositoryImpl
+import com.mifos.passcode.utility.Constants.PASSCODE_LENGTH
+import com.mifos.passcode.utility.PreferenceManager
+import com.mifos.passcode.utility.Step
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+/**
+ * @author pratyush
+ * @since 15/3/24
+ */
+
+class PasscodeViewModel :
+ ViewModel() {
+
+ private val
+ passcodeRepository = PasscodeRepositoryImpl(PreferenceManager())
+ private val _onPasscodeConfirmed = MutableSharedFlow()
+ private val _onPasscodeRejected = MutableSharedFlow()
+
+ private val _activeStep = MutableStateFlow(Step.Create)
+ private val _filledDots = MutableStateFlow(0)
+
+ private var createPasscode: StringBuilder = StringBuilder()
+ private var confirmPasscode: StringBuilder = StringBuilder()
+
+ val onPasscodeConfirmed = _onPasscodeConfirmed.asSharedFlow()
+ val onPasscodeRejected = _onPasscodeRejected.asSharedFlow()
+
+ val activeStep = _activeStep.asStateFlow()
+ val filledDots = _filledDots.asStateFlow()
+
+ private val _passcodeVisible = MutableStateFlow(false)
+ val passcodeVisible = _passcodeVisible.asStateFlow()
+
+ private val _currentPasscodeInput = MutableStateFlow("")
+ val currentPasscodeInput = _currentPasscodeInput.asStateFlow()
+
+ private var _isPasscodeAlreadySet = mutableStateOf(passcodeRepository.hasPasscode)
+
+ init {
+ resetData()
+ }
+
+ private fun emitActiveStep(activeStep: Step) = viewModelScope.launch {
+ _activeStep.emit(activeStep)
+ }
+
+ private fun emitFilledDots(filledDots: Int) = viewModelScope.launch {
+ _filledDots.emit(filledDots)
+ }
+
+ private fun emitOnPasscodeConfirmed(confirmPassword: String) = viewModelScope.launch {
+ _onPasscodeConfirmed.emit(confirmPassword)
+ }
+
+ private fun emitOnPasscodeRejected() = viewModelScope.launch {
+ _onPasscodeRejected.emit(Unit)
+ }
+
+ fun togglePasscodeVisibility() {
+ _passcodeVisible.value = !_passcodeVisible.value
+ }
+
+ private fun resetData() {
+ emitActiveStep(Step.Create)
+ emitFilledDots(0)
+
+ createPasscode.clear()
+ confirmPasscode.clear()
+ }
+
+ fun enterKey(key: String) {
+ if (_filledDots.value >= PASSCODE_LENGTH) {
+ return
+ }
+
+ val currentPasscode =
+ if (_activeStep.value == Step.Create) createPasscode else confirmPasscode
+ currentPasscode.append(key)
+ _currentPasscodeInput.value = currentPasscode.toString()
+ emitFilledDots(currentPasscode.length)
+
+ if (_filledDots.value == PASSCODE_LENGTH) {
+ if (_isPasscodeAlreadySet.value) {
+ if (passcodeRepository.getSavedPasscode() == createPasscode.toString()) {
+ emitOnPasscodeConfirmed(createPasscode.toString())
+ createPasscode.clear()
+ } else {
+ emitOnPasscodeRejected()
+ // logic for retires can be written here
+ }
+ _currentPasscodeInput.value = ""
+ } else if (_activeStep.value == Step.Create) {
+ emitActiveStep(Step.Confirm)
+ emitFilledDots(0)
+ _currentPasscodeInput.value = ""
+ } else {
+ if (createPasscode.toString() == confirmPasscode.toString()) {
+ emitOnPasscodeConfirmed(confirmPasscode.toString())
+ passcodeRepository.savePasscode(confirmPasscode.toString())
+ _isPasscodeAlreadySet.value = true
+ resetData()
+ } else {
+ emitOnPasscodeRejected()
+ resetData()
+ }
+ _currentPasscodeInput.value = ""
+ }
+ }
+ }
+
+ fun deleteKey() {
+ val currentPasscode =
+ if (_activeStep.value == Step.Create) createPasscode else confirmPasscode
+
+ if (currentPasscode.isNotEmpty()) {
+ currentPasscode.deleteAt(currentPasscode.length - 1)
+ _currentPasscodeInput.value = currentPasscode.toString()
+ emitFilledDots(currentPasscode.length)
+ }
+ }
+
+
+ fun deleteAllKeys() {
+ if (_activeStep.value == Step.Create) {
+ createPasscode.clear()
+ } else {
+ confirmPasscode.clear()
+ }
+ _currentPasscodeInput.value = ""
+ emitFilledDots(0)
+ }
+
+ fun restart() {
+ resetData()
+ _passcodeVisible.value = false
+ }
+}
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/desktopMain/kotlin/com/mifos/passcode/CipherUtilImpl.desktop.kt b/libs/cmp-mifos-passcode/src/desktopMain/kotlin/com/mifos/passcode/CipherUtilImpl.desktop.kt
new file mode 100644
index 000000000..ad5e9747e
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/desktopMain/kotlin/com/mifos/passcode/CipherUtilImpl.desktop.kt
@@ -0,0 +1,5 @@
+package com.mifos.passcode
+
+actual class CommonKeyPair
+actual interface CommonPublicKey
+actual class Crypto
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/desktopMain/kotlin/com/mifos/passcode/Platform.desktop.kt b/libs/cmp-mifos-passcode/src/desktopMain/kotlin/com/mifos/passcode/Platform.desktop.kt
new file mode 100644
index 000000000..aa93c4683
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/desktopMain/kotlin/com/mifos/passcode/Platform.desktop.kt
@@ -0,0 +1,8 @@
+package com.mifos.passcode
+
+class DesktopPlatform: com.mifos.passcode.Platform {
+ // override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
+ override val name: String = "Desktop"
+}
+
+actual fun getPlatform(): Platform = DesktopPlatform()
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/iosMain/kotlin/com/mifos/passcode/App.ios.kt b/libs/cmp-mifos-passcode/src/iosMain/kotlin/com/mifos/passcode/App.ios.kt
new file mode 100644
index 000000000..0c7470eb1
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/iosMain/kotlin/com/mifos/passcode/App.ios.kt
@@ -0,0 +1,26 @@
+package com.mifos.passcode
+
+import androidx.compose.ui.window.ComposeUIViewController
+import com.mifos.passcode.utility.BioMetricUtil
+import com.mifos.passcode.component.PasscodeScreen
+import com.mifos.passcode.viewmodels.BiometricAuthorizationViewModel
+import platform.UIKit.UIViewController
+
+fun MainViewController(
+ bioMetricUtil: BioMetricUtil,
+ biometricViewModel: BiometricAuthorizationViewModel
+): UIViewController = ComposeUIViewController {
+ PasscodeScreen(
+ onPasscodeConfirm = {
+ },
+ onSkipButton = {
+ },
+ onForgotButton = {},
+ onPasscodeRejected = {},
+ bioMetricUtil = bioMetricUtil,
+ biometricAuthorizationViewModel = biometricViewModel,
+ onBiometricAuthSuccess = {
+ },
+ enableBiometric = true
+ )
+}
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/iosMain/kotlin/com/mifos/passcode/CipherUtilImpl.ios.kt b/libs/cmp-mifos-passcode/src/iosMain/kotlin/com/mifos/passcode/CipherUtilImpl.ios.kt
new file mode 100644
index 000000000..3a8a65bee
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/iosMain/kotlin/com/mifos/passcode/CipherUtilImpl.ios.kt
@@ -0,0 +1,12 @@
+package com.mifos.passcode
+
+import com.mifos.passcode.CommonPublicKey
+
+actual data class CommonKeyPair(val publicKey: String?, val privateKey: String?)
+actual interface CommonPublicKey {
+ val encoded: String?
+}
+actual class Crypto
+
+data class CommonPublicKeyImpl(override val encoded: String):
+ com.mifos.passcode.CommonPublicKey
\ No newline at end of file
diff --git a/libs/cmp-mifos-passcode/src/iosMain/kotlin/com/mifos/passcode/Platform.ios.kt b/libs/cmp-mifos-passcode/src/iosMain/kotlin/com/mifos/passcode/Platform.ios.kt
new file mode 100644
index 000000000..37447a6f7
--- /dev/null
+++ b/libs/cmp-mifos-passcode/src/iosMain/kotlin/com/mifos/passcode/Platform.ios.kt
@@ -0,0 +1,8 @@
+package com.mifos.passcode
+
+class IOSPlatform: com.mifos.passcode.Platform {
+// override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
+ override val name: String = "Ios"
+}
+
+actual fun getPlatform(): com.mifos.passcode.Platform = IOSPlatform()
\ No newline at end of file
diff --git a/mifospay-android/src/main/res/values/strings.xml b/mifospay-android/src/main/res/values/strings.xml
index 8334b94dc..052bfa7cf 100644
--- a/mifospay-android/src/main/res/values/strings.xml
+++ b/mifospay-android/src/main/res/values/strings.xml
@@ -16,5 +16,7 @@
Profile
⚠️ You aren’t connected to the internet
FAQ
-
+ Login
+ Unlock Mifos
+ Confirm your screen lock pattern, PIN, password, or fingerprint to unlock
diff --git a/mifospay-ios/iosApp.xcodeproj/project.pbxproj b/mifospay-ios/iosApp.xcodeproj/project.pbxproj
index f20cd9102..f4913f99e 100644
--- a/mifospay-ios/iosApp.xcodeproj/project.pbxproj
+++ b/mifospay-ios/iosApp.xcodeproj/project.pbxproj
@@ -11,6 +11,9 @@
189117BF2C6CC76200DABAA8 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 189117BE2C6CC76200DABAA8 /* ContentView.swift */; };
189117C12C6CC76400DABAA8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 189117C02C6CC76400DABAA8 /* Assets.xcassets */; };
189117C42C6CC76400DABAA8 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 189117C32C6CC76400DABAA8 /* Preview Assets.xcassets */; };
+ 8E096B732C90B3B600C3BEA6 /* BiometricUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E096B722C90B3B600C3BEA6 /* BiometricUtil.swift */; };
+ 8E096B752C90B3C100C3BEA6 /* CipherUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E096B742C90B3C100C3BEA6 /* CipherUtil.swift */; };
+ 8EAA94DE2C92A681005081E9 /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 8EAA94DD2C92A681005081E9 /* Info.plist */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@@ -19,6 +22,9 @@
189117BE2C6CC76200DABAA8 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
189117C02C6CC76400DABAA8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
189117C32C6CC76400DABAA8 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
+ 8E096B722C90B3B600C3BEA6 /* BiometricUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiometricUtil.swift; sourceTree = ""; };
+ 8E096B742C90B3C100C3BEA6 /* CipherUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CipherUtil.swift; sourceTree = ""; };
+ 8EAA94DD2C92A681005081E9 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -51,7 +57,9 @@
189117BB2C6CC76200DABAA8 /* iosApp */ = {
isa = PBXGroup;
children = (
+ 8E096B712C90B3A000C3BEA6 /* Biometric */,
189117BC2C6CC76200DABAA8 /* iosAppApp.swift */,
+ 8EAA94DD2C92A681005081E9 /* Info.plist */,
189117BE2C6CC76200DABAA8 /* ContentView.swift */,
189117C02C6CC76400DABAA8 /* Assets.xcassets */,
189117C22C6CC76400DABAA8 /* Preview Content */,
@@ -67,6 +75,15 @@
path = "Preview Content";
sourceTree = "";
};
+ 8E096B712C90B3A000C3BEA6 /* Biometric */ = {
+ isa = PBXGroup;
+ children = (
+ 8E096B722C90B3B600C3BEA6 /* BiometricUtil.swift */,
+ 8E096B742C90B3C100C3BEA6 /* CipherUtil.swift */,
+ );
+ path = Biometric;
+ sourceTree = "";
+ };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -128,6 +145,7 @@
files = (
189117C42C6CC76400DABAA8 /* Preview Assets.xcassets in Resources */,
189117C12C6CC76400DABAA8 /* Assets.xcassets in Resources */,
+ 8EAA94DE2C92A681005081E9 /* Info.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -158,6 +176,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 8E096B752C90B3C100C3BEA6 /* CipherUtil.swift in Sources */,
+ 8E096B732C90B3B600C3BEA6 /* BiometricUtil.swift in Sources */,
189117BF2C6CC76200DABAA8 /* ContentView.swift in Sources */,
189117BD2C6CC76200DABAA8 /* iosAppApp.swift in Sources */,
);
diff --git a/mifospay-ios/iosApp/ContentView.swift b/mifospay-ios/iosApp/ContentView.swift
index 6c5b7759f..e6accf418 100644
--- a/mifospay-ios/iosApp/ContentView.swift
+++ b/mifospay-ios/iosApp/ContentView.swift
@@ -8,13 +8,32 @@
import SwiftUI
import shared
+let biometricUtil = BiometricUtilIosImpl()
struct ContentView: View {
+ let greet = Greeting().greet()
+ @State private var path = NavigationPath()
+
var body: some View {
- Text(Greeting().greet())
- .padding()
+ ZStack {
+ ComposeViewController()
+ }
+ }
+}
+
+struct ComposeViewController: UIViewControllerRepresentable {
+ @StateObject var biometricAuthorizationViewModel: BiometricAuthorizationViewModel = BiometricAuthorizationViewModel()
+ func makeUIViewController(context: Context) -> UIViewController {
+ return App_iosKt.MainViewController(bioMetricUtil: biometricUtil, biometricViewModel: biometricAuthorizationViewModel)
+ }
+
+ func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
}
}
-#Preview {
- ContentView()
+struct ContentView_Previews: PreviewProvider {
+ static var previews: some View {
+ ContentView()
+ }
}
+
+extension BiometricAuthorizationViewModel: ObservableObject {}
\ No newline at end of file
diff --git a/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/PasscodeNavGraph.kt b/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/PasscodeNavGraph.kt
index b65551ece..2965caed5 100644
--- a/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/PasscodeNavGraph.kt
+++ b/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/PasscodeNavGraph.kt
@@ -14,10 +14,15 @@ import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavGraphBuilder
import androidx.navigation.navOptions
import androidx.navigation.navigation
-import org.mifos.library.passcode.PASSCODE_SCREEN
-import org.mifos.library.passcode.passcodeRoute
+import com.mifos.passcode.PASSCODE_SCREEN
+import com.mifos.passcode.passcodeRoute
+import com.mifos.passcode.utility.BioMetricUtil
-internal fun NavGraphBuilder.passcodeNavGraph(navController: NavController) {
+internal fun NavGraphBuilder.passcodeNavGraph(
+ navController: NavController,
+ bioMetricUtil: BioMetricUtil,
+ enableBiometric: Boolean,
+) {
navigation(
route = MifosNavGraph.PASSCODE_GRAPH,
startDestination = PASSCODE_SCREEN,
@@ -39,6 +44,12 @@ internal fun NavGraphBuilder.passcodeNavGraph(navController: NavController) {
navController.popBackStack()
navController.navigateToMainGraph()
},
+ onBiometricAuthSucess = {
+ navController.popBackStack()
+ navController.navigate(MifosNavGraph.MAIN_GRAPH)
+ },
+ bioMetricUtil = bioMetricUtil,
+ enableBiometric = enableBiometric,
)
}
}