diff --git a/.gitignore b/.gitignore index a674bf8..b371fcc 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ *.class # Generated files +.kotlin/ bin/ gen/ out/ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 65d4004..91df435 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,14 +6,13 @@ espressoCoreVersion = "3.6.0-alpha01" gradle = "8.3.1" hiltCompiler = "2.48.1" hiltNavigationComposeVersion = "1.2.0" -kotlin = "2.0.0" +kotlin = "2.1.0" compose = "1.5.4" compose-material3 = "1.1.2" androidx-activityCompose = "1.8.0" kotlinStdlibJdk8 = "1.7.20" lifecycleViewmodelKtx = "2.8.1" compose-plugin = "1.6.10" -multiplatformSettings = "1.0.0" navigationCompose = "2.7.7" navigationComposeVersion = "2.7.0-alpha07" dagger-hilt = "2.48.1" @@ -31,6 +30,13 @@ appcompatVersion = "1.7.0" testng = "6.9.6" monitor = "1.6.1" runnerVersion = "1.5.2" +koin = "4.0.1-RC1" +protobuf = "4.26.0" +protobufPlugin = "0.9.4" +kotlinxSerializationJson = "1.7.3" +multiplatformSettings = "1.2.0" +kotlinxCoroutines = "1.9.0" +gradle-plugin = "8.2.0" [libraries] android-maven-gradle-plugin = { module = "com.github.dcendents:android-maven-gradle-plugin", version.ref = "androidMavenGradlePlugin" } @@ -68,6 +74,36 @@ testng = { group = "org.testng", name = "testng", version.ref = "testng" } androidx-monitor = { group = "androidx.test", name = "monitor", version.ref = "monitor" } runner = { group = "androidx.test", name = "runner", version.ref = "runnerVersion" } +protobuf-kotlin-lite = { group = "com.google.protobuf", name = "protobuf-kotlin-lite", version.ref = "protobuf" } + +kotlin-reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "kotlin" } + +jb-kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" } + +kotlinx-serialization-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-core", version.ref = "kotlinxSerializationJson" } + +multiplatform-settings = { group = "com.russhwolf", name = "multiplatform-settings-no-arg", version.ref = "multiplatformSettings" } +multiplatform-settings-coroutines = { group = "com.russhwolf", name = "multiplatform-settings-coroutines", version.ref = "multiplatformSettings" } +multiplatform-settings-serialization = { group = "com.russhwolf", name = "multiplatform-settings-serialization", version.ref = "multiplatformSettings" } + +kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } + +kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinxCoroutines" } + +protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" } + +koin-compose = { group = "io.insert-koin", name = "koin-compose", version.ref = "koin" } +koin-compose-viewmodel = { group = "io.insert-koin", name = "koin-compose-viewmodel", version.ref = "koin" } + +android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "gradle-plugin" } +gradle-plugin-development = { module = "org.gradle:gradle-plugin-development", version.ref = "gradle-plugin" } + +androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } +androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-compose-material-iconsExtended = { group = "androidx.compose.material", name = "material-icons-extended" } +compose-gradlePlugin = { group = "org.jetbrains.kotlin", name = "compose-compiler-gradle-plugin", version.ref = "kotlin" } + [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } androidLibrary = { id = "com.android.library", version.ref = "agp" } @@ -78,3 +114,10 @@ kotlinCocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", version.ref = compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } dagger-hilt = { id = "com.google.dagger.hilt.android", version.ref = "dagger-hilt" } jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } + +protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" } + +kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } + +mifospay-cmp-feature = { id = "mifospay.cmp.feature", version = "unspecified" } diff --git a/mifos-passcode/.gitignore b/mifos-passcode/.gitignore index 796b96d..42afabf 100644 --- a/mifos-passcode/.gitignore +++ b/mifos-passcode/.gitignore @@ -1 +1 @@ -/build +/build \ No newline at end of file diff --git a/mifos-passcode/README.md b/mifos-passcode/README.md new file mode 100644 index 0000000..41d45be --- /dev/null +++ b/mifos-passcode/README.md @@ -0,0 +1,6 @@ +# :feature:passcode module +## Dependency graph +![Dependency graph](../docs/images/graphs/dep_graph_feature_passcode.svg) +# :libs:mifos-passcode module +## Dependency graph +![Dependency graph](../../docs/images/graphs-kmp/dep_graph_libs_mifos_passcode.svg) diff --git a/mifos-passcode/build.gradle b/mifos-passcode/build.gradle deleted file mode 100644 index 2fc17de..0000000 --- a/mifos-passcode/build.gradle +++ /dev/null @@ -1,90 +0,0 @@ -plugins { - alias(libs.plugins.androidLibrary) - alias(libs.plugins.kotlinAndroid) -} - -ext { - bintrayRepo = 'maven' - bintrayName = 'mifos-passcode' - - publishedGroupId = 'com.mifos.mobile' - libraryName = 'mifos-passcode' - artifact = 'mifos-passcode' // artifact name and library name should be same. - - libraryDescription = 'A Library as feature of passcode' - - siteUrl = 'https://github.com/openMF/mobile-passcode' - gitUrl = 'https://github.com/openMF/mobile-passcode.git' - - libraryVersion = '1.0.0' - - developerId = 'mifos' - developerName = 'Mifos Initiative' - developerEmail = 'info@mifos.org' - - licenseName = 'The Apache Software License, Version 2.0' - licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt' - allLicenses = ["Apache-2.0"] -} - -android { - compileSdk 34 - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - defaultConfig { - namespace "com.mifos.mobile.passcode" - minSdk 24 - targetSdk 34 - versionCode 2 - versionName "1.0.0" - vectorDrawables.useSupportLibrary = true - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - - buildFeatures { - viewBinding = true - } - kotlinOptions { - jvmTarget = '17' - } -} - -dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) - - implementation libs.androidx.appcompat - implementation libs.material - - testImplementation libs.junit - androidTestImplementation libs.runner - androidTestImplementation libs.androidx.espresso.core - implementation libs.androidx.core.ktx.v1131 - implementation platform(libs.kotlin.bom) -} - -tasks.withType(Javadoc) { - excludes = ['**/*.kt'] - options.addStringOption('Xdoclint:none', '-quiet') - options.addStringOption('encoding', 'UTF-8') - options.addStringOption('charSet', 'UTF-8') -} - -repositories { - mavenCentral() -} -// Used this article to release the library -// https://inthecheesefactory.com/blog/how-to-upload-library-to-jcenter-maven-central-as-dependency/en - -// Add these line in local.properties to direct release on bintray -//bintray.user=YOUR_BINTRAY_USERNAME -//bintray.apikey=YOUR_BINTRAY_API_KEY -//bintray.gpg.password=YOUR_GPG_PASSWORD \ No newline at end of file diff --git a/mifos-passcode/build.gradle.kts b/mifos-passcode/build.gradle.kts new file mode 100644 index 0000000..d142fca --- /dev/null +++ b/mifos-passcode/build.gradle.kts @@ -0,0 +1,105 @@ +/* + * 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 + */ +plugins { + alias(libs.plugins.mifospay.cmp.feature) + alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.protobuf) +} + +android { + namespace = "com.mifos.library.passcode" +} + +kotlin { + sourceSets { + commonMain.dependencies { + implementation(compose.ui) + implementation(compose.foundation) + implementation(compose.material3) + implementation(compose.materialIconsExtended) + implementation(compose.components.resources) + implementation(compose.components.uiToolingPreview) + + implementation(libs.koin.compose.viewmodel) + implementation(libs.koin.compose) + + implementation(libs.jb.kotlin.stdlib) + implementation(libs.kotlin.reflect) + + api(libs.protobuf.kotlin.lite) + implementation(libs.kotlinx.serialization.core) + + implementation(libs.multiplatform.settings) + implementation(libs.multiplatform.settings.serialization) + implementation(libs.multiplatform.settings.coroutines) + + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.serialization.core) + + implementation(project(":core:ui")) + implementation(project(":core:designsystem")) + implementation(project(":core:data")) + + implementation(libs.findLibrary("jb.composeRuntime").get()) + implementation(libs.findLibrary("jb.composeViewmodel").get()) + implementation(libs.findLibrary("jb.lifecycleViewmodel").get()) + implementation(libs.findLibrary("jb.lifecycleViewmodelSavedState").get()) + implementation(libs.findLibrary("jb.savedstate").get()) + implementation(libs.findLibrary("jb.bundle").get()) + implementation(libs.findLibrary("jb.composeNavigation").get()) + implementation(libs.findLibrary("kotlinx.collections.immutable").get()) + } + + androidMain.dependencies { + implementation(libs.findLibrary("androidx.lifecycle.runtimeCompose").get()) + implementation(libs.findLibrary("androidx.lifecycle.viewModelCompose").get()) + implementation(libs.findLibrary("androidx.tracing.ktx").get()) + + implementation(platform(libs.findLibrary("koin-bom").get())) + implementation(libs.findLibrary("koin-android").get()) + implementation(libs.findLibrary("koin.androidx.compose").get()) + implementation(libs.findLibrary("koin.android").get()) + implementation(libs.findLibrary("koin.androidx.navigation").get()) + implementation(libs.findLibrary("koin.androidx.compose").get()) + implementation(libs.findLibrary("koin.core.viewmodel").get()) + } + + androidInstrumentedTest.dependencies { + implementation(libs.findLibrary("androidx.navigation.testing").get()) + implementation(libs.findLibrary("androidx.compose.ui.test").get()) + implementation(libs.findLibrary("androidx.lifecycle.runtimeTesting").get()) + } + + androidTest.dependencies { + implementation(libs.findLibrary("koin.test.junit4").get()) + } + + desktopMain.dependencies { + implementation(libs.kotlinx.coroutines.swing) + } + } +} + +// Setup protobuf configuration, generating lite Java and Kotlin classes +protobuf { + protoc { + artifact = libs.protobuf.protoc.get().toString() + } + generateProtoTasks { + all().forEach { task -> + task.builtins { + register("kotlin") { + option("lite") + } + } + } + } +} \ No newline at end of file diff --git a/mifos-passcode/proguard-rules.pro b/mifos-passcode/proguard-rules.pro deleted file mode 100644 index f1b4245..0000000 --- a/mifos-passcode/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile diff --git a/mifos-passcode/src/androidMain/AndroidManifest.xml b/mifos-passcode/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000..b9b046d --- /dev/null +++ b/mifos-passcode/src/androidMain/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/mifos-passcode/src/commonMain/composeResources/drawable/lib_mifos_passcode_mifos_logo.jpg b/mifos-passcode/src/commonMain/composeResources/drawable/lib_mifos_passcode_mifos_logo.jpg new file mode 100644 index 0000000..a067a3c Binary files /dev/null and b/mifos-passcode/src/commonMain/composeResources/drawable/lib_mifos_passcode_mifos_logo.jpg differ diff --git a/mifos-passcode/src/commonMain/composeResources/font/lib_mifos_passcode_lato_black.ttf b/mifos-passcode/src/commonMain/composeResources/font/lib_mifos_passcode_lato_black.ttf new file mode 100644 index 0000000..4340502 Binary files /dev/null and b/mifos-passcode/src/commonMain/composeResources/font/lib_mifos_passcode_lato_black.ttf differ diff --git a/mifos-passcode/src/commonMain/composeResources/font/lib_mifos_passcode_lato_bold.ttf b/mifos-passcode/src/commonMain/composeResources/font/lib_mifos_passcode_lato_bold.ttf new file mode 100644 index 0000000..016068b Binary files /dev/null and b/mifos-passcode/src/commonMain/composeResources/font/lib_mifos_passcode_lato_bold.ttf differ diff --git a/mifos-passcode/src/commonMain/composeResources/font/lib_mifos_passcode_lato_regular.ttf b/mifos-passcode/src/commonMain/composeResources/font/lib_mifos_passcode_lato_regular.ttf new file mode 100644 index 0000000..bb2e887 Binary files /dev/null and b/mifos-passcode/src/commonMain/composeResources/font/lib_mifos_passcode_lato_regular.ttf differ diff --git a/mifos-passcode/src/commonMain/composeResources/values/strings.xml b/mifos-passcode/src/commonMain/composeResources/values/strings.xml new file mode 100644 index 0000000..4398748 --- /dev/null +++ b/mifos-passcode/src/commonMain/composeResources/values/strings.xml @@ -0,0 +1,34 @@ + + + + Passcode will be reset, are you sure? + Cancel + Yes + Passcode + hasPasscode + hasDragPasscode + passcode + drag_passcode + + Create Passcode + Confirm Passcode + Enter your Passcode + Forgot Passcode + Delete Passcode Key Button + Drag your finger here only in one direction. + Drag your Pattern + Exit + Skip + Forgot Passcode, Login Manually + Try again + Passcode do not match! + Are you sure you want to exit? + \ No newline at end of file diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt new file mode 100644 index 0000000..ba9cf7b --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt @@ -0,0 +1,248 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.Animatable +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.launch +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.koin.compose.viewmodel.koinViewModel +import org.mifos.library.passcode.component.MifosIcon +import org.mifos.library.passcode.component.PasscodeForgotButton +import org.mifos.library.passcode.component.PasscodeHeader +import org.mifos.library.passcode.component.PasscodeKeys +import org.mifos.library.passcode.component.PasscodeMismatchedDialog +import org.mifos.library.passcode.component.PasscodeSkipButton +import org.mifos.library.passcode.component.PasscodeToolbar +import org.mifos.library.passcode.theme.blueTint +import org.mifos.library.passcode.utility.Constants.PASSCODE_LENGTH +import org.mifos.library.passcode.utility.ShakeAnimation.performShakeAnimation +import org.mifos.library.passcode.viewmodels.PasscodeAction +import org.mifos.library.passcode.viewmodels.PasscodeEvent +import org.mifos.library.passcode.viewmodels.PasscodeViewModel +import org.mifospay.core.ui.utils.EventsEffect + +@Composable +internal fun PasscodeScreen( + onForgotButton: () -> Unit, + onSkipButton: () -> Unit, + onPasscodeConfirm: (String) -> Unit, + onPasscodeRejected: () -> Unit, + modifier: Modifier = Modifier, + viewModel: PasscodeViewModel = koinViewModel(), +) { + val scope = rememberCoroutineScope() + val state by viewModel.stateFlow.collectAsStateWithLifecycle() + + val xShake = remember { Animatable(initialValue = 0.0F) } + var passcodeRejectedDialogVisible by remember { mutableStateOf(false) } + + EventsEffect(viewModel) { event -> + when (event) { + is PasscodeEvent.PasscodeConfirmed -> { + onPasscodeConfirm(event.passcode) + } + + is PasscodeEvent.PasscodeRejected -> { + passcodeRejectedDialogVisible = true + scope.launch { + performShakeAnimation(xShake) + } + onPasscodeRejected() + } + } + } + + Scaffold( + modifier = modifier + .fillMaxSize(), + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + .padding(paddingValues), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + PasscodeToolbar(activeStep = state.activeStep, state.hasPasscode) + + PasscodeSkipButton( + hasPassCode = state.hasPasscode, + onSkipButton = onSkipButton, + ) + + MifosIcon(modifier = Modifier.fillMaxWidth()) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp, bottom = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + PasscodeHeader( + activeStep = state.activeStep, + isPasscodeAlreadySet = state.hasPasscode, + ) + PasscodeView( + restart = remember(viewModel) { + { viewModel.trySendAction(PasscodeAction.Restart) } + }, + togglePasscodeVisibility = remember(viewModel) { + { viewModel.trySendAction(PasscodeAction.TogglePasscodeVisibility) } + }, + filledDots = state.filledDots, + passcodeVisible = state.passcodeVisible, + currentPasscode = state.currentPasscodeInput, + passcodeRejectedDialogVisible = passcodeRejectedDialogVisible, + onDismissDialog = { passcodeRejectedDialogVisible = false }, + xShake = xShake, + ) + } + + Spacer(modifier = Modifier.height(6.dp)) + + PasscodeKeys( + enterKey = remember(viewModel) { + { viewModel.trySendAction(PasscodeAction.EnterKey(it)) } + }, + deleteKey = remember(viewModel) { + { viewModel.trySendAction(PasscodeAction.DeleteKey) } + }, + deleteAllKeys = remember(viewModel) { + { viewModel.trySendAction(PasscodeAction.DeleteAllKeys) } + }, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + PasscodeForgotButton( + hasPassCode = state.hasPasscode, + onForgotButton = onForgotButton, + ) + } + } +} + +@Composable +private fun PasscodeView( + restart: () -> Unit, + togglePasscodeVisibility: () -> Unit, + filledDots: Int, + passcodeVisible: Boolean, + currentPasscode: String, + passcodeRejectedDialogVisible: Boolean, + onDismissDialog: () -> Unit, + xShake: Animatable, + modifier: Modifier = Modifier, +) { + PasscodeMismatchedDialog( + visible = passcodeRejectedDialogVisible, + onDismiss = { + onDismissDialog.invoke() + restart() + }, + ) + + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Row( + modifier = Modifier.offset { IntOffset(xShake.value.toInt(), 0) }, + horizontalArrangement = Arrangement.spacedBy( + space = 26.dp, + alignment = Alignment.CenterHorizontally, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + repeat(PASSCODE_LENGTH) { dotIndex -> + if (passcodeVisible && dotIndex < currentPasscode.length) { + Text( + text = currentPasscode[dotIndex].toString(), + color = blueTint, + ) + } else { + val isFilledDot = dotIndex + 1 <= filledDots + val dotColor = animateColorAsState( + if (isFilledDot) blueTint else Color.Gray, + label = "", + ) + + Box( + modifier = Modifier + .size(14.dp) + .background( + color = dotColor.value, + shape = CircleShape, + ), + ) + } + } + } + + IconButton( + onClick = togglePasscodeVisibility, + modifier = Modifier.padding(start = 10.dp), + ) { + Icon( + imageVector = if (passcodeVisible) { + Icons.Filled.Visibility + } else { + Icons.Filled.VisibilityOff + }, + contentDescription = null, + ) + } + } +} + +@Preview +@Composable +private fun PasscodeScreenPreview() { + PasscodeScreen( + onForgotButton = {}, + onSkipButton = {}, + onPasscodeConfirm = {}, + onPasscodeRejected = {}, + ) +} diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PasscodeNavigation.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PasscodeNavigation.kt new file mode 100644 index 0000000..eab00ee --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PasscodeNavigation.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable + +const val PASSCODE_SCREEN = "passcode_screen" + +fun NavGraphBuilder.passcodeRoute( + onForgotButton: () -> Unit, + onSkipButton: () -> Unit, + onPasscodeConfirm: (String) -> Unit, + onPasscodeRejected: () -> Unit, +) { + composable( + route = PASSCODE_SCREEN, + ) { + PasscodeScreen( + onForgotButton = onForgotButton, + onSkipButton = onSkipButton, + onPasscodeConfirm = onPasscodeConfirm, + onPasscodeRejected = onPasscodeRejected, + ) + } +} + +fun NavController.navigateToPasscodeScreen(options: NavOptions? = null) { + navigate(PASSCODE_SCREEN, options) +} diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/MifosIcon.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/MifosIcon.kt new file mode 100644 index 0000000..986fa2e --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/MifosIcon.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import mobile_wallet.libs.mifos_passcode.generated.resources.Res +import mobile_wallet.libs.mifos_passcode.generated.resources.lib_mifos_passcode_mifos_logo +import org.jetbrains.compose.resources.painterResource + +@Composable +internal fun MifosIcon(modifier: Modifier = Modifier) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.Center, + ) { + Image( + modifier = Modifier.size(180.dp), + painter = painterResource(resource = Res.drawable.lib_mifos_passcode_mifos_logo), + contentDescription = null, + ) + } +} diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeButton.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeButton.kt new file mode 100644 index 0000000..bb7ecfd --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeButton.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import mobile_wallet.libs.mifos_passcode.generated.resources.Res +import mobile_wallet.libs.mifos_passcode.generated.resources.library_mifos_passcode_login_manually +import mobile_wallet.libs.mifos_passcode.generated.resources.library_mifos_passcode_skip +import org.jetbrains.compose.resources.stringResource +import org.mifos.library.passcode.theme.forgotButtonStyle +import org.mifos.library.passcode.theme.skipButtonStyle + +@Composable +internal fun PasscodeSkipButton( + hasPassCode: Boolean, + onSkipButton: () -> Unit, + modifier: Modifier = Modifier, +) { + if (!hasPassCode) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(end = 16.dp), + horizontalArrangement = Arrangement.End, + ) { + TextButton( + onClick = { onSkipButton.invoke() }, + ) { + Text( + text = stringResource(Res.string.library_mifos_passcode_skip), + style = skipButtonStyle, + ) + } + } + } +} + +@Composable +internal fun PasscodeForgotButton( + hasPassCode: Boolean, + onForgotButton: () -> Unit, + modifier: Modifier = Modifier, +) { + if (hasPassCode) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(end = 16.dp), + horizontalArrangement = Arrangement.Center, + ) { + TextButton( + onClick = { onForgotButton.invoke() }, + ) { + Text( + text = stringResource(Res.string.library_mifos_passcode_login_manually), + style = forgotButtonStyle, + ) + } + } + } +} diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeHeader.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeHeader.kt new file mode 100644 index 0000000..81eaa19 --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeHeader.kt @@ -0,0 +1,124 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.component + +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.core.Transition +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.animateOffset +import androidx.compose.animation.core.rememberTransition +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.scale +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import mobile_wallet.libs.mifos_passcode.generated.resources.Res +import mobile_wallet.libs.mifos_passcode.generated.resources.library_mifos_passcode_confirm_passcode +import mobile_wallet.libs.mifos_passcode.generated.resources.library_mifos_passcode_create_passcode +import mobile_wallet.libs.mifos_passcode.generated.resources.library_mifos_passcode_enter_your_passcode +import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.mifos.library.passcode.utility.Step + +@Composable +internal fun PasscodeHeader( + activeStep: Step, + isPasscodeAlreadySet: Boolean, + modifier: Modifier = Modifier, +) { + val transitionState = remember { MutableTransitionState(activeStep) } + transitionState.targetState = activeStep + + val transition: Transition = rememberTransition( + transitionState = transitionState, + label = "Headers Transition", + ) + + val offset = 200.0F + val zeroOffset = Offset(x = 0.0F, y = 0.0F) + val negativeOffset = Offset(x = -offset, y = 0.0F) + val positiveOffset = Offset(x = offset, y = 0.0F) + + val xTransitionHeader1 by transition.animateOffset(label = "Transition Offset Header 1") { + if (it == Step.Create) zeroOffset else negativeOffset + } + val xTransitionHeader2 by transition.animateOffset(label = "Transition Offset Header 2") { + if (it == Step.Confirm) zeroOffset else positiveOffset + } + val alphaHeader1 by transition.animateFloat(label = "Transition Alpha Header 1") { + if (it == Step.Create) 1.0F else 0.0F + } + val alphaHeader2 by transition.animateFloat(label = "Transition Alpha Header 2") { + if (it == Step.Confirm) 1.0F else 0.0F + } + val scaleHeader1 by transition.animateFloat(label = "Transition Alpha Header 1") { + if (it == Step.Create) 1.0F else 0.5F + } + val scaleHeader2 by transition.animateFloat(label = "Transition Alpha Header 2") { + if (it == Step.Confirm) 1.0F else 0.5F + } + + Box( + modifier = modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + if (isPasscodeAlreadySet) { + Text( + modifier = Modifier + .offset(x = xTransitionHeader1.x.dp) + .alpha(alpha = alphaHeader1) + .scale(scale = scaleHeader1), + text = stringResource(Res.string.library_mifos_passcode_enter_your_passcode), + style = TextStyle(fontSize = 20.sp), + ) + } else { + if (activeStep == Step.Create) { + Text( + modifier = Modifier + .offset(x = xTransitionHeader1.x.dp) + .alpha(alpha = alphaHeader1) + .scale(scale = scaleHeader1), + text = stringResource(Res.string.library_mifos_passcode_create_passcode), + style = TextStyle(fontSize = 20.sp), + ) + } else if (activeStep == Step.Confirm) { + Text( + modifier = Modifier + .offset(x = xTransitionHeader2.x.dp) + .alpha(alpha = alphaHeader2) + .scale(scale = scaleHeader2), + text = stringResource(Res.string.library_mifos_passcode_confirm_passcode), + style = TextStyle(fontSize = 20.sp), + ) + } + } + } + } +} + +@Preview +@Composable +private fun PasscodeHeaderPreview() { + PasscodeHeader(activeStep = Step.Create, isPasscodeAlreadySet = true) +} diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeKeys.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeKeys.kt new file mode 100644 index 0000000..521b958 --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeKeys.kt @@ -0,0 +1,197 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.component + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Backspace +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.mifos.library.passcode.theme.PasscodeKeyButtonStyle +import org.mifos.library.passcode.theme.blueTint + +@Composable +internal fun PasscodeKeys( + enterKey: (String) -> Unit, + deleteKey: () -> Unit, + deleteAllKeys: () -> Unit, + modifier: Modifier = Modifier, +) { + val onEnterKeyClick = { keyTitle: String -> + enterKey(keyTitle) + } + Column( + modifier = modifier + .fillMaxWidth() + .padding(16.dp), + ) { + Row(modifier = Modifier.fillMaxWidth()) { + PasscodeKey( + modifier = Modifier.weight(weight = 1.0F), + keyTitle = "1", + onClick = onEnterKeyClick, + ) + PasscodeKey( + modifier = Modifier.weight(weight = 1.0F), + keyTitle = "2", + onClick = onEnterKeyClick, + ) + PasscodeKey( + modifier = Modifier.weight(weight = 1.0F), + keyTitle = "3", + onClick = onEnterKeyClick, + ) + } + Row(modifier = Modifier.fillMaxWidth()) { + PasscodeKey( + modifier = Modifier.weight(weight = 1.0F), + keyTitle = "4", + onClick = onEnterKeyClick, + ) + PasscodeKey( + modifier = Modifier.weight(weight = 1.0F), + keyTitle = "5", + onClick = onEnterKeyClick, + ) + PasscodeKey( + modifier = Modifier.weight(weight = 1.0F), + keyTitle = "6", + onClick = onEnterKeyClick, + ) + } + Row(modifier = Modifier.fillMaxWidth()) { + PasscodeKey( + modifier = Modifier.weight(weight = 1.0F), + keyTitle = "7", + onClick = onEnterKeyClick, + ) + PasscodeKey( + modifier = Modifier.weight(weight = 1.0F), + keyTitle = "8", + onClick = onEnterKeyClick, + ) + PasscodeKey( + modifier = Modifier.weight(weight = 1.0F), + keyTitle = "9", + onClick = onEnterKeyClick, + ) + } + Row(modifier = Modifier.fillMaxWidth()) { + PasscodeKey(modifier = Modifier.weight(weight = 1.0F)) + PasscodeKey( + modifier = Modifier.weight(weight = 1.0F), + keyTitle = "0", + onClick = onEnterKeyClick, + ) + PasscodeKey( + modifier = Modifier.weight(weight = 1.0F), + keyIcon = Icons.Default.Delete, + keyIconContentDescription = "Delete Passcode Key Button", + onClick = { + deleteKey() + }, + onLongClick = { + deleteAllKeys() + }, + ) + } + } +} + +@Composable +internal fun PasscodeKey( + modifier: Modifier = Modifier, + keyTitle: String = "", + keyIcon: ImageVector? = null, + keyIconContentDescription: String = "", + onClick: ((String) -> Unit)? = null, + onLongClick: (() -> Unit)? = null, +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.Center, + ) { + CombinedClickableIconButton( + onClick = { + onClick?.invoke(keyTitle) + }, + onLongClick = { + onLongClick?.invoke() + }, + modifier = Modifier + .padding(all = 4.dp), + ) { + if (keyIcon == null) { + Text( + text = keyTitle, + style = PasscodeKeyButtonStyle.copy(color = blueTint), + ) + } else { + Icon( + imageVector = Icons.AutoMirrored.Filled.Backspace, + contentDescription = keyIconContentDescription, + tint = blueTint, + ) + } + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +internal fun CombinedClickableIconButton( + onClick: () -> Unit, + onLongClick: () -> Unit, + modifier: Modifier = Modifier, + size: Dp = 48.dp, + enabled: Boolean = true, + content: @Composable () -> Unit, +) { + Column( + modifier = modifier + .size(size = size) + .combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + enabled = enabled, + role = Role.Button, + ), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + val contentAlpha = + if (enabled) LocalContentColor.current else LocalContentColor.current.copy(alpha = 0f) + CompositionLocalProvider(LocalContentColor provides contentAlpha, content = content) + } +} + +@Preview +@Composable +private fun PasscodeKeysPreview() { + PasscodeKeys({}, {}, {}) +} diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeMismatchedDialog.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeMismatchedDialog.kt new file mode 100644 index 0000000..6cfe56b --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeMismatchedDialog.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.component + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import mobile_wallet.libs.mifos_passcode.generated.resources.Res +import mobile_wallet.libs.mifos_passcode.generated.resources.library_mifos_passcode_do_not_match +import mobile_wallet.libs.mifos_passcode.generated.resources.library_mifos_passcode_try_again +import org.jetbrains.compose.resources.stringResource + +@Composable +internal fun PasscodeMismatchedDialog( + visible: Boolean, + onDismiss: () -> Unit, +) { + if (visible) { + AlertDialog( + shape = MaterialTheme.shapes.large, + containerColor = Color.White, + title = { + Text( + text = stringResource(Res.string.library_mifos_passcode_do_not_match), + color = Color.Black, + ) + }, + confirmButton = { + TextButton(onClick = onDismiss) { + Text( + text = stringResource(Res.string.library_mifos_passcode_try_again), + color = Color.Black, + ) + } + }, + onDismissRequest = onDismiss, + ) + } +} diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeStepIndicator.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeStepIndicator.kt new file mode 100644 index 0000000..43d582a --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeStepIndicator.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.component + +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import org.mifos.library.passcode.theme.blueTint +import org.mifos.library.passcode.utility.Constants.STEPS_COUNT +import org.mifos.library.passcode.utility.Step + +@Composable +internal fun PasscodeStepIndicator( + activeStep: Step, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy( + space = 6.dp, + alignment = Alignment.CenterHorizontally, + ), + ) { + repeat(STEPS_COUNT) { step -> + val isActiveStep = step <= activeStep.index + val stepColor = + animateColorAsState(if (isActiveStep) blueTint else Color.Gray, label = "") + + Box( + modifier = Modifier + .size( + width = 72.dp, + height = 4.dp, + ) + .background( + color = stepColor.value, + shape = MaterialTheme.shapes.medium, + ), + ) + } + } +} diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeToolbar.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeToolbar.kt new file mode 100644 index 0000000..eebeb1b --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/component/PasscodeToolbar.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.component + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import mobile_wallet.libs.mifos_passcode.generated.resources.Res +import mobile_wallet.libs.mifos_passcode.generated.resources.lib_mifos_passcode_cancel +import mobile_wallet.libs.mifos_passcode.generated.resources.library_mifos_passcode_exit +import mobile_wallet.libs.mifos_passcode.generated.resources.library_mifos_passcode_exit_message +import org.jetbrains.compose.resources.stringResource +import org.mifos.library.passcode.utility.Step + +@Composable +internal fun PasscodeToolbar( + activeStep: Step, + hasPasscode: Boolean, + modifier: Modifier = Modifier, +) { + var exitWarningDialogVisible by remember { mutableStateOf(false) } + + AnimatedVisibility(exitWarningDialogVisible) { + ExitWarningDialog( + visible = exitWarningDialogVisible, + onConfirm = {}, + onDismiss = { + exitWarningDialogVisible = false + }, + ) + } + + Row( + modifier = modifier + .fillMaxWidth() + .padding(top = 8.dp), + horizontalArrangement = Arrangement.Center, + ) { + if (!hasPasscode) { + PasscodeStepIndicator( + activeStep = activeStep, + ) + } + } +} + +@Composable +private fun ExitWarningDialog( + visible: Boolean, + onConfirm: () -> Unit, + onDismiss: () -> Unit, +) { + if (visible) { + AlertDialog( + shape = MaterialTheme.shapes.large, + containerColor = Color.White, + title = { + Text( + text = stringResource(Res.string.library_mifos_passcode_exit_message), + color = Color.Black, + ) + }, + confirmButton = { + TextButton(onClick = onConfirm) { + Text(text = stringResource(Res.string.library_mifos_passcode_exit)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(text = stringResource(Res.string.lib_mifos_passcode_cancel)) + } + }, + onDismissRequest = onDismiss, + ) + } +} diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/di/PasscodeModule.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/di/PasscodeModule.kt new file mode 100644 index 0000000..92ca27c --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/di/PasscodeModule.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.di + +import org.koin.core.module.dsl.viewModelOf +import org.koin.dsl.module +import org.mifos.library.passcode.viewmodels.PasscodeViewModel +import proto.org.mifos.library.passcode.di.PasscodePreferenceModule + +val PasscodeModule = module { + includes(PasscodePreferenceModule) + + viewModelOf(::PasscodeViewModel) +} diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/theme/Color.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/theme/Color.kt new file mode 100644 index 0000000..87e3798 --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/theme/Color.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.theme + +import androidx.compose.ui.graphics.Color + +internal val blueTint = Color(0xFF03A9F4) diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/theme/Font.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/theme/Font.kt new file mode 100644 index 0000000..33528be --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/theme/Font.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.theme + +import androidx.compose.material3.Typography +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp +import mobile_wallet.libs.mifos_passcode.generated.resources.Res +import mobile_wallet.libs.mifos_passcode.generated.resources.lib_mifos_passcode_lato_black +import mobile_wallet.libs.mifos_passcode.generated.resources.lib_mifos_passcode_lato_bold +import mobile_wallet.libs.mifos_passcode.generated.resources.lib_mifos_passcode_lato_regular +import org.jetbrains.compose.resources.Font + +@Composable +fun getFontFamily() = FontFamily( + Font(Res.font.lib_mifos_passcode_lato_regular, FontWeight.Normal, FontStyle.Normal), + Font(Res.font.lib_mifos_passcode_lato_bold, FontWeight.Bold, FontStyle.Normal), + Font(Res.font.lib_mifos_passcode_lato_black, FontWeight.Black, FontStyle.Normal), +) + +@Composable +fun getTypography(): Typography { + return Typography( + displayLarge = TextStyle( + fontFamily = getFontFamily(), + fontWeight = FontWeight.Black, + fontSize = 34.sp, + ), + ) +} diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/theme/Theme.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/theme/Theme.kt new file mode 100644 index 0000000..39b181f --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/theme/Theme.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Color.Companion.Blue + +private val DarkColorPalette = darkColorScheme( + primary = Color.Cyan, + onPrimary = Color.Cyan, + secondary = Color.Black.copy(alpha = 0.2f), + background = Color.Black, +) +private val LightColorPalette = lightColorScheme( + primary = Blue, + onPrimary = Blue, + secondary = Color.Blue.copy(alpha = 0.4f), + background = Color.White, +) + +@Composable +internal fun MifosPasscodeTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit, +) { + val colors = if (darkTheme) { + DarkColorPalette + } else { + LightColorPalette + } + + MaterialTheme( + colorScheme = colors, + typography = getTypography(), + content = content, + ) +} diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/theme/Type.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/theme/Type.kt new file mode 100644 index 0000000..1325f60 --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/theme/Type.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.theme + +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +internal val PasscodeKeyButtonStyle = TextStyle( + fontWeight = FontWeight.Bold, + fontSize = 24.sp, +) + +internal val skipButtonStyle = TextStyle( + color = blueTint, + fontSize = 20.sp, +) + +internal val forgotButtonStyle = TextStyle( + color = blueTint, + fontSize = 14.sp, +) diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/Constants.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/Constants.kt new file mode 100644 index 0000000..4cb95a4 --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/Constants.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.utility + +internal object Constants { + const val STEPS_COUNT = 2 + const val PASSCODE_LENGTH = 4 + const val VIBRATE_FEEDBACK_DURATION = 300L +} diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/ShakeAnimation.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/ShakeAnimation.kt new file mode 100644 index 0000000..88e75cf --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/ShakeAnimation.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.utility + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.keyframes +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +internal object ShakeAnimation { + + fun CoroutineScope.performShakeAnimation(xShake: Animatable) { + launch { + xShake.animateTo( + targetValue = 0f, + animationSpec = keyframes { + durationMillis = 280 + 0f at 0 using LinearOutSlowInEasing + 20f at 80 using LinearOutSlowInEasing + 20f at 120 using LinearOutSlowInEasing + 10f at 160 using LinearOutSlowInEasing + 10f at 200 using LinearOutSlowInEasing + 5f at 240 using LinearOutSlowInEasing + 0f at 280 + }, + ) + } + } +} diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/Step.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/Step.kt new file mode 100644 index 0000000..6c66457 --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/utility/Step.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.utility + +enum class Step(var index: Int) { + Create(0), + Confirm(1), +} diff --git a/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt new file mode 100644 index 0000000..b6c5cdd --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt @@ -0,0 +1,199 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.viewmodels + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.mifos.library.passcode.utility.Step +import org.mifospay.core.common.Parcelable +import org.mifospay.core.common.Parcelize +import org.mifospay.core.ui.utils.BaseViewModel +import proto.org.mifos.library.passcode.data.PasscodeManager + +private const val KEY_STATE = "passcode_state" +private const val PASSCODE_LENGTH = 4 + +class PasscodeViewModel( + private val passcodeRepository: PasscodeManager, + savedStateHandle: SavedStateHandle, +) : BaseViewModel( + initialState = savedStateHandle[KEY_STATE] ?: PasscodeState(), +) { + private var createPasscode: StringBuilder = StringBuilder() + private var confirmPasscode: StringBuilder = StringBuilder() + + init { + observePasscodeRepository() + } + + private fun observePasscodeRepository() { + viewModelScope.launch { + passcodeRepository.hasPasscode.collect { hasPasscode -> + mutableStateFlow.update { + it.copy( + hasPasscode = hasPasscode, + isPasscodeAlreadySet = hasPasscode, + ) + } + } + } + } + + override fun handleAction(action: PasscodeAction) { + when (action) { + is PasscodeAction.EnterKey -> enterKey(action.key) + is PasscodeAction.DeleteKey -> deleteKey() + is PasscodeAction.DeleteAllKeys -> deleteAllKeys() + is PasscodeAction.TogglePasscodeVisibility -> togglePasscodeVisibility() + is PasscodeAction.Restart -> restart() + is PasscodeAction.Internal.ProcessCompletedPasscode -> processCompletedPasscode() + } + } + + private fun enterKey(key: String) { + if (state.filledDots >= PASSCODE_LENGTH) return + + val currentPasscode = + if (state.activeStep == Step.Create) createPasscode else confirmPasscode + currentPasscode.append(key) + + mutableStateFlow.update { + it.copy( + currentPasscodeInput = currentPasscode.toString(), + filledDots = currentPasscode.length, + ) + } + + if (state.filledDots == PASSCODE_LENGTH) { + viewModelScope.launch { + sendAction(PasscodeAction.Internal.ProcessCompletedPasscode) + } + } + } + + private fun deleteKey() { + val currentPasscode = + if (state.activeStep == Step.Create) createPasscode else confirmPasscode + if (currentPasscode.isNotEmpty()) { + currentPasscode.deleteAt(currentPasscode.length - 1) + mutableStateFlow.update { + it.copy( + currentPasscodeInput = currentPasscode.toString(), + filledDots = currentPasscode.length, + ) + } + } + } + + private fun deleteAllKeys() { + if (state.activeStep == Step.Create) { + createPasscode.clear() + } else { + confirmPasscode.clear() + } + mutableStateFlow.update { + it.copy( + currentPasscodeInput = "", + filledDots = 0, + ) + } + } + + private fun togglePasscodeVisibility() { + mutableStateFlow.update { it.copy(passcodeVisible = !it.passcodeVisible) } + } + + private fun restart() { + resetState() + } + + private fun processCompletedPasscode() { + viewModelScope.launch { + when { + state.isPasscodeAlreadySet -> validateExistingPasscode() + state.activeStep == Step.Create -> moveToConfirmStep() + else -> validateNewPasscode() + } + } + } + + private suspend fun validateExistingPasscode() { + val savedPasscode = passcodeRepository.getPasscode.first() + if (savedPasscode == createPasscode.toString()) { + sendEvent(PasscodeEvent.PasscodeConfirmed(createPasscode.toString())) + createPasscode.clear() + } else { + sendEvent(PasscodeEvent.PasscodeRejected) + } + mutableStateFlow.update { it.copy(currentPasscodeInput = "") } + } + + private fun moveToConfirmStep() { + mutableStateFlow.update { + it.copy( + activeStep = Step.Confirm, + filledDots = 0, + currentPasscodeInput = "", + ) + } + } + + private suspend fun validateNewPasscode() { + if (createPasscode.toString() == confirmPasscode.toString()) { + passcodeRepository.savePasscode(confirmPasscode.toString()) + sendEvent(PasscodeEvent.PasscodeConfirmed(confirmPasscode.toString())) + resetState() + } else { + sendEvent(PasscodeEvent.PasscodeRejected) + resetState() + } + } + + private fun resetState() { + mutableStateFlow.update { + PasscodeState( + hasPasscode = it.hasPasscode, + isPasscodeAlreadySet = it.isPasscodeAlreadySet, + ) + } + createPasscode.clear() + confirmPasscode.clear() + } +} + +@Parcelize +data class PasscodeState( + val hasPasscode: Boolean = false, + val activeStep: Step = Step.Create, + val filledDots: Int = 0, + val passcodeVisible: Boolean = false, + val currentPasscodeInput: String = "", + val isPasscodeAlreadySet: Boolean = false, +) : Parcelable + +sealed class PasscodeEvent { + data class PasscodeConfirmed(val passcode: String) : PasscodeEvent() + data object PasscodeRejected : PasscodeEvent() +} + +sealed class PasscodeAction { + data class EnterKey(val key: String) : PasscodeAction() + data object DeleteKey : PasscodeAction() + data object DeleteAllKeys : PasscodeAction() + data object TogglePasscodeVisibility : PasscodeAction() + data object Restart : PasscodeAction() + + sealed class Internal : PasscodeAction() { + data object ProcessCompletedPasscode : Internal() + } +} diff --git a/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodeManager.kt b/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodeManager.kt new file mode 100644 index 0000000..2d6a3dc --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodeManager.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package proto.org.mifos.library.passcode.data + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn +import proto.org.mifos.library.passcode.model.PasscodePreferencesProto + +class PasscodeManager( + private val source: PasscodePreferencesDataSource, + dispatcher: CoroutineDispatcher, +) { + private val coroutineScope = CoroutineScope(dispatcher) + + val getPasscode = source.passcode.stateIn( + scope = coroutineScope, + started = SharingStarted.Eagerly, + initialValue = "", + ) + + val hasPasscode = source.hasPasscode.stateIn( + scope = coroutineScope, + started = SharingStarted.Eagerly, + initialValue = false, + ) + + suspend fun savePasscode(passcode: String) { + source.updatePasscodeSettings( + PasscodePreferencesProto( + passcode = passcode, + hasPasscode = passcode.isNotEmpty(), + ), + ) + } + + suspend fun clearPasscode() { + source.clearInfo() + } +} diff --git a/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodePreferencesDataSource.kt b/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodePreferencesDataSource.kt new file mode 100644 index 0000000..9bf273f --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/data/PasscodePreferencesDataSource.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +@file:OptIn(ExperimentalSerializationApi::class, ExperimentalSettingsApi::class) + +package proto.org.mifos.library.passcode.data + +import com.russhwolf.settings.ExperimentalSettingsApi +import com.russhwolf.settings.Settings +import com.russhwolf.settings.serialization.decodeValue +import com.russhwolf.settings.serialization.decodeValueOrNull +import com.russhwolf.settings.serialization.encodeValue +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import kotlinx.serialization.ExperimentalSerializationApi +import proto.org.mifos.library.passcode.model.PasscodePreferencesProto + +private const val PASSCODE_INFO_KEY = "passcodeInfo" + +class PasscodePreferencesDataSource( + private val settings: Settings, + private val dispatcher: CoroutineDispatcher, +) { + private val passcodeSettings = MutableStateFlow( + settings.decodeValue( + key = PASSCODE_INFO_KEY, + serializer = PasscodePreferencesProto.serializer(), + defaultValue = settings.decodeValueOrNull( + key = PASSCODE_INFO_KEY, + serializer = PasscodePreferencesProto.serializer(), + ) ?: PasscodePreferencesProto.DEFAULT, + ), + ) + + val passcode = passcodeSettings.map { it.passcode } + val hasPasscode = passcodeSettings.map { it.hasPasscode } + + suspend fun updatePasscodeSettings(passcodePreferences: PasscodePreferencesProto) { + withContext(dispatcher) { + settings.putPasscodePreference(passcodePreferences) + passcodeSettings.value = passcodePreferences + } + } + + suspend fun clearInfo() { + withContext(dispatcher) { + settings.remove(PASSCODE_INFO_KEY) + } + } +} + +private fun Settings.putPasscodePreference(preference: PasscodePreferencesProto) { + encodeValue( + key = PASSCODE_INFO_KEY, + serializer = PasscodePreferencesProto.serializer(), + value = preference, + ) +} diff --git a/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/di/PreferenceModule.kt b/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/di/PreferenceModule.kt new file mode 100644 index 0000000..575121f --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/di/PreferenceModule.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package proto.org.mifos.library.passcode.di + +import com.russhwolf.settings.Settings +import org.koin.core.qualifier.named +import org.koin.dsl.module +import org.mifospay.core.common.MifosDispatchers +import proto.org.mifos.library.passcode.data.PasscodeManager +import proto.org.mifos.library.passcode.data.PasscodePreferencesDataSource + +val PasscodePreferenceModule = module { + factory { Settings() } + factory { PasscodePreferencesDataSource(get(), get(named(MifosDispatchers.IO.name))) } + factory { PasscodeManager(get(), get(named(MifosDispatchers.Unconfined.name))) } +} diff --git a/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/model/PasscodePreferencesProto.kt b/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/model/PasscodePreferencesProto.kt new file mode 100644 index 0000000..0f7b38b --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/model/PasscodePreferencesProto.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md + */ +package proto.org.mifos.library.passcode.model + +import kotlinx.serialization.Serializable + +@Serializable +data class PasscodePreferencesProto( + val passcode: String, + val hasPasscode: Boolean, +) { + companion object { + val DEFAULT = PasscodePreferencesProto(passcode = "", hasPasscode = false) + } +} diff --git a/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/passcode_preferences.proto b/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/passcode_preferences.proto new file mode 100644 index 0000000..7ff555c --- /dev/null +++ b/mifos-passcode/src/commonMain/kotlin/proto/org/mifos/library/passcode/passcode_preferences.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; + +option java_package = "org.mifos.library.passcode.proto"; +option java_multiple_files = true; + +message PasscodePreferences { + string passcode = 1; +} \ No newline at end of file diff --git a/mifos-passcode/src/main/AndroidManifest.xml b/mifos-passcode/src/main/AndroidManifest.xml deleted file mode 100644 index 7f0c06c..0000000 --- a/mifos-passcode/src/main/AndroidManifest.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/mifos-passcode/src/main/java/com/mifos/mobile/passcode/BasePassCodeActivity.kt b/mifos-passcode/src/main/java/com/mifos/mobile/passcode/BasePassCodeActivity.kt deleted file mode 100644 index f42d852..0000000 --- a/mifos-passcode/src/main/java/com/mifos/mobile/passcode/BasePassCodeActivity.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.mifos.mobile.passcode - -import androidx.activity.ComponentActivity -import androidx.appcompat.app.AppCompatActivity -import com.mifos.mobile.passcode.utils.ForegroundChecker -import com.mifos.mobile.passcode.utils.ForegroundChecker.Companion.get - -/** - * Created by dilpreet on 19/01/18. - */ -abstract class BasePassCodeActivity : ComponentActivity(), ForegroundChecker.Listener { - override fun onResume() { - super.onResume() - get()!!.addListener(this) - get()!!.onActivityResumed() - } - - override fun onPause() { - super.onPause() - get()!!.onActivityPaused() - } - - override fun onBecameForeground() { - MifosPassCodeActivity.startMifosPassCodeActivity( - this, passCodeClass, - false - ) - } - - abstract val passCodeClass: Class<*>? -} \ No newline at end of file diff --git a/mifos-passcode/src/main/java/com/mifos/mobile/passcode/FpAuthCallback.kt b/mifos-passcode/src/main/java/com/mifos/mobile/passcode/FpAuthCallback.kt deleted file mode 100644 index 9b423d6..0000000 --- a/mifos-passcode/src/main/java/com/mifos/mobile/passcode/FpAuthCallback.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.mifos.mobile.passcode - -interface FpAuthCallback { - fun onFpAuthSuccess() - fun onCancel() -} \ No newline at end of file diff --git a/mifos-passcode/src/main/java/com/mifos/mobile/passcode/FpAuthDialog.kt b/mifos-passcode/src/main/java/com/mifos/mobile/passcode/FpAuthDialog.kt deleted file mode 100644 index 76916c0..0000000 --- a/mifos-passcode/src/main/java/com/mifos/mobile/passcode/FpAuthDialog.kt +++ /dev/null @@ -1,108 +0,0 @@ -package com.mifos.mobile.passcode - -import android.app.AlertDialog -import android.content.Context -import android.util.Log -import android.view.LayoutInflater -import android.widget.TextView -import android.widget.Toast -import androidx.appcompat.widget.AppCompatButton - -class FpAuthDialog(private val context: Context) { - private var dialogTitle = context.getString(R.string.fp_dialog_title) - private var dialogMessage = context.getString(R.string.fp_dialog_message) - private var dialogCancelText = context.getString(R.string.fp_dialog_cancel) - - private var layoutInflater = LayoutInflater.from(context) - private var dialogView = layoutInflater.inflate(R.layout.dialog_fingerprint_auth, null) - private var tvDialogTitle = dialogView.findViewById(R.id.fingerprint_dialog_title) - private var tvDialogMessage = dialogView.findViewById(R.id.fingerprint_dialog_message) - private var tvDialogStatus = dialogView.findViewById(R.id.fingerprint_dialog_status) - private var btnCancel = dialogView.findViewById(R.id.fingerprint_dialog_cancel) - - private var dialogBuilder = AlertDialog.Builder(context) - private var dialog: AlertDialog? = null - - private var fpAuthCallback: FpAuthCallback = object : FpAuthCallback { - override fun onFpAuthSuccess() { - Toast.makeText(context, context.getString(R.string.authentication_successful), Toast.LENGTH_SHORT).show() - } - - override fun onCancel() { - Toast.makeText(context, context.getString(R.string.authentication_cancelled), Toast.LENGTH_SHORT).show() - } - - } - private var fpAuthHelper = FpAuthHelper(context, fpAuthCallback, this) - - fun setTitle(title: String): FpAuthDialog { - dialogTitle = title - return this - } - - fun setTitle(resTitle: Int): FpAuthDialog { - dialogTitle = context.resources.getString(resTitle) - return this - } - - fun setMessage(message: String): FpAuthDialog { - dialogMessage = message - return this - } - - fun setMessage(resMessage: Int): FpAuthDialog { - dialogMessage = context.resources.getString(resMessage) - return this - } - - fun setCancelText(cancelText: String): FpAuthDialog { - dialogCancelText = cancelText - return this - } - - fun setCancelText(resCancelText: Int): FpAuthDialog { - dialogCancelText = context.resources.getString(resCancelText) - return this - } - - fun setCallback(fpAuthCallback: FpAuthCallback): FpAuthDialog { - this.fpAuthCallback = fpAuthCallback - fpAuthHelper = FpAuthHelper(context, fpAuthCallback, this) - return this - } - - internal fun setStatusText(statusText: String) { - tvDialogStatus.text = statusText - } - - internal fun setStatusIcon(resIcon: Int) { - tvDialogStatus.setCompoundDrawablesWithIntrinsicBounds(resIcon, 0, 0, 0) - } - - fun dismiss() { - fpAuthHelper.stopFpAuth() - dialog!!.dismiss() - } - - fun show() { - if (!FpAuthSupport.checkAvailabiltyAndIfFingerprintRegistered(context)) { - Log.e("FingerprintAuth", "Device not Suitable for Fingerprint Authentication") - return - } - tvDialogTitle.text = dialogTitle - tvDialogMessage.text = dialogMessage - btnCancel.text = dialogCancelText - - dialog = dialogBuilder.setView(dialogView).create() - dialog!!.setCancelable(false) - dialog!!.show() - - fpAuthHelper.startFpAuth() - - btnCancel.setOnClickListener { - fpAuthHelper.stopFpAuth() - dialog!!.dismiss() - fpAuthCallback.onCancel() - } - } -} \ No newline at end of file diff --git a/mifos-passcode/src/main/java/com/mifos/mobile/passcode/FpAuthHelper.kt b/mifos-passcode/src/main/java/com/mifos/mobile/passcode/FpAuthHelper.kt deleted file mode 100644 index 4f61947..0000000 --- a/mifos-passcode/src/main/java/com/mifos/mobile/passcode/FpAuthHelper.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.mifos.mobile.passcode - -import android.content.Context -import android.os.Handler -import android.widget.Toast -import androidx.core.hardware.fingerprint.FingerprintManagerCompat -import androidx.core.os.CancellationSignal - -class FpAuthHelper(private val context: Context, private val fpAuthCallback: FpAuthCallback, - private val fpAuthDialog: FpAuthDialog) { - private var cancellationSignal: CancellationSignal? = null - private var handler: Handler = Handler() - - companion object { - const val AUTH_FAILED_DELAY: Long = 1000 - const val AUTH_SUCCESS_DELAY: Long = 500 - } - - private val startScanning = Runnable { - fpAuthDialog.run { - setStatusText(context.getString(R.string.touch_the_sensor)) - setStatusIcon(R.drawable.ic_fingerprint_blue_48dp) - } - } - - fun startFpAuth() { - if (!FpAuthSupport.checkAvailabiltyAndIfFingerprintRegistered(context)) { - return - } - if (cancellationSignal == null) { - cancellationSignal = CancellationSignal() - } - -// val fingerprintManager = FingerprintManagerCompat.from(context) -// -// fingerprintManager.authenticate( -// null, 0, cancellationSignal, -// object : FingerprintManagerCompat.AuthenticationCallback() { -// -// override fun onAuthenticationHelp(helpMsgId: Int, helpString: CharSequence?) { -// super.onAuthenticationHelp(helpMsgId, helpString) -// Toast.makeText(context, helpString, Toast.LENGTH_SHORT).show() -// } -// -// override fun onAuthenticationFailed() { -// super.onAuthenticationFailed() -// fpAuthDialog.run { -// setStatusIcon(R.drawable.ic_cancel_red_48dp) -// setStatusText(context.getString(R.string.finger_print_not_recognized)) -// } -// handler.postDelayed(startScanning, AUTH_FAILED_DELAY) -// } -// -// override fun onAuthenticationSucceeded(result: FingerprintManagerCompat.AuthenticationResult?) { -// super.onAuthenticationSucceeded(result) -// fpAuthDialog.run { -// setStatusIcon(R.drawable.ic_check_circle_green_48dp) -// setStatusText(context.getString(R.string.authentication_successful)) -// } -// handler.postDelayed({ -// fpAuthDialog.dismiss() -// fpAuthCallback.onFpAuthSuccess() -// }, AUTH_SUCCESS_DELAY) -// } -// }, null -// ) - } - - fun stopFpAuth() { - cancellationSignal?.run { - cancel() - cancellationSignal = null - } - } -} \ No newline at end of file diff --git a/mifos-passcode/src/main/java/com/mifos/mobile/passcode/FpAuthSupport.kt b/mifos-passcode/src/main/java/com/mifos/mobile/passcode/FpAuthSupport.kt deleted file mode 100644 index 5cf2118..0000000 --- a/mifos-passcode/src/main/java/com/mifos/mobile/passcode/FpAuthSupport.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.mifos.mobile.passcode - -import android.content.Context -import android.os.Build -import androidx.core.hardware.fingerprint.FingerprintManagerCompat - -object FpAuthSupport { - - @JvmStatic - fun checkAvailability(context: Context): Boolean { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && - FingerprintManagerCompat.from(context).isHardwareDetected - } - - @JvmStatic - fun isFingerprintRegistered(context: Context): Boolean { - return FingerprintManagerCompat.from(context).hasEnrolledFingerprints() - } - - @JvmStatic - fun checkAvailabiltyAndIfFingerprintRegistered(context: Context): Boolean { - val fingerprintManagerCompat = FingerprintManagerCompat.from(context) - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && - fingerprintManagerCompat.isHardwareDetected && - fingerprintManagerCompat.hasEnrolledFingerprints() - } -} \ No newline at end of file diff --git a/mifos-passcode/src/main/java/com/mifos/mobile/passcode/MifosPassCodeActivity.kt b/mifos-passcode/src/main/java/com/mifos/mobile/passcode/MifosPassCodeActivity.kt deleted file mode 100644 index 13d0eb8..0000000 --- a/mifos-passcode/src/main/java/com/mifos/mobile/passcode/MifosPassCodeActivity.kt +++ /dev/null @@ -1,357 +0,0 @@ -package com.mifos.mobile.passcode - -import android.content.Context -import android.content.DialogInterface -import android.content.Intent -import android.os.Bundle -import android.view.View -import android.view.animation.Animation -import android.view.animation.AnimationUtils -import android.widget.Toast -import androidx.appcompat.app.AlertDialog -import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.ContextCompat -import com.mifos.mobile.passcode.FpAuthSupport.checkAvailabiltyAndIfFingerprintRegistered -import com.mifos.mobile.passcode.databinding.ActivityPassCodeBinding -import com.mifos.mobile.passcode.utils.EncryptionUtil -import com.mifos.mobile.passcode.utils.EncryptionUtil.TYPE -import com.mifos.mobile.passcode.utils.PassCodeConstants -import com.mifos.mobile.passcode.utils.PassCodeNetworkChecker.isConnected -import com.mifos.mobile.passcode.utils.PasscodePreferencesHelper - -abstract class MifosPassCodeActivity : AppCompatActivity(), PassCodeListener { - - var shakeAnimation: Animation? = null - private var counter = 0 - private var isInitialScreen = false - private var isPassCodeVerified = false - private var strPassCodeEntered: String? = null - private var passcodePreferencesHelper: PasscodePreferencesHelper? = null - private var resetPasscode = false - abstract val logo: Int - abstract val fpDialogTitle: String? - abstract fun startNextActivity() - abstract fun startLoginActivity() - abstract fun showToaster(view: View?, msg: Int) - - private lateinit var binding: ActivityPassCodeBinding - - @get:TYPE - abstract val encryptionType: Int - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = ActivityPassCodeBinding.inflate(layoutInflater) - setContentView(binding.root) - shakeAnimation = AnimationUtils.loadAnimation(this, R.anim.shake) - binding.ivLogo.setImageResource(logo) - passcodePreferencesHelper = PasscodePreferencesHelper(this) - isInitialScreen = intent.getBooleanExtra( - PassCodeConstants.PASSCODE_INITIAL_LOGIN, - false - ) - - //Show Prompt Dialog if device Support Fingerprint Authentication and has fingerprint - // registered - if (checkAvailabiltyAndIfFingerprintRegistered(this) - && passcodePreferencesHelper!!.fingerprintEnableDialogState - ) { - val builder = AlertDialog.Builder( - this, - R.style.MaterialAlertDialogStyle - ) - builder.setTitle(R.string.fingerprint) - builder.setIcon(R.drawable.ic_fingerprint_blue_48dp) - builder.setMessage(R.string.FingerprintEnableMessage) - builder.setPositiveButton("Yes", object : DialogInterface.OnClickListener { - override fun onClick(dialogInterface: DialogInterface, i: Int) { - passcodePreferencesHelper!!.fingerprintEnableDialogState = false - passcodePreferencesHelper!!.authType = "fpauth" - fpDialogTitle?.let { - FpAuthDialog(this@MifosPassCodeActivity) - .setTitle(it) - .setCallback(object : FpAuthCallback { - override fun onFpAuthSuccess() { - startHomeActivity() - } - - override fun onCancel() { - cancelFingerprintAuth() - } - }).show() - } - } - }) - builder.setNegativeButton("No") { dialogInterface, i -> - passcodePreferencesHelper!!.fingerprintEnableDialogState = false - passcodePreferencesHelper!!.authType = "passcode" - } - val alertDialog = builder.create() - alertDialog.setCancelable(false) - alertDialog.show() - } - if (passcodePreferencesHelper!!.authType.equals("passcode", ignoreCase = true)) { - resetPasscode = intent.getBooleanExtra(PassCodeConstants.RESET_PASSCODE, false) - isPassCodeVerified = false - strPassCodeEntered = "" - if (!passcodePreferencesHelper!!.passCode!!.isEmpty()) { - binding.btnSkip.visibility = View.GONE - binding.btnSave.visibility = View.GONE - binding.tvPasscode.visibility = View.GONE - binding.btnForgotPasscode.visibility = View.VISIBLE - //enabling passCodeListener only when user has already setup PassCode - binding.pvPasscode.setPassCodeListener(this) - } - if (resetPasscode) { - binding.btnSkip.visibility = View.GONE - binding.btnSave.visibility = View.GONE - binding.tvPasscode.visibility = View.VISIBLE - binding.tvPasscode.text = getString(R.string.confirm_passcode) - } - } - } - - private fun encryptPassCode(passCode: String): String? { - @TYPE val type = encryptionType - var encryptedPassCode: String? = null - when (type) { - EncryptionUtil.MOBILE_BANKING -> encryptedPassCode = - EncryptionUtil.getMobileBankingHash(passCode) - - EncryptionUtil.ANDROID_CLIENT -> encryptedPassCode = - EncryptionUtil.getAndroidClientHash(passCode) - - EncryptionUtil.FINERACT_CN -> encryptedPassCode = - EncryptionUtil.getFineractCNHash(passCode) - - EncryptionUtil.DEFAULT -> encryptedPassCode = EncryptionUtil.getDefaultHash(passCode) - } - return encryptedPassCode - } - - fun clearTokenPreferences() { - passcodePreferencesHelper!!.clear() - } - - fun skip(v: View?) { - startHomeActivity() - } - - /** - * Saves the passcode by encrypting it which we got from [MifosPassCodeView] - * - * @param view Passcode View - */ - fun savePassCode(view: View?) { - if (isPassCodeLengthCorrect) { - if (isPassCodeVerified) { - if (strPassCodeEntered!!.compareTo(binding.pvPasscode.passcode) == 0) { - passcodePreferencesHelper!!.savePassCode(encryptPassCode(binding.pvPasscode.passcode)) - startHomeActivity() - } else { - showToaster(binding.clRootview, R.string.passcode_does_not_match) - binding.pvPasscode.clearPasscodeField() - } - } else { - binding.btnSkip.visibility = View.INVISIBLE - binding.btnSave.text = getString(R.string.save) - binding.tvPasscode.text = getString(R.string.reenter_passcode) - strPassCodeEntered = binding.pvPasscode.passcode - binding.pvPasscode.clearPasscodeField() - isPassCodeVerified = true - } - } - } - - /** - * It is a callback for [MifosPassCodeView], provides with the passcode entered by user - * - * @param passcode Passcode that is entered by user. - */ - override fun passCodeEntered(passcode: String?) { - if (!isInternetAvailable) { - binding.pvPasscode.clearPasscodeField() - return - } - if (counter == 3) { - Toast.makeText( - applicationContext, R.string.incorrect_passcode_more_than_three, - Toast.LENGTH_SHORT - ).show() - clearTokenPreferences() - startLoginActivity() - return - } - if (isPassCodeLengthCorrect) { - val passwordEntered = encryptPassCode(binding.pvPasscode.passcode) - if (passcodePreferencesHelper!!.passCode == passwordEntered) { - if (resetPasscode) { - resetPasscode() - return - } - startHomeActivity() - } else { - binding.pvPasscode.startAnimation(shakeAnimation) - counter++ - binding.pvPasscode.clearPasscodeField() - showToaster(binding.clRootview, R.string.incorrect_passcode) - } - } - } - - fun forgotPassCode(v: View?) { - clearTokenPreferences() - startLoginActivity() - } - - fun cancelFingerprintAuth() { - clearTokenPreferences() - startLoginActivity() - finish() - } - - private val isInternetAvailable: Boolean - /** - * Checks for internet availability - * - * @return Returns true if connected else returns false - */ - get() = if (isConnected(this)) { - true - } else { - showToaster(binding.clRootview, R.string.no_internet_connection) - false - } - - fun clickedOne(v: View?) { - binding.pvPasscode.enterCode(getString(R.string.one)) - } - - fun clickedTwo(v: View?) { - binding.pvPasscode.enterCode(getString(R.string.two)) - } - - fun clickedThree(v: View?) { - binding.pvPasscode.enterCode(getString(R.string.three)) - } - - fun clickedFour(v: View?) { - binding.pvPasscode.enterCode(getString(R.string.four)) - } - - fun clickedFive(v: View?) { - binding.pvPasscode.enterCode(getString(R.string.five)) - } - - fun clickedSix(v: View?) { - binding.pvPasscode.enterCode(getString(R.string.six)) - } - - fun clickedSeven(v: View?) { - binding.pvPasscode.enterCode(getString(R.string.seven)) - } - - fun clickedEight(v: View?) { - binding.pvPasscode.enterCode(getString(R.string.eight)) - } - - fun clickedNine(v: View?) { - binding.pvPasscode.enterCode(getString(R.string.nine)) - } - - fun clickedZero(v: View?) { - binding.pvPasscode.enterCode(getString(R.string.zero)) - } - - fun clickedBackSpace(v: View?) { - binding.pvPasscode.backSpace() - } - - /** - * @param view PasscodeView that changes to text if it was hidden and vice a versa - */ - fun visibilityChange(view: View?) { - binding.pvPasscode.revertPassCodeVisibility() - if (!binding.pvPasscode.passcodeVisible()) { - binding.ivVisibility.setColorFilter( - ContextCompat.getColor( - this@MifosPassCodeActivity, - R.color.light_grey - ) - ) - } else { - binding.ivVisibility.setColorFilter( - ContextCompat.getColor( - this@MifosPassCodeActivity, - R.color.gray_dark - ) - ) - } - } - - private val isPassCodeLengthCorrect: Boolean - /** - * Checks whether passcode entered is of correct length - * - * @return Returns true if passcode lenght is 4 else shows message - */ - private get() { - if (binding.pvPasscode.passcode.length == 4) { - return true - } - showToaster(binding.clRootview, R.string.error_passcode) - return false - } - - private fun startHomeActivity() { - if (isInitialScreen) { - startNextActivity() - } - finish() - } - - override fun onBackPressed() { - if (isInitialScreen) { - super.onBackPressed() - } - } - - private fun resetPasscode() { - resetPasscode = false - binding.btnSkip.visibility = View.VISIBLE - binding.btnSave.visibility = View.VISIBLE - binding.tvPasscode.setText(R.string.passcode_setup) - counter = 0 - binding.pvPasscode.clearPasscodeField() - binding.pvPasscode.setPassCodeListener(null) - passcodePreferencesHelper!!.clear() - } - - override fun onResume() { - super.onResume() - if (passcodePreferencesHelper!!.authType.equals("fpauth", ignoreCase = true)) { - FpAuthDialog(this@MifosPassCodeActivity) - .setTitle(fpDialogTitle!!) - .setCallback(object : FpAuthCallback { - override fun onFpAuthSuccess() { - startHomeActivity() - } - - override fun onCancel() { - cancelFingerprintAuth() - } - }).show() - } - } - - companion object { - @JvmOverloads - fun startMifosPassCodeActivity( - context: Context, clazz: Class<*>?, - isInitialLogin: Boolean = true - ) { - val intent = Intent(context, clazz) - intent.putExtra(PassCodeConstants.PASSCODE_INITIAL_LOGIN, isInitialLogin) - context.startActivity(intent) - } - } -} \ No newline at end of file diff --git a/mifos-passcode/src/main/java/com/mifos/mobile/passcode/MifosPassCodeView.kt b/mifos-passcode/src/main/java/com/mifos/mobile/passcode/MifosPassCodeView.kt deleted file mode 100644 index 279f108..0000000 --- a/mifos-passcode/src/main/java/com/mifos/mobile/passcode/MifosPassCodeView.kt +++ /dev/null @@ -1,133 +0,0 @@ -package com.mifos.mobile.passcode - -import android.content.Context -import android.graphics.Canvas -import android.graphics.Color -import android.graphics.Paint -import android.util.AttributeSet -import android.view.View - -/** - * Created by dilpreet on 15/7/17. - */ -class MifosPassCodeView : View { - private var emptyCirclePaint: Paint? = null - private var fillCirclePaint: Paint? = null - private val PASSWORD_LENGTH = 4 - private var passwordList: MutableList? = null - private var isPasscodeVisible = false - private var passCodeListener: PassCodeListener? = null - - constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { - init(attrs) - } - - constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super( - context, - attrs, - defStyleAttr - ) { - init(attrs) - } - - fun setPassCodeListener(passCodeListener: PassCodeListener?) { - this.passCodeListener = passCodeListener - } - - private fun init(attrs: AttributeSet?) { - val attributes = context.obtainStyledAttributes(attrs, R.styleable.MifosPassCodeView) - val defaultTextSize = (12 * context.resources.displayMetrics.density).toInt() - val color = attributes.getColor(R.styleable.MifosPassCodeView_color, Color.WHITE) - emptyCirclePaint = Paint() - emptyCirclePaint!!.color = color - emptyCirclePaint!!.isAntiAlias = true - emptyCirclePaint!!.style = Paint.Style.STROKE - emptyCirclePaint!!.strokeWidth = 1f - fillCirclePaint = Paint() - fillCirclePaint!!.color = color - fillCirclePaint!!.isAntiAlias = true - fillCirclePaint!!.textSize = - attributes.getDimensionPixelSize( - R.styleable.MifosPassCodeView_text_size, - defaultTextSize - ) - .toFloat() - fillCirclePaint!!.style = Paint.Style.FILL - attributes.recycle() - passwordList = ArrayList() - isPasscodeVisible = false - } - - override fun onDraw(canvas: Canvas) { - super.onDraw(canvas) - val stackSize = passwordList!!.size - var xPosition = width / (PASSWORD_LENGTH * 2) - for (i in 1..PASSWORD_LENGTH) { - if (stackSize >= i) { - if (!isPasscodeVisible) { - canvas.drawCircle( - xPosition.toFloat(), - (height / 2).toFloat(), - 8f, - fillCirclePaint!! - ) - } else { - canvas.drawText( - passwordList!![i - 1], xPosition.toFloat(), (height / 2 + - height / 8).toFloat(), fillCirclePaint!! - ) - } - } else { - canvas.drawCircle( - xPosition.toFloat(), - (height / 2).toFloat(), - 8f, - emptyCirclePaint!! - ) - } - xPosition += width / PASSWORD_LENGTH - } - } - - fun enterCode(character: String) { - if (passwordList!!.size < PASSWORD_LENGTH) { - passwordList!!.add(character) - invalidate() - } - if (passwordList!!.size == PASSWORD_LENGTH && passCodeListener != null) { - passCodeListener!!.passCodeEntered(passcode) - } - } - - val passcode: String - get() { - val builder = StringBuilder() - for (character in passwordList!!) { - builder.append(character) - } - return builder.toString() - } - - fun clearPasscodeField() { - passwordList!!.clear() - invalidate() - } - - fun backSpace() { - if (passwordList!!.size > 0) { - passwordList!!.removeAt(passwordList!!.size - 1) - invalidate() - } - } - - fun revertPassCodeVisibility() { - isPasscodeVisible = !isPasscodeVisible - invalidate() - } - - fun passcodeVisible(): Boolean { - return isPasscodeVisible - } - - -} \ No newline at end of file diff --git a/mifos-passcode/src/main/java/com/mifos/mobile/passcode/PassCodeListener.kt b/mifos-passcode/src/main/java/com/mifos/mobile/passcode/PassCodeListener.kt deleted file mode 100644 index 2ac613c..0000000 --- a/mifos-passcode/src/main/java/com/mifos/mobile/passcode/PassCodeListener.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.mifos.mobile.passcode - -interface PassCodeListener { - fun passCodeEntered(passcode: String?) -} \ No newline at end of file diff --git a/mifos-passcode/src/main/java/com/mifos/mobile/passcode/utils/EncryptionUtil.kt b/mifos-passcode/src/main/java/com/mifos/mobile/passcode/utils/EncryptionUtil.kt deleted file mode 100644 index af2906d..0000000 --- a/mifos-passcode/src/main/java/com/mifos/mobile/passcode/utils/EncryptionUtil.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.mifos.mobile.passcode.utils - -import android.util.Log -import androidx.annotation.IntDef - -object EncryptionUtil { - - const val DEFAULT = 1 - const val MOBILE_BANKING = 2 - const val ANDROID_CLIENT = 3 - const val FINERACT_CN = 4 - - - @IntDef(DEFAULT, MOBILE_BANKING, ANDROID_CLIENT, FINERACT_CN) - @Retention(AnnotationRetention.SOURCE) - annotation class TYPE - - init { - try { - System.loadLibrary("encryption") - } catch (e: UnsatisfiedLinkError) { - Log.e("LoadJniLib", "Error: Could not load native library: ${e.message}") - } - } - - external fun getPassCodeHash(passcode: String): String - - fun getDefaultHash(passCode: String): String { - return getPassCodeHash(passCode) - } - - fun getMobileBankingHash(passCode: String): String { - return getPassCodeHash(passCode) - } - - fun getAndroidClientHash(passCode: String): String { - return getPassCodeHash(passCode) - } - - fun getFineractCNHash(passCode: String): String { - return getPassCodeHash(passCode) - } -} diff --git a/mifos-passcode/src/main/java/com/mifos/mobile/passcode/utils/ForegroundChecker.kt b/mifos-passcode/src/main/java/com/mifos/mobile/passcode/utils/ForegroundChecker.kt deleted file mode 100644 index c88942c..0000000 --- a/mifos-passcode/src/main/java/com/mifos/mobile/passcode/utils/ForegroundChecker.kt +++ /dev/null @@ -1,110 +0,0 @@ -package com.mifos.mobile.passcode.utils - -import android.content.Context -import android.os.Handler -import com.mifos.mobile.passcode.utils.ForegroundChecker - -/** - * Created by dilpreet on 18/7/17. - */ -class ForegroundChecker private constructor(context: Context) { - interface Listener { - fun onBecameForeground() - } - - /** - * Returns True if application is in foreground - * @return State of Application - */ - var isForeground = false - private set - private var paused = true - private val handler = Handler() - private var listener: Listener? = null - private var check: Runnable? = null - private var backgroundTimeStart: Long - private val passcodePreferencesHelper: PasscodePreferencesHelper - - /** - * Initializes [ForegroundChecker] - * @param context Application Context - */ - init { - backgroundTimeStart = -1 - passcodePreferencesHelper = PasscodePreferencesHelper(context) - } - - val isBackground: Boolean - /** - * Returns True if application is in background - * @return State of Application - */ - get() = !isForeground - - fun addListener(listener: Listener?) { - this.listener = listener - } - - /** - * It calls `onBecameForeground()` if `secondsInBackground` >= - * `MIN_BACKGROUND_THRESHOLD` - */ - fun onActivityResumed() { - paused = false - val wasBackground = !isForeground - isForeground = true - if (check != null) handler.removeCallbacks(check!!) - if (wasBackground) { - val secondsInBackground = ((System.currentTimeMillis() - backgroundTimeStart) / - 1000).toInt() - if (backgroundTimeStart != -1L && secondsInBackground >= MIN_BACKGROUND_THRESHOLD && listener != null && passcodePreferencesHelper.passCode?.isEmpty() == true) { - listener!!.onBecameForeground() - } - } - } - - /** - * It executes a Handler after `CHECK_DELAY` and then sets `foreground` to false - */ - fun onActivityPaused() { - paused = true - if (check != null) handler.removeCallbacks(check!!) - handler.postDelayed(object : Runnable { - override fun run() { - if (isForeground && paused) { - isForeground = false - backgroundTimeStart = System.currentTimeMillis() - } - } - }.also { check = it }, CHECK_DELAY) - } - - companion object { - const val CHECK_DELAY: Long = 500 - const val MIN_BACKGROUND_THRESHOLD = 60 - val TAG = ForegroundChecker::class.java.name - private var instance: ForegroundChecker? = null - - /** - * Used to initialize `instance` of [ForegroundChecker] - * @param context Application Content - * @return Instance of [ForegroundChecker] - */ - @JvmStatic - fun init(context: Context): ForegroundChecker? { - if (instance == null) { - instance = ForegroundChecker(context) - } - return instance - } - - /** - * Provides instance of [ForegroundChecker] - * @return Instance of [ForegroundChecker] - */ - @JvmStatic - fun get(): ForegroundChecker? { - return instance - } - } -} \ No newline at end of file diff --git a/mifos-passcode/src/main/java/com/mifos/mobile/passcode/utils/PassCodeConstants.kt b/mifos-passcode/src/main/java/com/mifos/mobile/passcode/utils/PassCodeConstants.kt deleted file mode 100644 index db51517..0000000 --- a/mifos-passcode/src/main/java/com/mifos/mobile/passcode/utils/PassCodeConstants.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.mifos.mobile.passcode.utils - -/** - * Created by dilpreet on 19/01/18. - */ -object PassCodeConstants { - const val PASSCODE_INITIAL_LOGIN = "initial_login" - const val RESET_PASSCODE = "reset_passcode" -} \ No newline at end of file diff --git a/mifos-passcode/src/main/java/com/mifos/mobile/passcode/utils/PassCodeNetworkChecker.kt b/mifos-passcode/src/main/java/com/mifos/mobile/passcode/utils/PassCodeNetworkChecker.kt deleted file mode 100644 index 2dd4433..0000000 --- a/mifos-passcode/src/main/java/com/mifos/mobile/passcode/utils/PassCodeNetworkChecker.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.mifos.mobile.passcode.utils - -import android.content.Context -import android.net.ConnectivityManager -import android.net.NetworkInfo -import android.telephony.TelephonyManager - -/** - * Created by rishabhkhanna on 07/03/17. - */ -object PassCodeNetworkChecker { - /** - * Get the network info - * - * @param context Context - * @return NetworkInfo - */ - fun getNetworkInfo(context: Context): NetworkInfo? { - val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - return cm.activeNetworkInfo - } - - /** - * Check if there is any connectivity - * - * @param context Context - * @return state of network - */ - @JvmStatic - fun isConnected(context: Context): Boolean { - val info = getNetworkInfo(context) - return info != null && info.isConnected - } - - /** - * Check if there is any connectivity to a Wifi network - * - * @param context Context - * @return state if wifi connection - */ - fun isConnectedWifi(context: Context): Boolean { - val info = getNetworkInfo(context) - return info != null && info.isConnected && info.type == ConnectivityManager.TYPE_WIFI - } - - /** - * Check if there is any connectivity to a mobile network - * - * @param context Context - * @return mobile connected to network or not - */ - fun isConnectedMobile(context: Context): Boolean { - val info = getNetworkInfo(context) - return info != null && info.isConnected && info.type == ConnectivityManager.TYPE_MOBILE - } - - /** - * Check if there is fast connectivity - * - * @param context Context - * @return connection is fast or not - */ - fun isConnectedFast(context: Context): Boolean { - val info = getNetworkInfo(context) - return info != null && info.isConnected && - isConnectionFast(info.type, info.subtype) - } - - /** - * Check if the connection is fast - * - * @param type Type of connection - * @param subType SubType of Connection - * @return connection is fast or not - */ - fun isConnectionFast(type: Int, subType: Int): Boolean { - return if (type == ConnectivityManager.TYPE_WIFI) { - true - } else if (type == ConnectivityManager.TYPE_MOBILE) { - when (subType) { - TelephonyManager.NETWORK_TYPE_1xRTT -> false // ~ 50-100 kbps - TelephonyManager.NETWORK_TYPE_CDMA -> false // ~ 14-64 kbps - TelephonyManager.NETWORK_TYPE_EDGE -> false // ~ 50-100 kbps - TelephonyManager.NETWORK_TYPE_EVDO_0 -> true // ~ 400-1000 kbps - TelephonyManager.NETWORK_TYPE_EVDO_A -> true // ~ 600-1400 kbps - TelephonyManager.NETWORK_TYPE_GPRS -> false // ~ 100 kbps - TelephonyManager.NETWORK_TYPE_HSDPA -> true // ~ 2-14 Mbps - TelephonyManager.NETWORK_TYPE_HSPA -> true // ~ 700-1700 kbps - TelephonyManager.NETWORK_TYPE_HSUPA -> true // ~ 1-23 Mbps - TelephonyManager.NETWORK_TYPE_UMTS -> true // ~ 400-7000 kbps - TelephonyManager.NETWORK_TYPE_EHRPD -> true // ~ 1-2 Mbps - TelephonyManager.NETWORK_TYPE_EVDO_B -> true // ~ 5 Mbps - TelephonyManager.NETWORK_TYPE_HSPAP -> true // ~ 10-20 Mbps - TelephonyManager.NETWORK_TYPE_IDEN -> false // ~25 kbps - TelephonyManager.NETWORK_TYPE_LTE -> true // ~ 10+ Mbps - TelephonyManager.NETWORK_TYPE_UNKNOWN -> false - else -> false - } - } else { - false - } - } -} \ No newline at end of file diff --git a/mifos-passcode/src/main/java/com/mifos/mobile/passcode/utils/PasscodePreferencesHelper.kt b/mifos-passcode/src/main/java/com/mifos/mobile/passcode/utils/PasscodePreferencesHelper.kt deleted file mode 100644 index 372f263..0000000 --- a/mifos-passcode/src/main/java/com/mifos/mobile/passcode/utils/PasscodePreferencesHelper.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.mifos.mobile.passcode.utils - -import android.content.Context -import android.content.SharedPreferences -import android.preference.PreferenceManager - -/** - * Created by dilpreet on 19/01/18. - */ -class PasscodePreferencesHelper(context: Context?) { - private val sharedPreferences: SharedPreferences - private val TOKEN = "preferences_mifos_passcode_string" - private val FINGERPRINTENABLER = "fingerprint_enable_dialog" - private val AUTHTYPE = "auth_type" - - init { - sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) - } - - fun savePassCode(token: String?) { - sharedPreferences.edit().putString(TOKEN, token).apply() - } - - val passCode: String? - get() = sharedPreferences.getString(TOKEN, "") - var fingerprintEnableDialogState: Boolean - get() = sharedPreferences.getBoolean(FINGERPRINTENABLER, true) - set(show) { - sharedPreferences.edit().putBoolean(FINGERPRINTENABLER, show).apply() - } - var authType: String? - get() = sharedPreferences.getString(AUTHTYPE, "") - set(authType) { - sharedPreferences.edit().putString(AUTHTYPE, authType).apply() - } - - fun clear() { - sharedPreferences.edit().clear().apply() - } -} \ No newline at end of file diff --git a/mifos-passcode/src/main/jniLibs/arm64-v8a/libencryption.so b/mifos-passcode/src/main/jniLibs/arm64-v8a/libencryption.so deleted file mode 100644 index 3beba0d..0000000 Binary files a/mifos-passcode/src/main/jniLibs/arm64-v8a/libencryption.so and /dev/null differ diff --git a/mifos-passcode/src/main/jniLibs/armeabi-v7a/libencryption.so b/mifos-passcode/src/main/jniLibs/armeabi-v7a/libencryption.so deleted file mode 100644 index f7bb7ef..0000000 Binary files a/mifos-passcode/src/main/jniLibs/armeabi-v7a/libencryption.so and /dev/null differ diff --git a/mifos-passcode/src/main/jniLibs/armeabi/libencryption.so b/mifos-passcode/src/main/jniLibs/armeabi/libencryption.so deleted file mode 100644 index 64484d8..0000000 Binary files a/mifos-passcode/src/main/jniLibs/armeabi/libencryption.so and /dev/null differ diff --git a/mifos-passcode/src/main/jniLibs/mips/libencryption.so b/mifos-passcode/src/main/jniLibs/mips/libencryption.so deleted file mode 100644 index 35f5e0b..0000000 Binary files a/mifos-passcode/src/main/jniLibs/mips/libencryption.so and /dev/null differ diff --git a/mifos-passcode/src/main/jniLibs/mips64/libencryption.so b/mifos-passcode/src/main/jniLibs/mips64/libencryption.so deleted file mode 100644 index 922e2b1..0000000 Binary files a/mifos-passcode/src/main/jniLibs/mips64/libencryption.so and /dev/null differ diff --git a/mifos-passcode/src/main/jniLibs/x86/libencryption.so b/mifos-passcode/src/main/jniLibs/x86/libencryption.so deleted file mode 100644 index 7e6ab86..0000000 Binary files a/mifos-passcode/src/main/jniLibs/x86/libencryption.so and /dev/null differ diff --git a/mifos-passcode/src/main/jniLibs/x86_64/libencryption.so b/mifos-passcode/src/main/jniLibs/x86_64/libencryption.so deleted file mode 100644 index 0e3461f..0000000 Binary files a/mifos-passcode/src/main/jniLibs/x86_64/libencryption.so and /dev/null differ diff --git a/mifos-passcode/src/main/res/anim/shake.xml b/mifos-passcode/src/main/res/anim/shake.xml deleted file mode 100644 index 1b5fd73..0000000 --- a/mifos-passcode/src/main/res/anim/shake.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - \ No newline at end of file diff --git a/mifos-passcode/src/main/res/drawable/ic_backspace_48px.xml b/mifos-passcode/src/main/res/drawable/ic_backspace_48px.xml deleted file mode 100644 index 6be920a..0000000 --- a/mifos-passcode/src/main/res/drawable/ic_backspace_48px.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/mifos-passcode/src/main/res/drawable/ic_cancel_red_48dp.xml b/mifos-passcode/src/main/res/drawable/ic_cancel_red_48dp.xml deleted file mode 100644 index 73beaa6..0000000 --- a/mifos-passcode/src/main/res/drawable/ic_cancel_red_48dp.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - \ No newline at end of file diff --git a/mifos-passcode/src/main/res/drawable/ic_check_circle_green_48dp.xml b/mifos-passcode/src/main/res/drawable/ic_check_circle_green_48dp.xml deleted file mode 100644 index fb0884a..0000000 --- a/mifos-passcode/src/main/res/drawable/ic_check_circle_green_48dp.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - \ No newline at end of file diff --git a/mifos-passcode/src/main/res/drawable/ic_fingerprint_blue_48dp.xml b/mifos-passcode/src/main/res/drawable/ic_fingerprint_blue_48dp.xml deleted file mode 100644 index 7a75ae6..0000000 --- a/mifos-passcode/src/main/res/drawable/ic_fingerprint_blue_48dp.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - \ No newline at end of file diff --git a/mifos-passcode/src/main/res/drawable/ic_visibility_48px.xml b/mifos-passcode/src/main/res/drawable/ic_visibility_48px.xml deleted file mode 100644 index 44f9d4f..0000000 --- a/mifos-passcode/src/main/res/drawable/ic_visibility_48px.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/mifos-passcode/src/main/res/layout/activity_pass_code.xml b/mifos-passcode/src/main/res/layout/activity_pass_code.xml deleted file mode 100644 index 6b15861..0000000 --- a/mifos-passcode/src/main/res/layout/activity_pass_code.xml +++ /dev/null @@ -1,226 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mifos-passcode/src/main/res/layout/dialog_fingerprint_auth.xml b/mifos-passcode/src/main/res/layout/dialog_fingerprint_auth.xml deleted file mode 100644 index 4d835da..0000000 --- a/mifos-passcode/src/main/res/layout/dialog_fingerprint_auth.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - diff --git a/mifos-passcode/src/main/res/values/attr.xml b/mifos-passcode/src/main/res/values/attr.xml deleted file mode 100644 index 612c58d..0000000 --- a/mifos-passcode/src/main/res/values/attr.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/mifos-passcode/src/main/res/values/colors.xml b/mifos-passcode/src/main/res/values/colors.xml deleted file mode 100644 index c5936fe..0000000 --- a/mifos-passcode/src/main/res/values/colors.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - #BB666666 - #03A9F4 - #ffd1d1d1 - \ No newline at end of file diff --git a/mifos-passcode/src/main/res/values/dimens.xml b/mifos-passcode/src/main/res/values/dimens.xml deleted file mode 100644 index ac4778a..0000000 --- a/mifos-passcode/src/main/res/values/dimens.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - 16dp - 72dp - 56dp - 20dp - 8dp - 20sp - 16dp - 24dp - 20dp - 16sp - 28dp - \ No newline at end of file diff --git a/mifos-passcode/src/main/res/values/strings.xml b/mifos-passcode/src/main/res/values/strings.xml deleted file mode 100644 index 8fadea8..0000000 --- a/mifos-passcode/src/main/res/values/strings.xml +++ /dev/null @@ -1,39 +0,0 @@ - - Mobile-Passcode - Enter 4 digit Passcode - Passcode should be of 4 digit - Incorrect Passcode - You have entered incorrect Passcode more than 3 times - Skip - Save - Proceed - Setup a passcode to login - Please re-enter your passcode - Passcode does not match. - Forgot passcode, login manually - No Internet Connection - Confirm your passcode - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 0 - MainActivity - - Touch the Sensor - Use your fingerprint to access the app - Login to Mifos - Use password instead - Touch the Sensor - Fingerprint Not Recognized - Authentication Successful - Authentication Cancelled - Fingerprint - Do you want to enable Fingerprint Authentication? - \ No newline at end of file diff --git a/mifos-passcode/src/main/res/values/styles.xml b/mifos-passcode/src/main/res/values/styles.xml deleted file mode 100644 index e52906d..0000000 --- a/mifos-passcode/src/main/res/values/styles.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - -