diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 88d3d2a5e..0705978e9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,7 @@ [versions] accompanistPagerVersion = "0.34.0" activityVersion = "1.9.1" +agp = "8.3.0" androidDesugarJdkLibs = "2.0.4" androidGradlePlugin = "8.5.2" androidTools = "31.5.2" @@ -23,6 +24,7 @@ androidxNavigation = "2.8.0-rc01" androidxProfileinstaller = "1.3.1" androidxTracing = "1.3.0-alpha02" appcompatVersion = "1.7.0" +biometricKtx = "1.1.0" cameraLifecycleVersion = "1.3.4" cameraViewVersion = "1.3.4" coil = "2.6.0" @@ -58,9 +60,12 @@ ktorVersion = "2.3.4" libphonenumberAndroidVersion = "8.13.35" lifecycleExtensionsVersion = "2.2.0" lifecycleVersion = "2.8.4" +lifecycleViewmodelKtx = "2.8.1" logbackClassicVersion = "1.2.3" minSdk = "24" moduleGraph = "2.5.0" +multiplatformSettings = "1.0.0" +navigationComposeVersion = "2.7.0-alpha07" okHttp3Version = "4.12.0" playServicesAuthVersion = "21.2.0" playServicesCodeScanner = "16.1.0" @@ -90,6 +95,7 @@ android-tools-common = { group = "com.android.tools", name = "common", version.r androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidxActivity" } androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityVersion" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompatVersion" } +androidx-biometric = { group = "androidx.biometric", name = "biometric", version.ref = "biometricKtx" } androidx-browser = { group = "androidx.browser", name = "browser", version.ref = "androidxBrowser" } androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "cameraLifecycleVersion" } androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "cameraViewVersion" } @@ -123,6 +129,7 @@ androidx-lifecycle-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewm androidx-lifecycle-runtimeCompose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" } androidx-lifecycle-runtimeTesting = { group = "androidx.lifecycle", name = "lifecycle-runtime-testing", version.ref = "androidxLifecycle" } androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" } +androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "lifecycleViewmodelKtx" } androidx-metrics = { group = "androidx.metrics", name = "metrics-performance", version.ref = "androidxMetrics" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" } androidx-navigation-testing = { group = "androidx.navigation", name = "navigation-testing", version.ref = "androidxNavigation" } @@ -159,6 +166,7 @@ koin-compose-viewmodel = { group = "io.insert-koin", name = "koin-compose-viewmo koin-core = { group = "io.insert-koin", name = "koin-core", version.ref = "koin" } kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib-jdk8", version.ref = "kotlin" } +kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin" } kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinxImmutable" } kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } @@ -180,6 +188,8 @@ lint-api = { group = "com.android.tools.lint", name = "lint-api", version.ref = lint-checks = { group = "com.android.tools.lint", name = "lint-checks", version.ref = "androidTools" } lint-tests = { group = "com.android.tools.lint", name = "lint-tests", version.ref = "androidTools" } logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logbackClassicVersion" } +multiplatform-settings-no-arg = { group = "com.russhwolf", name = "multiplatform-settings-no-arg", version.ref = "multiplatformSettings" } +navigation-compose = { group = "org.jetbrains.androidx.navigation", name = "navigation-compose", version.ref = "navigationComposeVersion" } play-services-auth = { group = "com.google.android.gms", name = "play-services-auth", version.ref = "playServicesAuthVersion" } protobuf-kotlin-lite = { group = "com.google.protobuf", name = "protobuf-kotlin-lite", version.ref = "protobuf" } protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" } diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index f20cd9102..f4913f99e 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/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/iosApp/iosApp/Biometric/BiometricUtil.swift b/iosApp/iosApp/Biometric/BiometricUtil.swift new file mode 100644 index 000000000..e9910c3d5 --- /dev/null +++ b/iosApp/iosApp/Biometric/BiometricUtil.swift @@ -0,0 +1,112 @@ +import Foundation +import shared +import LocalAuthentication + +class BiometricUtilIosImpl: BioMetricUtil { + + private let cipherUtil = CipherUtilIosImpl() + + private var promptDescription: String = "Authenticate" + + func setAndReturnPublicKey(completionHandler: @escaping (String?, (any Error)?) -> Void) { + + let laContext = LAContext() + laContext.localizedReason = promptDescription + laContext.localizedFallbackTitle = "Cancel" + laContext.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: promptDescription) { + [weak self] success, authenticationError in + + DispatchQueue.main.async { + if success { + completionHandler(self?.generatePublicKey(), nil) + } else { + completionHandler(nil, authenticationError) + } + } + } + } + + func authenticate() async throws -> AuthenticationResult { + do { + _ = try self.cipherUtil.getCrypto() + return AuthenticationResult.Success() + } catch { + print("AuthenticateError: \(error.localizedDescription)") + return AuthenticationResult.Error(error: error.localizedDescription) + } + } + + func canAuthenticate() -> Bool { + var error: NSError? + let laContext = LAContext() + return laContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) + } + + + func generatePublicKey() -> String? { + let keyPair = try? cipherUtil.generateKeyPair() + return keyPair?.publicKey?.toPemFormat().toBase64() + } + + func getPublicKey() -> String? { + return cipherUtil.getPublicKey()?.encoded?.toPemFormat().toBase64() + } + + func isBiometricSet() -> Bool { + return (getPublicKey() != nil) && isValidCrypto() + } + + func isValidCrypto() -> Bool { + do { + _ = try cipherUtil.getCrypto() + return true + } catch { + return false + } + } + + func signUserId(ucc: String) -> String { + guard let data = ucc.data(using: .utf8) else { + print("Failed to convert UCC to data") + fatalError() + } + + var error: Unmanaged? + guard let signature = SecKeyCreateSignature(cipherUtil.getKey()!, .rsaSignatureMessagePKCS1v15SHA256, data as CFData, &error) else { + if let error = error { + print("Error creating signature: \(error.takeRetainedValue())") + } + fatalError() + } + + return (signature as Data).base64EncodedString() + } + + +} + +extension String { + func toPemFormat() -> String { + let chunkSize = 64 + var pemString = "-----BEGIN RSA PUBLIC KEY-----\n" + var base64String = self + while base64String.count > 0 { + let chunkIndex = base64String.index(base64String.startIndex, offsetBy: min(chunkSize, base64String.count)) + let chunk = base64String[.. String? { + guard let data = self.data(using: .utf8) else { + return nil + } + return data.base64EncodedString() + } +} \ No newline at end of file diff --git a/iosApp/iosApp/Biometric/CipherUtil.swift b/iosApp/iosApp/Biometric/CipherUtil.swift new file mode 100644 index 000000000..2b1daea27 --- /dev/null +++ b/iosApp/iosApp/Biometric/CipherUtil.swift @@ -0,0 +1,102 @@ +import Foundation +import shared + +class CipherUtilIosImpl: ICipherUtil { + private let KEY_NAME = "my_biometric_key" + private let tag: Data + + private lazy var key: SecKey? = { + let query: [String: Any] = [ + kSecClass as String: kSecClassKey, + kSecAttrApplicationTag as String: tag, + kSecAttrKeyType as String: kSecAttrKeyTypeRSA, + kSecReturnRef as String: true + ] + var item: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &item) + guard status == errSecSuccess else { return nil } + return (item as! SecKey) + }() + + init() { + self.tag = KEY_NAME.data(using: .utf8)! + } + + func generateKeyPair() throws -> CommonKeyPair { + let access = SecAccessControlCreateWithFlags( + nil, + kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, + .userPresence, + nil + )! + let attributes: [String: Any] = [ + kSecAttrKeyType as String: kSecAttrKeyTypeRSA, + kSecAttrKeySizeInBits as String: 2048, + kSecPrivateKeyAttrs as String: [ + kSecAttrIsPermanent as String: true, + kSecAttrApplicationTag as String: tag, + kSecAttrAccessControl as String: access, + ] + ] + + var error: Unmanaged? + guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else { + throw error!.takeRetainedValue() as Error + } + guard let publicKey = SecKeyCopyPublicKey(privateKey) else { + throw NSError(domain: NSOSStatusErrorDomain, code: Int(errSecInternalError), userInfo: nil) + } + + let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, nil)! as Data + let privateKeyData = SecKeyCopyExternalRepresentation(privateKey, nil)! as Data + + return CommonKeyPair(publicKey: publicKeyData.base64EncodedString(), privateKey: privateKeyData.base64EncodedString()) + } + + func getCrypto() throws -> Crypto { + guard let privateKey = getKey() else { + throw NSError(domain: NSOSStatusErrorDomain, code: Int(errSecItemNotFound), userInfo: nil) + } + + let publicKey = SecKeyCopyPublicKey(privateKey)! + let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, nil)! as Data + let privateKeyData = SecKeyCopyExternalRepresentation(privateKey, nil)! as Data + + UserDefaults.standard.setValue(publicKeyData.base64EncodedString(), forKey: "PublicKey") + + return Crypto() + } + + func getPublicKey() -> (any CommonPublicKey)? { + let savedPublicKey = UserDefaults.standard.string(forKey: "PublicKey") + if (savedPublicKey != nil) { + return CommonPublicKeyImpl(encoded: savedPublicKey!) + } + + guard let privateKey = getKey() else { return CommonPublicKeyImpl(encoded: "") } + guard let publicKey = SecKeyCopyPublicKey(privateKey) else { return CommonPublicKeyImpl(encoded: "") } + let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, nil)! as Data + let publicKeyString = publicKeyData.base64EncodedString() + UserDefaults.standard.setValue(publicKeyString, forKey: "PublicKey") + return CommonPublicKeyImpl(encoded: publicKeyString) + } + + + func removePublicKey() async throws { + UserDefaults.standard.removeObject(forKey: "PublicKey") + let query: [String: Any] = [ + kSecClass as String: kSecClassKey, + kSecAttrApplicationTag as String: tag, + kSecAttrKeyType as String: kSecAttrKeyTypeRSA + ] + let status = SecItemDelete(query as CFDictionary) + if status != errSecSuccess && status != errSecItemNotFound { + throw NSError(domain: NSOSStatusErrorDomain, code: Int(status), userInfo: nil) + } + } + + func getKey() -> SecKey? { + return key + } + +} diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift index db5219cba..2fe20dad9 100644 --- a/iosApp/iosApp/ContentView.swift +++ b/iosApp/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/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist new file mode 100644 index 000000000..2a21373b3 --- /dev/null +++ b/iosApp/iosApp/Info.plist @@ -0,0 +1,50 @@ + + + + + NSFaceIDUsageDescription + Set Biometric 2FA + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UILaunchScreen + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + 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/build.gradle.kts b/mifospay/build.gradle.kts index 4460c2e83..e4272d58b 100644 --- a/mifospay/build.gradle.kts +++ b/mifospay/build.gradle.kts @@ -7,6 +7,7 @@ * * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ + import org.mifospay.MifosBuildType /* @@ -116,7 +117,7 @@ dependencies { implementation(projects.feature.standingInstruction) implementation(projects.feature.search) - implementation(projects.libs.mifosPasscode) + implementation(projects.libs.cmpMifosPasscode) implementation(projects.libs.material3Navigation) // Compose diff --git a/mifospay/dependencies/prodReleaseRuntimeClasspath.tree.txt b/mifospay/dependencies/prodReleaseRuntimeClasspath.tree.txt index b96d6758e..4414f0cc8 100644 --- a/mifospay/dependencies/prodReleaseRuntimeClasspath.tree.txt +++ b/mifospay/dependencies/prodReleaseRuntimeClasspath.tree.txt @@ -230,9 +230,9 @@ | +--- androidx.compose.animation:animation-graphics:1.6.8 -> 1.7.0-rc01 (c) | +--- androidx.compose.ui:ui-geometry:1.6.8 -> 1.7.0-rc01 (c) | +--- androidx.compose.animation:animation-core:1.6.8 -> 1.7.0-rc01 (c) -| +--- androidx.compose.foundation:foundation-layout-android:1.6.8 -> 1.7.0-rc01 (c) | +--- androidx.compose.runtime:runtime-android:1.6.8 -> 1.7.0-rc01 (c) | +--- androidx.compose.ui:ui-android:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.foundation:foundation-layout-android:1.6.8 -> 1.7.0-rc01 (c) | +--- androidx.compose.runtime:runtime-saveable-android:1.6.8 -> 1.7.0-rc01 (c) | +--- androidx.compose.material3:material3-android:1.2.1 (c) | +--- androidx.compose.material:material-icons-extended-android:1.6.8 (c) @@ -243,11 +243,11 @@ | +--- androidx.compose.ui:ui-text:1.6.8 -> 1.7.0-rc01 (c) | +--- androidx.compose.material:material-icons-core:1.6.8 (c) | +--- androidx.compose.material:material-ripple:1.6.8 (c) +| +--- androidx.compose.ui:ui-geometry-android:1.6.8 -> 1.7.0-rc01 (c) | +--- androidx.compose.ui:ui-unit-android:1.6.8 -> 1.7.0-rc01 (c) | +--- androidx.compose.ui:ui-util-android:1.6.8 -> 1.7.0-rc01 (c) -| +--- androidx.compose.foundation:foundation-android:1.6.8 -> 1.7.0-rc01 (c) -| +--- androidx.compose.ui:ui-geometry-android:1.6.8 -> 1.7.0-rc01 (c) | +--- androidx.compose.ui:ui-tooling-preview-android:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.foundation:foundation-android:1.6.8 -> 1.7.0-rc01 (c) | +--- androidx.compose.ui:ui-graphics-android:1.6.8 -> 1.7.0-rc01 (c) | +--- androidx.compose.ui:ui-text-android:1.6.8 -> 1.7.0-rc01 (c) | +--- androidx.compose.material:material-icons-core-android:1.6.8 (c) @@ -873,6 +873,7 @@ | | | | +--- androidx.lifecycle:lifecycle-common:2.8.4 (c) | | | | +--- androidx.lifecycle:lifecycle-common-java8:2.8.4 (c) | | | | +--- androidx.lifecycle:lifecycle-livedata:2.8.4 (c) +| | | | +--- androidx.lifecycle:lifecycle-livedata-core:2.8.4 (c) | | | | +--- androidx.lifecycle:lifecycle-process:2.8.4 (c) | | | | +--- androidx.lifecycle:lifecycle-runtime:2.8.4 (c) | | | | +--- androidx.lifecycle:lifecycle-runtime-ktx:2.8.4 (c) @@ -881,7 +882,6 @@ | | | | +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (c) | | | | +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4 (c) | | | | +--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.4 (c) -| | | | +--- androidx.lifecycle:lifecycle-livedata-core:2.8.4 (c) | | | | \--- androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.4 (c) | | | +--- androidx.lifecycle:lifecycle-viewmodel:2.6.1 -> 2.8.4 (*) | | | +--- androidx.profileinstaller:profileinstaller:1.3.1 (*) @@ -965,6 +965,7 @@ | | | +--- androidx.lifecycle:lifecycle-common:2.8.4 (c) | | | +--- androidx.lifecycle:lifecycle-common-java8:2.8.4 (c) | | | +--- androidx.lifecycle:lifecycle-livedata:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-livedata-core:2.8.4 (c) | | | +--- androidx.lifecycle:lifecycle-process:2.8.4 (c) | | | +--- androidx.lifecycle:lifecycle-runtime:2.8.4 (c) | | | +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (c) @@ -973,107 +974,194 @@ | | | +--- androidx.lifecycle:lifecycle-viewmodel:2.8.4 (c) | | | +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4 (c) | | | +--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.4 (c) -| | | +--- androidx.lifecycle:lifecycle-livedata-core:2.8.4 (c) | | | \--- androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.4 (c) | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.24 -> 2.0.20 (*) | +--- io.insert-koin:koin-core:3.6.0-Beta4 (*) +| +--- project :libs:cmp-mifos-passcode +| | +--- androidx.biometric:biometric:1.1.0 +| | | +--- androidx.activity:activity:1.1.0 -> 1.9.1 (*) +| | | +--- androidx.appcompat:appcompat:1.2.0 -> 1.7.0 (*) +| | | +--- androidx.lifecycle:lifecycle-livedata-core:2.2.0 -> 2.8.4 (*) +| | | +--- androidx.lifecycle:lifecycle-viewmodel:2.2.0 -> 2.8.4 (*) +| | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | +--- androidx.core:core:1.3.2 -> 1.13.1 (*) +| | | \--- androidx.fragment:fragment:1.2.5 -> 1.7.1 (*) +| | +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| | +--- androidx.lifecycle:lifecycle-viewmodel:2.8.1 -> 2.8.4 (*) +| | +--- org.jetbrains.compose.ui:ui:1.6.11 +| | | \--- androidx.compose.ui:ui:1.6.7 -> 1.7.0-rc01 (*) +| | +--- org.jetbrains.compose.runtime:runtime:1.6.11 (*) +| | +--- org.jetbrains.compose.foundation:foundation:1.6.11 +| | | \--- androidx.compose.foundation:foundation:1.6.7 -> 1.7.0-rc01 +| | | \--- androidx.compose.foundation:foundation-android:1.7.0-rc01 +| | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*) +| | | +--- androidx.collection:collection:1.4.0 -> 1.4.2 (*) +| | | +--- androidx.compose.animation:animation:1.7.0-rc01 +| | | | \--- androidx.compose.animation:animation-android:1.7.0-rc01 +| | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | | +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*) +| | | | +--- androidx.collection:collection:1.4.0 -> 1.4.2 (*) +| | | | +--- androidx.compose.animation:animation-core:1.7.0-rc01 +| | | | | \--- androidx.compose.animation:animation-core-android:1.7.0-rc01 +| | | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | | | +--- androidx.collection:collection:1.4.0 -> 1.4.2 (*) +| | | | | +--- androidx.compose.runtime:runtime:1.7.0-rc01 (*) +| | | | | +--- androidx.compose.ui:ui:1.6.0 -> 1.7.0-rc01 (*) +| | | | | +--- androidx.compose.ui:ui-graphics:1.7.0-rc01 (*) +| | | | | +--- androidx.compose.ui:ui-unit:1.6.0 -> 1.7.0-rc01 (*) +| | | | | +--- androidx.compose.ui:ui-util:1.7.0-rc01 (*) +| | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 -> 1.8.1 (*) +| | | | | +--- androidx.compose.animation:animation:1.7.0-rc01 (c) +| | | | | \--- androidx.compose.animation:animation-graphics:1.7.0-rc01 (c) +| | | | +--- androidx.compose.foundation:foundation-layout:1.6.0 -> 1.7.0-rc01 +| | | | | \--- androidx.compose.foundation:foundation-layout-android:1.7.0-rc01 +| | | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | | | +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*) +| | | | | +--- androidx.collection:collection:1.4.0 -> 1.4.2 (*) +| | | | | +--- androidx.compose.animation:animation-core:1.2.1 -> 1.7.0-rc01 (*) +| | | | | +--- androidx.compose.runtime:runtime:1.7.0-rc01 (*) +| | | | | +--- androidx.compose.ui:ui:1.6.0 -> 1.7.0-rc01 (*) +| | | | | +--- androidx.compose.ui:ui-unit:1.7.0-rc01 (*) +| | | | | +--- androidx.compose.ui:ui-util:1.7.0-rc01 (*) +| | | | | +--- androidx.core:core:1.7.0 -> 1.13.1 (*) +| | | | | \--- androidx.compose.foundation:foundation:1.7.0-rc01 (c) +| | | | +--- androidx.compose.runtime:runtime:1.7.0-rc01 (*) +| | | | +--- androidx.compose.ui:ui:1.7.0-rc01 (*) +| | | | +--- androidx.compose.ui:ui-geometry:1.6.0 -> 1.7.0-rc01 (*) +| | | | +--- androidx.compose.ui:ui-graphics:1.7.0-rc01 (*) +| | | | +--- androidx.compose.ui:ui-util:1.7.0-rc01 (*) +| | | | +--- androidx.compose.animation:animation-core:1.7.0-rc01 (c) +| | | | \--- androidx.compose.animation:animation-graphics:1.7.0-rc01 (c) +| | | +--- androidx.compose.foundation:foundation-layout:1.7.0-rc01 (*) +| | | +--- androidx.compose.runtime:runtime:1.7.0-rc01 (*) +| | | +--- androidx.compose.ui:ui:1.7.0-rc01 (*) +| | | +--- androidx.compose.ui:ui-text:1.6.0 -> 1.7.0-rc01 (*) +| | | +--- androidx.compose.ui:ui-util:1.6.0 -> 1.7.0-rc01 (*) +| | | +--- androidx.core:core:1.13.1 (*) +| | | +--- androidx.emoji2:emoji2:1.3.0 (*) +| | | \--- androidx.compose.foundation:foundation-layout:1.7.0-rc01 (c) +| | +--- org.jetbrains.compose.material3:material3:1.6.11 +| | | \--- androidx.compose.material3:material3:1.2.1 +| | | \--- androidx.compose.material3:material3-android:1.2.1 +| | | +--- androidx.activity:activity-compose:1.5.0 -> 1.9.1 (*) +| | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*) +| | | +--- androidx.collection:collection:1.4.0 -> 1.4.2 (*) +| | | +--- androidx.compose.animation:animation-core:1.6.0 -> 1.7.0-rc01 (*) +| | | +--- androidx.compose.foundation:foundation:1.6.0 -> 1.7.0-rc01 (*) +| | | +--- androidx.compose.foundation:foundation-layout:1.6.0 -> 1.7.0-rc01 (*) +| | | +--- androidx.compose.material:material-icons-core:1.6.0 -> 1.6.8 +| | | | \--- androidx.compose.material:material-icons-core-android:1.6.8 +| | | | +--- androidx.compose.ui:ui:1.6.8 -> 1.7.0-rc01 (*) +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | | +--- androidx.compose.material:material-icons-extended:1.6.8 (c) +| | | | \--- androidx.compose.material:material-ripple:1.6.8 (c) +| | | +--- androidx.compose.material:material-ripple:1.6.0 -> 1.6.8 +| | | | \--- androidx.compose.material:material-ripple-android:1.6.8 +| | | | +--- androidx.compose.animation:animation:1.6.8 -> 1.7.0-rc01 (*) +| | | | +--- androidx.compose.foundation:foundation:1.6.8 -> 1.7.0-rc01 (*) +| | | | +--- androidx.compose.runtime:runtime:1.6.8 -> 1.7.0-rc01 (*) +| | | | +--- androidx.compose.ui:ui-util:1.6.8 -> 1.7.0-rc01 (*) +| | | | +--- androidx.compose.material:material-icons-core:1.6.8 (c) +| | | | \--- androidx.compose.material:material-icons-extended:1.6.8 (c) +| | | +--- androidx.compose.runtime:runtime:1.6.0 -> 1.7.0-rc01 (*) +| | | +--- androidx.compose.ui:ui-graphics:1.6.0 -> 1.7.0-rc01 (*) +| | | +--- androidx.compose.ui:ui-text:1.6.0 -> 1.7.0-rc01 (*) +| | | +--- androidx.compose.ui:ui-util:1.6.0 -> 1.7.0-rc01 (*) +| | | +--- androidx.lifecycle:lifecycle-common-java8:2.6.1 -> 2.8.4 (*) +| | | +--- androidx.lifecycle:lifecycle-runtime:2.6.1 -> 2.8.4 (*) +| | | +--- androidx.lifecycle:lifecycle-viewmodel:2.6.1 -> 2.8.4 (*) +| | | +--- androidx.savedstate:savedstate-ktx:1.2.1 (*) +| | | \--- androidx.compose.material3:material3-window-size-class:1.2.1 (c) +| | +--- org.jetbrains.compose.components:components-resources:1.6.11 +| | | \--- org.jetbrains.compose.components:components-resources-android:1.6.11 +| | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.9.23 -> 2.0.20 (*) +| | | +--- org.jetbrains.compose.runtime:runtime:1.6.11 (*) +| | | +--- org.jetbrains.compose.foundation:foundation:1.6.11 (*) +| | | \--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0 -> 1.8.1 (*) +| | +--- org.jetbrains.androidx.navigation:navigation-compose:2.7.0-alpha07 +| | | \--- androidx.navigation:navigation-compose:2.7.7 -> 2.8.0-rc01 +| | | +--- androidx.activity:activity-compose:1.8.0 -> 1.9.1 (*) +| | | +--- androidx.compose.animation:animation:1.7.0-rc01 (*) +| | | +--- androidx.compose.foundation:foundation-layout:1.7.0-rc01 (*) +| | | +--- androidx.compose.runtime:runtime:1.7.0-rc01 (*) +| | | +--- androidx.compose.runtime:runtime-saveable:1.7.0-rc01 (*) +| | | +--- androidx.compose.ui:ui:1.7.0-rc01 (*) +| | | +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2 -> 2.8.4 (*) +| | | +--- androidx.navigation:navigation-runtime-ktx:2.8.0-rc01 +| | | | +--- androidx.navigation:navigation-common-ktx:2.8.0-rc01 +| | | | | +--- androidx.navigation:navigation-common:2.8.0-rc01 +| | | | | | +--- androidx.annotation:annotation:1.8.1 (*) +| | | | | | +--- androidx.collection:collection-ktx:1.4.2 (*) +| | | | | | +--- androidx.core:core-ktx:1.1.0 -> 1.13.1 (*) +| | | | | | +--- androidx.lifecycle:lifecycle-common:2.6.2 -> 2.8.4 (*) +| | | | | | +--- androidx.lifecycle:lifecycle-runtime-ktx:2.6.2 -> 2.8.4 (*) +| | | | | | +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2 -> 2.8.4 (*) +| | | | | | +--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.2 -> 2.8.4 (*) +| | | | | | +--- androidx.profileinstaller:profileinstaller:1.3.1 (*) +| | | | | | +--- androidx.savedstate:savedstate-ktx:1.2.1 (*) +| | | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | | | | +--- org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3 -> 1.7.1 +| | | | | | | \--- org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.1 +| | | | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.0 -> 2.0.20 (*) +| | | | | | | +--- org.jetbrains.kotlinx:kotlinx-serialization-bom:1.7.1 +| | | | | | | | +--- org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.1 (c) +| | | | | | | | +--- org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.7.1 (c) +| | | | | | | | +--- org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1 (c) +| | | | | | | | \--- org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.1 (c) +| | | | | | | \--- org.jetbrains.kotlin:kotlin-stdlib-common:2.0.0 -> 2.0.20 +| | | | | | | \--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| | | | | | +--- androidx.navigation:navigation-common-ktx:2.8.0-rc01 (c) +| | | | | | +--- androidx.navigation:navigation-compose:2.8.0-rc01 (c) +| | | | | | +--- androidx.navigation:navigation-runtime:2.8.0-rc01 (c) +| | | | | | \--- androidx.navigation:navigation-runtime-ktx:2.8.0-rc01 (c) +| | | | | +--- androidx.navigation:navigation-common:2.8.0-rc01 (c) +| | | | | +--- androidx.navigation:navigation-compose:2.8.0-rc01 (c) +| | | | | +--- androidx.navigation:navigation-runtime:2.8.0-rc01 (c) +| | | | | \--- androidx.navigation:navigation-runtime-ktx:2.8.0-rc01 (c) +| | | | +--- androidx.navigation:navigation-runtime:2.8.0-rc01 +| | | | | +--- androidx.activity:activity-ktx:1.7.1 -> 1.9.1 (*) +| | | | | +--- androidx.annotation:annotation-experimental:1.4.1 (*) +| | | | | +--- androidx.collection:collection:1.4.2 (*) +| | | | | +--- androidx.lifecycle:lifecycle-runtime-ktx:2.6.2 -> 2.8.4 (*) +| | | | | +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2 -> 2.8.4 (*) +| | | | | +--- androidx.navigation:navigation-common:2.8.0-rc01 (*) +| | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | | | +--- org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3 -> 1.7.1 (*) +| | | | | +--- androidx.navigation:navigation-common:2.8.0-rc01 (c) +| | | | | +--- androidx.navigation:navigation-common-ktx:2.8.0-rc01 (c) +| | | | | +--- androidx.navigation:navigation-compose:2.8.0-rc01 (c) +| | | | | \--- androidx.navigation:navigation-runtime-ktx:2.8.0-rc01 (c) +| | | | +--- androidx.navigation:navigation-common-ktx:2.8.0-rc01 (c) +| | | | +--- androidx.navigation:navigation-compose:2.8.0-rc01 (c) +| | | | +--- androidx.navigation:navigation-runtime:2.8.0-rc01 (c) +| | | | \--- androidx.navigation:navigation-common:2.8.0-rc01 (c) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | +--- org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3 -> 1.7.1 (*) +| | | +--- androidx.navigation:navigation-runtime-ktx:2.8.0-rc01 (c) +| | | +--- androidx.navigation:navigation-runtime:2.8.0-rc01 (c) +| | | +--- androidx.navigation:navigation-common-ktx:2.8.0-rc01 (c) +| | | \--- androidx.navigation:navigation-common:2.8.0-rc01 (c) +| | +--- com.russhwolf:multiplatform-settings-no-arg:1.0.0 +| | | \--- com.russhwolf:multiplatform-settings-no-arg-android:1.0.0 +| | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0 -> 1.9.20 (*) +| | | +--- androidx.startup:startup-runtime:1.1.1 (*) +| | | \--- com.russhwolf:multiplatform-settings:1.0.0 +| | | \--- com.russhwolf:multiplatform-settings-android:1.0.0 +| | | \--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0 -> 1.9.20 (*) +| | +--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) +| | \--- androidx.compose.ui:ui:1.6.8 -> 1.7.0-rc01 (*) | +--- com.squareup.wire:wire-runtime:5.0.0 | | \--- com.squareup.wire:wire-runtime-jvm:5.0.0 | | +--- com.squareup.okio:okio:3.9.0 (*) | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.24 -> 2.0.20 (*) | +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) | +--- org.jetbrains.compose.runtime:runtime:1.6.11 (*) -| +--- org.jetbrains.compose.material3:material3:1.6.11 -| | \--- androidx.compose.material3:material3:1.2.1 -| | \--- androidx.compose.material3:material3-android:1.2.1 -| | +--- androidx.activity:activity-compose:1.5.0 -> 1.9.1 (*) -| | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) -| | +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*) -| | +--- androidx.collection:collection:1.4.0 -> 1.4.2 (*) -| | +--- androidx.compose.animation:animation-core:1.6.0 -> 1.7.0-rc01 -| | | \--- androidx.compose.animation:animation-core-android:1.7.0-rc01 -| | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) -| | | +--- androidx.collection:collection:1.4.0 -> 1.4.2 (*) -| | | +--- androidx.compose.runtime:runtime:1.7.0-rc01 (*) -| | | +--- androidx.compose.ui:ui:1.6.0 -> 1.7.0-rc01 (*) -| | | +--- androidx.compose.ui:ui-graphics:1.7.0-rc01 (*) -| | | +--- androidx.compose.ui:ui-unit:1.6.0 -> 1.7.0-rc01 (*) -| | | +--- androidx.compose.ui:ui-util:1.7.0-rc01 (*) -| | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) -| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 -> 1.8.1 (*) -| | | +--- androidx.compose.animation:animation:1.7.0-rc01 (c) -| | | \--- androidx.compose.animation:animation-graphics:1.7.0-rc01 (c) -| | +--- androidx.compose.foundation:foundation:1.6.0 -> 1.7.0-rc01 -| | | \--- androidx.compose.foundation:foundation-android:1.7.0-rc01 -| | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) -| | | +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*) -| | | +--- androidx.collection:collection:1.4.0 -> 1.4.2 (*) -| | | +--- androidx.compose.animation:animation:1.7.0-rc01 -| | | | \--- androidx.compose.animation:animation-android:1.7.0-rc01 -| | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) -| | | | +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*) -| | | | +--- androidx.collection:collection:1.4.0 -> 1.4.2 (*) -| | | | +--- androidx.compose.animation:animation-core:1.7.0-rc01 (*) -| | | | +--- androidx.compose.foundation:foundation-layout:1.6.0 -> 1.7.0-rc01 -| | | | | \--- androidx.compose.foundation:foundation-layout-android:1.7.0-rc01 -| | | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) -| | | | | +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*) -| | | | | +--- androidx.collection:collection:1.4.0 -> 1.4.2 (*) -| | | | | +--- androidx.compose.animation:animation-core:1.2.1 -> 1.7.0-rc01 (*) -| | | | | +--- androidx.compose.runtime:runtime:1.7.0-rc01 (*) -| | | | | +--- androidx.compose.ui:ui:1.6.0 -> 1.7.0-rc01 (*) -| | | | | +--- androidx.compose.ui:ui-unit:1.7.0-rc01 (*) -| | | | | +--- androidx.compose.ui:ui-util:1.7.0-rc01 (*) -| | | | | +--- androidx.core:core:1.7.0 -> 1.13.1 (*) -| | | | | \--- androidx.compose.foundation:foundation:1.7.0-rc01 (c) -| | | | +--- androidx.compose.runtime:runtime:1.7.0-rc01 (*) -| | | | +--- androidx.compose.ui:ui:1.7.0-rc01 (*) -| | | | +--- androidx.compose.ui:ui-geometry:1.6.0 -> 1.7.0-rc01 (*) -| | | | +--- androidx.compose.ui:ui-graphics:1.7.0-rc01 (*) -| | | | +--- androidx.compose.ui:ui-util:1.7.0-rc01 (*) -| | | | +--- androidx.compose.animation:animation-core:1.7.0-rc01 (c) -| | | | \--- androidx.compose.animation:animation-graphics:1.7.0-rc01 (c) -| | | +--- androidx.compose.foundation:foundation-layout:1.7.0-rc01 (*) -| | | +--- androidx.compose.runtime:runtime:1.7.0-rc01 (*) -| | | +--- androidx.compose.ui:ui:1.7.0-rc01 (*) -| | | +--- androidx.compose.ui:ui-text:1.6.0 -> 1.7.0-rc01 (*) -| | | +--- androidx.compose.ui:ui-util:1.6.0 -> 1.7.0-rc01 (*) -| | | +--- androidx.core:core:1.13.1 (*) -| | | +--- androidx.emoji2:emoji2:1.3.0 (*) -| | | \--- androidx.compose.foundation:foundation-layout:1.7.0-rc01 (c) -| | +--- androidx.compose.foundation:foundation-layout:1.6.0 -> 1.7.0-rc01 (*) -| | +--- androidx.compose.material:material-icons-core:1.6.0 -> 1.6.8 -| | | \--- androidx.compose.material:material-icons-core-android:1.6.8 -| | | +--- androidx.compose.ui:ui:1.6.8 -> 1.7.0-rc01 (*) -| | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) -| | | +--- androidx.compose.material:material-icons-extended:1.6.8 (c) -| | | \--- androidx.compose.material:material-ripple:1.6.8 (c) -| | +--- androidx.compose.material:material-ripple:1.6.0 -> 1.6.8 -| | | \--- androidx.compose.material:material-ripple-android:1.6.8 -| | | +--- androidx.compose.animation:animation:1.6.8 -> 1.7.0-rc01 (*) -| | | +--- androidx.compose.foundation:foundation:1.6.8 -> 1.7.0-rc01 (*) -| | | +--- androidx.compose.runtime:runtime:1.6.8 -> 1.7.0-rc01 (*) -| | | +--- androidx.compose.ui:ui-util:1.6.8 -> 1.7.0-rc01 (*) -| | | +--- androidx.compose.material:material-icons-core:1.6.8 (c) -| | | \--- androidx.compose.material:material-icons-extended:1.6.8 (c) -| | +--- androidx.compose.runtime:runtime:1.6.0 -> 1.7.0-rc01 (*) -| | +--- androidx.compose.ui:ui-graphics:1.6.0 -> 1.7.0-rc01 (*) -| | +--- androidx.compose.ui:ui-text:1.6.0 -> 1.7.0-rc01 (*) -| | +--- androidx.compose.ui:ui-util:1.6.0 -> 1.7.0-rc01 (*) -| | +--- androidx.lifecycle:lifecycle-common-java8:2.6.1 -> 2.8.4 (*) -| | +--- androidx.lifecycle:lifecycle-runtime:2.6.1 -> 2.8.4 (*) -| | +--- androidx.lifecycle:lifecycle-viewmodel:2.6.1 -> 2.8.4 (*) -| | +--- androidx.savedstate:savedstate-ktx:1.2.1 (*) -| | \--- androidx.compose.material3:material3-window-size-class:1.2.1 (c) -| +--- org.jetbrains.compose.ui:ui:1.6.11 -| | \--- androidx.compose.ui:ui:1.6.7 -> 1.7.0-rc01 (*) -| +--- org.jetbrains.compose.components:components-resources:1.6.11 -| | \--- org.jetbrains.compose.components:components-resources-android:1.6.11 -| | +--- org.jetbrains.kotlin:kotlin-stdlib:1.9.23 -> 2.0.20 (*) -| | +--- org.jetbrains.compose.runtime:runtime:1.6.11 (*) -| | +--- org.jetbrains.compose.foundation:foundation:1.6.11 -| | | \--- androidx.compose.foundation:foundation:1.6.7 -> 1.7.0-rc01 (*) -| | \--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0 -> 1.8.1 (*) +| +--- org.jetbrains.compose.material3:material3:1.6.11 (*) +| +--- org.jetbrains.compose.ui:ui:1.6.11 (*) +| +--- org.jetbrains.compose.components:components-resources:1.6.11 (*) | +--- org.jetbrains.compose.components:components-ui-tooling-preview:1.6.11 | | \--- org.jetbrains.compose.components:components-ui-tooling-preview-android:1.6.11 | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.23 -> 2.0.20 (*) @@ -1083,77 +1171,15 @@ | +--- org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1 | | \--- org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.7.1 | | +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.0 -> 2.0.20 (*) -| | +--- org.jetbrains.kotlinx:kotlinx-serialization-bom:1.7.1 -| | | +--- org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.1 (c) -| | | +--- org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.7.1 (c) -| | | +--- org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1 (c) -| | | \--- org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.1 (c) -| | \--- org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.1 -| | \--- org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.1 -| | +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.0 -> 2.0.20 (*) -| | +--- org.jetbrains.kotlinx:kotlinx-serialization-bom:1.7.1 (*) -| | \--- org.jetbrains.kotlin:kotlin-stdlib-common:2.0.0 -> 2.0.20 -| | \--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| | +--- org.jetbrains.kotlinx:kotlinx-serialization-bom:1.7.1 (*) +| | \--- org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.1 (*) | +--- io.insert-koin:koin-compose:1.2.0-Beta4 (*) | +--- io.insert-koin:koin-compose-viewmodel:1.2.0-Beta4 | | \--- io.insert-koin:koin-compose-viewmodel-jvm:1.2.0-Beta4 | | +--- io.insert-koin:koin-compose:1.2.0-Beta4 (*) | | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0-rc03 | | | \--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0 -> 2.8.4 (*) -| | +--- org.jetbrains.androidx.navigation:navigation-compose:2.7.0-alpha06 -| | | \--- androidx.navigation:navigation-compose:2.7.7 -> 2.8.0-rc01 -| | | +--- androidx.activity:activity-compose:1.8.0 -> 1.9.1 (*) -| | | +--- androidx.compose.animation:animation:1.7.0-rc01 (*) -| | | +--- androidx.compose.foundation:foundation-layout:1.7.0-rc01 (*) -| | | +--- androidx.compose.runtime:runtime:1.7.0-rc01 (*) -| | | +--- androidx.compose.runtime:runtime-saveable:1.7.0-rc01 (*) -| | | +--- androidx.compose.ui:ui:1.7.0-rc01 (*) -| | | +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2 -> 2.8.4 (*) -| | | +--- androidx.navigation:navigation-runtime-ktx:2.8.0-rc01 -| | | | +--- androidx.navigation:navigation-common-ktx:2.8.0-rc01 -| | | | | +--- androidx.navigation:navigation-common:2.8.0-rc01 -| | | | | | +--- androidx.annotation:annotation:1.8.1 (*) -| | | | | | +--- androidx.collection:collection-ktx:1.4.2 (*) -| | | | | | +--- androidx.core:core-ktx:1.1.0 -> 1.13.1 (*) -| | | | | | +--- androidx.lifecycle:lifecycle-common:2.6.2 -> 2.8.4 (*) -| | | | | | +--- androidx.lifecycle:lifecycle-runtime-ktx:2.6.2 -> 2.8.4 (*) -| | | | | | +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2 -> 2.8.4 (*) -| | | | | | +--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.2 -> 2.8.4 (*) -| | | | | | +--- androidx.profileinstaller:profileinstaller:1.3.1 (*) -| | | | | | +--- androidx.savedstate:savedstate-ktx:1.2.1 (*) -| | | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) -| | | | | | +--- org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3 -> 1.7.1 (*) -| | | | | | +--- androidx.navigation:navigation-common-ktx:2.8.0-rc01 (c) -| | | | | | +--- androidx.navigation:navigation-compose:2.8.0-rc01 (c) -| | | | | | +--- androidx.navigation:navigation-runtime:2.8.0-rc01 (c) -| | | | | | \--- androidx.navigation:navigation-runtime-ktx:2.8.0-rc01 (c) -| | | | | +--- androidx.navigation:navigation-common:2.8.0-rc01 (c) -| | | | | +--- androidx.navigation:navigation-compose:2.8.0-rc01 (c) -| | | | | +--- androidx.navigation:navigation-runtime:2.8.0-rc01 (c) -| | | | | \--- androidx.navigation:navigation-runtime-ktx:2.8.0-rc01 (c) -| | | | +--- androidx.navigation:navigation-runtime:2.8.0-rc01 -| | | | | +--- androidx.activity:activity-ktx:1.7.1 -> 1.9.1 (*) -| | | | | +--- androidx.annotation:annotation-experimental:1.4.1 (*) -| | | | | +--- androidx.collection:collection:1.4.2 (*) -| | | | | +--- androidx.lifecycle:lifecycle-runtime-ktx:2.6.2 -> 2.8.4 (*) -| | | | | +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2 -> 2.8.4 (*) -| | | | | +--- androidx.navigation:navigation-common:2.8.0-rc01 (*) -| | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) -| | | | | +--- org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3 -> 1.7.1 (*) -| | | | | +--- androidx.navigation:navigation-common:2.8.0-rc01 (c) -| | | | | +--- androidx.navigation:navigation-common-ktx:2.8.0-rc01 (c) -| | | | | +--- androidx.navigation:navigation-compose:2.8.0-rc01 (c) -| | | | | \--- androidx.navigation:navigation-runtime-ktx:2.8.0-rc01 (c) -| | | | +--- androidx.navigation:navigation-common-ktx:2.8.0-rc01 (c) -| | | | +--- androidx.navigation:navigation-compose:2.8.0-rc01 (c) -| | | | +--- androidx.navigation:navigation-runtime:2.8.0-rc01 (c) -| | | | \--- androidx.navigation:navigation-common:2.8.0-rc01 (c) -| | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) -| | | +--- org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3 -> 1.7.1 (*) -| | | +--- androidx.navigation:navigation-runtime-ktx:2.8.0-rc01 (c) -| | | +--- androidx.navigation:navigation-runtime:2.8.0-rc01 (c) -| | | +--- androidx.navigation:navigation-common-ktx:2.8.0-rc01 (c) -| | | \--- androidx.navigation:navigation-common:2.8.0-rc01 (c) +| | +--- org.jetbrains.androidx.navigation:navigation-compose:2.7.0-alpha06 -> 2.7.0-alpha07 (*) | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.24 -> 2.0.20 (*) | +--- androidx.datastore:datastore-core-okio:1.1.1 (*) | \--- org.jetbrains.kotlin:kotlin-parcelize-runtime:2.0.20 (*) @@ -1998,23 +2024,7 @@ | +--- androidx.compose:compose-bom:2024.08.00 (*) | +--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) | \--- project :core:data (*) -+--- project :libs:mifos-passcode -| +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) -| +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) -| +--- androidx.compose:compose-bom:2024.08.00 (*) -| +--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) -| +--- com.google.dagger:hilt-android:2.52 (*) -| +--- androidx.core:core-ktx:1.13.1 (*) -| +--- androidx.compose.foundation:foundation -> 1.7.0-rc01 (*) -| +--- androidx.compose.foundation:foundation-layout -> 1.7.0-rc01 (*) -| +--- androidx.compose.material:material-icons-extended -> 1.6.8 (*) -| +--- androidx.compose.material3:material3 -> 1.2.1 (*) -| +--- androidx.compose.runtime:runtime:1.6.8 -> 1.7.0-rc01 (*) -| +--- androidx.compose.ui:ui-util:1.6.8 -> 1.7.0-rc01 (*) -| +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (*) -| +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (*) -| +--- androidx.navigation:navigation-compose:2.8.0-rc01 (*) -| \--- androidx.hilt:hilt-navigation-compose:1.2.0 (*) ++--- project :libs:cmp-mifos-passcode (*) +--- project :libs:material3-navigation (*) +--- androidx.core:core-ktx:1.13.1 (*) +--- androidx.appcompat:appcompat:1.7.0 (*) diff --git a/mifospay/dependencies/prodReleaseRuntimeClasspath.txt b/mifospay/dependencies/prodReleaseRuntimeClasspath.txt index 6dc1ab157..3455cac25 100644 --- a/mifospay/dependencies/prodReleaseRuntimeClasspath.txt +++ b/mifospay/dependencies/prodReleaseRuntimeClasspath.txt @@ -30,9 +30,9 @@ :feature:settings :feature:standing-instruction :feature:upi-setup +:libs:cmp-mifos-passcode :libs:country-code-picker :libs:material3-navigation -:libs:mifos-passcode :libs:pullrefresh :shared androidx.activity:activity-compose:1.9.1 @@ -46,6 +46,7 @@ androidx.appcompat:appcompat:1.7.0 androidx.arch.core:core-common:2.2.0 androidx.arch.core:core-runtime:2.2.0 androidx.autofill:autofill:1.0.0 +androidx.biometric:biometric:1.1.0 androidx.browser:browser:1.8.0 androidx.camera:camera-core:1.3.4 androidx.camera:camera-lifecycle:1.3.4 @@ -263,6 +264,10 @@ com.google.zxing:core:3.5.3 com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0 com.maxkeppeler.sheets-compose-dialogs:calendar:1.3.0 com.maxkeppeler.sheets-compose-dialogs:core:1.3.0 +com.russhwolf:multiplatform-settings-android:1.0.0 +com.russhwolf:multiplatform-settings-no-arg-android:1.0.0 +com.russhwolf:multiplatform-settings-no-arg:1.0.0 +com.russhwolf:multiplatform-settings:1.0.0 com.squareup.okhttp3:logging-interceptor:4.12.0 com.squareup.okhttp3:okhttp:4.12.0 com.squareup.okio:okio-jvm:3.9.0 @@ -324,7 +329,7 @@ jakarta.inject:jakarta.inject-api:2.0.1 javax.inject:javax.inject:1 org.checkerframework:checker-qual:3.12.0 org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0-rc03 -org.jetbrains.androidx.navigation:navigation-compose:2.7.0-alpha06 +org.jetbrains.androidx.navigation:navigation-compose:2.7.0-alpha07 org.jetbrains.compose.components:components-resources-android:1.6.11 org.jetbrains.compose.components:components-resources:1.6.11 org.jetbrains.compose.components:components-ui-tooling-preview-android:1.6.11 diff --git a/mifospay/src/main/java/org/mifospay/MainActivity.kt b/mifospay/src/main/java/org/mifospay/MainActivity.kt index 8416a5d33..b98508a74 100644 --- a/mifospay/src/main/java/org/mifospay/MainActivity.kt +++ b/mifospay/src/main/java/org/mifospay/MainActivity.kt @@ -10,7 +10,6 @@ package org.mifospay import android.os.Bundle -import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels @@ -21,12 +20,18 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.fragment.app.FragmentActivity import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.metrics.performance.JankStats import androidx.navigation.compose.rememberNavController +import com.mifos.passcode.BiometricUtilAndroidImpl +import com.mifos.passcode.CipherUtilAndroidImpl +import com.mifos.passcode.data.PasscodeRepository +import com.mifos.passcode.data.PasscodeRepositoryImpl +import com.mifos.passcode.utility.PreferenceManager import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach @@ -47,7 +52,7 @@ import javax.inject.Inject @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) @AndroidEntryPoint -class MainActivity : ComponentActivity() { +class MainActivity : FragmentActivity() { /** * Lazily inject [JankStats], which is used to track jank throughout the app. @@ -66,6 +71,12 @@ class MainActivity : ComponentActivity() { private val viewModel: MainActivityViewModel by viewModels() + private val bioMetricUtil by lazy { + BiometricUtilAndroidImpl(this, CipherUtilAndroidImpl()) + } + + private lateinit var passcodeRepository: PasscodeRepository + override fun onCreate(savedInstanceState: Bundle?) { val splashScreen = installSplashScreen() super.onCreate(savedInstanceState) @@ -88,6 +99,13 @@ class MainActivity : ComponentActivity() { } } + bioMetricUtil.preparePrompt( + title = getString(R.string.biometric_auth_title), + subtitle = "", + description = getString(R.string.biometric_auth_description), + ) + passcodeRepository = PasscodeRepositoryImpl(PreferenceManager()) + enableEdgeToEdge() setContent { @@ -128,6 +146,8 @@ class MainActivity : ComponentActivity() { } } }, + bioMetricUtil = bioMetricUtil, + enableBiometric = true, ) } } diff --git a/mifospay/src/main/java/org/mifospay/MainActivityViewModel.kt b/mifospay/src/main/java/org/mifospay/MainActivityViewModel.kt index 2a3677a51..5438e819a 100644 --- a/mifospay/src/main/java/org/mifospay/MainActivityViewModel.kt +++ b/mifospay/src/main/java/org/mifospay/MainActivityViewModel.kt @@ -18,14 +18,13 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import org.mifos.library.passcode.data.PasscodeManager import org.mifospay.core.data.repository.auth.UserDataRepository import javax.inject.Inject @HiltViewModel class MainActivityViewModel @Inject constructor( private val userDataRepository: UserDataRepository, - private val passcodeManager: PasscodeManager, +// private val passcodeManager: PasscodeManager, ) : ViewModel() { val uiState: StateFlow = userDataRepository.userData.map { @@ -39,7 +38,7 @@ class MainActivityViewModel @Inject constructor( fun logOut() { viewModelScope.launch { userDataRepository.logOut() - passcodeManager.clearPasscode() +// passcodeManager.clearPasscode() } } } diff --git a/mifospay/src/main/java/org/mifospay/navigation/LoginNavGraph.kt b/mifospay/src/main/java/org/mifospay/navigation/LoginNavGraph.kt index c80043848..4bb11f2b2 100644 --- a/mifospay/src/main/java/org/mifospay/navigation/LoginNavGraph.kt +++ b/mifospay/src/main/java/org/mifospay/navigation/LoginNavGraph.kt @@ -12,7 +12,7 @@ package org.mifospay.navigation import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.navigation -import org.mifos.library.passcode.navigateToPasscodeScreen +import com.mifos.passcode.navigateToPasscodeScreen import org.mifospay.common.Constants import org.mifospay.feature.auth.navigation.LOGIN_ROUTE import org.mifospay.feature.auth.navigation.loginScreen diff --git a/mifospay/src/main/java/org/mifospay/navigation/PasscodeNavGraph.kt b/mifospay/src/main/java/org/mifospay/navigation/PasscodeNavGraph.kt index a672817e2..eaaa8bcdf 100644 --- a/mifospay/src/main/java/org/mifospay/navigation/PasscodeNavGraph.kt +++ b/mifospay/src/main/java/org/mifospay/navigation/PasscodeNavGraph.kt @@ -12,10 +12,15 @@ package org.mifospay.navigation import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder 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, @@ -37,6 +42,12 @@ internal fun NavGraphBuilder.passcodeNavGraph(navController: NavController) { navController.popBackStack() navController.navigate(MifosNavGraph.MAIN_GRAPH) }, + onBiometricAuthSucess = { + navController.popBackStack() + navController.navigate(MifosNavGraph.MAIN_GRAPH) + }, + bioMetricUtil = bioMetricUtil, + enableBiometric = enableBiometric, ) } } diff --git a/mifospay/src/main/java/org/mifospay/navigation/RootNavGraph.kt b/mifospay/src/main/java/org/mifospay/navigation/RootNavGraph.kt index 1a0735b28..a15b95036 100644 --- a/mifospay/src/main/java/org/mifospay/navigation/RootNavGraph.kt +++ b/mifospay/src/main/java/org/mifospay/navigation/RootNavGraph.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import com.mifos.passcode.utility.BioMetricUtil import org.mifospay.ui.MifosApp import org.mifospay.ui.MifosAppState @@ -22,6 +23,8 @@ internal fun RootNavGraph( appState: MifosAppState, navHostController: NavHostController, startDestination: String, + bioMetricUtil: BioMetricUtil, + enableBiometric: Boolean, onClickLogout: () -> Unit, modifier: Modifier = Modifier, ) { @@ -33,7 +36,11 @@ internal fun RootNavGraph( ) { loginNavGraph(navHostController) - passcodeNavGraph(navHostController) + passcodeNavGraph( + navHostController, + bioMetricUtil = bioMetricUtil, + enableBiometric = enableBiometric, + ) composable(MifosNavGraph.MAIN_GRAPH) { MifosApp( diff --git a/mifospay/src/main/res/values/strings.xml b/mifospay/src/main/res/values/strings.xml index 8334b94dc..052bfa7cf 100644 --- a/mifospay/src/main/res/values/strings.xml +++ b/mifospay/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/settings.gradle.kts b/settings.gradle.kts index f43a17c79..7c9e37527 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -78,6 +78,8 @@ include(":libs:country-code-picker") include(":libs:pullrefresh") include(":libs:material3-navigation") include(":libs:mifos-passcode") +include(":libs:cmp-mifos-passcode") include(":shared") include(":desktop") + diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 5c99a7fef..a08fcf95b 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -65,17 +65,19 @@ kotlin { implementation(compose.ui) implementation(compose.components.resources) implementation(compose.components.uiToolingPreview) - implementation(libs.kotlinx.datetime) implementation(libs.kotlinx.serialization.json) - api(libs.koin.core) implementation(libs.koin.compose) implementation(libs.koin.compose.viewmodel) - implementation(libs.datastore) + implementation(project(":libs:cmp-mifos-passcode")) + api(project(":libs:cmp-mifos-passcode")) } + iosMain.dependencies { + api(project(":libs:cmp-mifos-passcode")) + } val desktopMain by getting { dependencies { // Desktop specific dependencies @@ -117,4 +119,4 @@ android { dependencies { debugImplementation(compose.uiTooling) } -} +} \ No newline at end of file diff --git a/shared/src/iosMain/kotlin/org/mifospay/shared/preferences/DataStoreModule.ios.kt b/shared/src/iosMain/kotlin/org/mifospay/shared/preferences/DataStoreModule.ios.kt index 6e7e19d38..72f5d51fd 100644 --- a/shared/src/iosMain/kotlin/org/mifospay/shared/preferences/DataStoreModule.ios.kt +++ b/shared/src/iosMain/kotlin/org/mifospay/shared/preferences/DataStoreModule.ios.kt @@ -10,9 +10,13 @@ package org.mifospay.shared.preferences import androidx.datastore.core.DataStore +import kotlinx.cinterop.ExperimentalForeignApi import okio.FileSystem import okio.Path.Companion.toPath import org.mifospay.shared.commonMain.proto.UserPreferences +import platform.Foundation.NSDocumentDirectory +import platform.Foundation.NSFileManager +import platform.Foundation.NSUserDomainMask actual fun getDataStore(): DataStore { return createDataStore( @@ -21,6 +25,7 @@ actual fun getDataStore(): DataStore { ) } +@OptIn(ExperimentalForeignApi::class) private fun documentDirectory(): String { val documentDirectory = NSFileManager.defaultManager.URLForDirectory( directory = NSDocumentDirectory,