diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6ac84cfec2..324da8447d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -201,6 +201,7 @@ dependencies { implementation(projects.features.call) implementation(projects.anvilannotations) implementation(projects.appnav) + implementation(projects.appconfig) anvil(projects.anvilcodegen) implementation(libs.appyx.core) diff --git a/appconfig/build.gradle.kts b/appconfig/build.gradle.kts index 3c03739553..3f9275d383 100644 --- a/appconfig/build.gradle.kts +++ b/appconfig/build.gradle.kts @@ -16,9 +16,20 @@ plugins { id("java-library") alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.anvil) + alias(libs.plugins.ksp) } java { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(libs.dagger) + implementation(projects.libraries.di) +} diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt index 75cf8406ed..e73b3cd150 100644 --- a/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt +++ b/appconfig/src/main/kotlin/io/element/android/appconfig/LockScreenConfig.kt @@ -16,30 +16,52 @@ package io.element.android.appconfig -object LockScreenConfig { +import com.squareup.anvil.annotations.ContributesTo +import dagger.Module +import dagger.Provides +import io.element.android.libraries.di.AppScope +/** + * Configuration for the lock screen feature. + */ +data class LockScreenConfig( /** * Whether the PIN is mandatory or not. */ - const val IS_PIN_MANDATORY: Boolean = false + val isPinMandatory: Boolean, /** * Some PINs are blacklisted. */ - val PIN_BLACKLIST = setOf("0000", "1234") + val pinBlacklist: Set, /** * The size of the PIN. */ - const val PIN_SIZE = 4 + val pinSize: Int, /** * Number of attempts before the user is logged out. */ - const val MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT = 3 + val maxPinCodeAttemptsBeforeLogout: Int, /** * Time period before locking the app once backgrounded. */ - const val GRACE_PERIOD_IN_MILLIS = 90 * 1000L + val gracePeriodInMillis: Long +) + +@ContributesTo(AppScope::class) +@Module +object LockScreenConfigModule { + + @Provides + fun providesLockScreenConfig(): LockScreenConfig = LockScreenConfig( + isPinMandatory = false, + pinBlacklist = setOf("0000", "1234"), + pinSize = 4, + maxPinCodeAttemptsBeforeLogout = 3, + gracePeriodInMillis = 90_000L + ) + } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt index afdc4e7899..32e1e3d1b9 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/DefaultLockScreenService.kt @@ -39,6 +39,7 @@ import javax.inject.Inject @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) class DefaultLockScreenService @Inject constructor( + private val lockScreenConfig: LockScreenConfig, private val featureFlagService: FeatureFlagService, private val pinCodeManager: PinCodeManager, private val coroutineScope: CoroutineScope, @@ -91,14 +92,14 @@ class DefaultLockScreenService @Inject constructor( if (isInForeground) { lockJob?.cancel() } else { - lockJob = lockIfNeeded(delayInMillis = LockScreenConfig.GRACE_PERIOD_IN_MILLIS) + lockJob = lockIfNeeded(delayInMillis = lockScreenConfig.gracePeriodInMillis) } } } } override suspend fun isSetupRequired(): Boolean { - return LockScreenConfig.IS_PIN_MANDATORY + return lockScreenConfig.isPinMandatory && featureFlagService.isFeatureEnabled(FeatureFlags.PinUnlock) && !pinCodeManager.isPinCodeAvailable() } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt index 04f849e117..8631c05502 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/pin/storage/PreferencesPinCodeStore.kt @@ -38,6 +38,7 @@ private val Context.dataStore: DataStore by preferencesDataStore(na @ContributesBinding(AppScope::class) class PreferencesPinCodeStore @Inject constructor( @ApplicationContext private val context: Context, + private val lockScreenConfig: LockScreenConfig, ) : PinCodeStore { private val pinCodeKey = stringPreferencesKey("encoded_pin_code") @@ -59,7 +60,7 @@ class PreferencesPinCodeStore @Inject constructor( override suspend fun resetCounter() { context.dataStore.edit { preferences -> - preferences[remainingAttemptsKey] = LockScreenConfig.MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT + preferences[remainingAttemptsKey] = lockScreenConfig.maxPinCodeAttemptsBeforeLogout } } @@ -87,5 +88,5 @@ class PreferencesPinCodeStore @Inject constructor( }.first() } - private fun Preferences.getRemainingPinCodeAttemptsNumber() = this[remainingAttemptsKey] ?: LockScreenConfig.MAX_PIN_CODE_ATTEMPTS_NUMBER_BEFORE_LOGOUT + private fun Preferences.getRemainingPinCodeAttemptsNumber() = this[remainingAttemptsKey] ?: lockScreenConfig.maxPinCodeAttemptsBeforeLogout } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt index 110ca542de..9032e8d0ef 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsEvents.kt @@ -17,7 +17,7 @@ package io.element.android.features.lockscreen.impl.settings sealed interface LockScreenSettingsEvents { - data object RemovePin : LockScreenSettingsEvents + data object OnRemovePin : LockScreenSettingsEvents data object ConfirmRemovePin : LockScreenSettingsEvents data object CancelRemovePin : LockScreenSettingsEvents data object ToggleBiometric : LockScreenSettingsEvents diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt index a91597448a..9bef944194 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenter.kt @@ -31,6 +31,7 @@ import kotlinx.coroutines.launch import javax.inject.Inject class LockScreenSettingsPresenter @Inject constructor( + private val lockScreenConfig: LockScreenConfig, private val pinCodeManager: PinCodeManager, private val coroutineScope: CoroutineScope, ) : Presenter { @@ -50,7 +51,7 @@ class LockScreenSettingsPresenter @Inject constructor( mutableStateOf(false) } LaunchedEffect(triggerComputation) { - showRemovePinOption = !LockScreenConfig.IS_PIN_MANDATORY && pinCodeManager.isPinCodeAvailable() + showRemovePinOption = !lockScreenConfig.isPinMandatory && pinCodeManager.isPinCodeAvailable() } fun handleEvents(event: LockScreenSettingsEvents) { @@ -58,12 +59,14 @@ class LockScreenSettingsPresenter @Inject constructor( LockScreenSettingsEvents.CancelRemovePin -> showRemovePinConfirmation = false LockScreenSettingsEvents.ConfirmRemovePin -> { coroutineScope.launch { - showRemovePinConfirmation = false - pinCodeManager.deletePinCode() - triggerComputation++ + if (showRemovePinConfirmation) { + showRemovePinConfirmation = false + pinCodeManager.deletePinCode() + triggerComputation++ + } } } - LockScreenSettingsEvents.RemovePin -> showRemovePinConfirmation = true + LockScreenSettingsEvents.OnRemovePin -> showRemovePinConfirmation = true LockScreenSettingsEvents.ToggleBiometric -> { //TODO branch biometric logic } @@ -77,5 +80,4 @@ class LockScreenSettingsPresenter @Inject constructor( eventSink = ::handleEvents ) } - } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt index 08234b6b07..97c640ba78 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt @@ -54,7 +54,7 @@ fun LockScreenSettingsView( title = stringResource(id = R.string.screen_app_lock_settings_remove_pin), tintColor = ElementTheme.colors.textCriticalPrimary, onClick = { - state.eventSink(LockScreenSettingsEvents.RemovePin) + state.eventSink(LockScreenSettingsEvents.OnRemovePin) } ) } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt index 06de31281c..89e8b98c51 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenter.kt @@ -33,6 +33,7 @@ import kotlinx.coroutines.delay import javax.inject.Inject class SetupPinPresenter @Inject constructor( + private val lockScreenConfig: LockScreenConfig, private val pinValidator: PinValidator, private val buildMeta: BuildMeta, private val pinCodeManager: PinCodeManager, @@ -41,10 +42,10 @@ class SetupPinPresenter @Inject constructor( @Composable override fun present(): SetupPinState { var choosePinEntry by remember { - mutableStateOf(PinEntry.createEmpty(LockScreenConfig.PIN_SIZE)) + mutableStateOf(PinEntry.createEmpty(lockScreenConfig.pinSize)) } var confirmPinEntry by remember { - mutableStateOf(PinEntry.createEmpty(LockScreenConfig.PIN_SIZE)) + mutableStateOf(PinEntry.createEmpty(lockScreenConfig.pinSize)) } var isConfirmationStep by remember { mutableStateOf(false) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt index b164ee8c88..ec17411396 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/validation/PinValidator.kt @@ -20,10 +20,7 @@ import io.element.android.appconfig.LockScreenConfig import io.element.android.features.lockscreen.impl.pin.model.PinEntry import javax.inject.Inject -class PinValidator internal constructor(private val pinBlacklist: Set) { - - @Inject - constructor() : this(LockScreenConfig.PIN_BLACKLIST) +class PinValidator @Inject constructor(private val lockScreenConfig: LockScreenConfig) { sealed interface Result { data object Valid : Result @@ -32,7 +29,7 @@ class PinValidator internal constructor(private val pinBlacklist: Set) { fun isPinValid(pinEntry: PinEntry): Result { val pinAsText = pinEntry.toText() - val isBlacklisted = pinBlacklist.any { it == pinAsText } + val isBlacklisted = lockScreenConfig.pinBlacklist.any { it == pinAsText } return if (isBlacklisted) { Result.Invalid(SetupPinFailure.PinBlacklisted) } else { diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/LockScreenConfig.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/LockScreenConfig.kt new file mode 100644 index 0000000000..d42dad101a --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/LockScreenConfig.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.lockscreen.impl.fixtures + +import io.element.android.appconfig.LockScreenConfig + +internal fun aLockScreenConfig( + isPinMandatory: Boolean = false, + pinBlacklist: Set = emptySet(), + pinSize: Int = 4, + maxPinCodeAttemptsBeforeLogout: Int = 3, + gracePeriodInMillis: Long = 5 * 60 * 1000L +): LockScreenConfig { + return LockScreenConfig( + isPinMandatory = isPinMandatory, + pinBlacklist = pinBlacklist, + pinSize = pinSize, + maxPinCodeAttemptsBeforeLogout = maxPinCodeAttemptsBeforeLogout, + gracePeriodInMillis = gracePeriodInMillis + ) +} diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/PinCodeManager.kt similarity index 59% rename from features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt rename to features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/PinCodeManager.kt index a2e2dacf97..bf9ebdf541 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/pin/PinCodeManager.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/fixtures/PinCodeManager.kt @@ -14,15 +14,20 @@ * limitations under the License. */ -package io.element.android.features.lockscreen.impl.pin +package io.element.android.features.lockscreen.impl.fixtures +import io.element.android.features.lockscreen.impl.pin.DefaultPinCodeManager +import io.element.android.features.lockscreen.impl.pin.PinCodeManager import io.element.android.features.lockscreen.impl.pin.storage.InMemoryPinCodeStore +import io.element.android.features.lockscreen.impl.pin.storage.PinCodeStore +import io.element.android.libraries.cryptography.api.EncryptionDecryptionService import io.element.android.libraries.cryptography.impl.AESEncryptionDecryptionService import io.element.android.libraries.cryptography.test.SimpleSecretKeyProvider -internal fun createPinCodeManager(): PinCodeManager { - val pinCodeStore = InMemoryPinCodeStore() - val secretKeyProvider = SimpleSecretKeyProvider() - val encryptionDecryptionService = AESEncryptionDecryptionService() +internal fun aPinCodeManager( + pinCodeStore: PinCodeStore = InMemoryPinCodeStore(), + secretKeyProvider: SimpleSecretKeyProvider = SimpleSecretKeyProvider(), + encryptionDecryptionService: EncryptionDecryptionService = AESEncryptionDecryptionService(), +): PinCodeManager { return DefaultPinCodeManager(secretKeyProvider, encryptionDecryptionService, pinCodeStore) } diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenterTest.kt new file mode 100644 index 0000000000..578f20e7d9 --- /dev/null +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsPresenterTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.lockscreen.impl.settings + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.appconfig.LockScreenConfig +import io.element.android.features.lockscreen.impl.fixtures.aLockScreenConfig +import io.element.android.features.lockscreen.impl.fixtures.aPinCodeManager +import io.element.android.tests.testutils.awaitLastSequentialItem +import io.element.android.tests.testutils.consumeItemsUntilPredicate +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class LockScreenSettingsPresenterTest { + + @Test + fun `present - remove pin flow`() = runTest { + val presenter = createLockScreenSettingsPresenter(this) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + consumeItemsUntilPredicate { state -> + state.showRemovePinOption + }.last().also { state -> + state.eventSink(LockScreenSettingsEvents.OnRemovePin) + } + awaitLastSequentialItem().also { state -> + assertThat(state.showRemovePinConfirmation).isTrue() + state.eventSink(LockScreenSettingsEvents.CancelRemovePin) + } + awaitLastSequentialItem().also { state -> + assertThat(state.showRemovePinConfirmation).isFalse() + state.eventSink(LockScreenSettingsEvents.OnRemovePin) + } + awaitLastSequentialItem().also { state -> + assertThat(state.showRemovePinConfirmation).isTrue() + state.eventSink(LockScreenSettingsEvents.ConfirmRemovePin) + } + consumeItemsUntilPredicate { + it.showRemovePinOption.not() + }.last().also { state -> + assertThat(state.showRemovePinConfirmation).isFalse() + assertThat(state.showRemovePinOption).isFalse() + } + } + } + + private suspend fun createLockScreenSettingsPresenter( + coroutineScope: CoroutineScope, + lockScreenConfig: LockScreenConfig = aLockScreenConfig(), + ): LockScreenSettingsPresenter { + val pinCodeManager = aPinCodeManager().apply { + createPinCode("1234") + } + return LockScreenSettingsPresenter( + pinCodeManager = pinCodeManager, + coroutineScope = coroutineScope, + lockScreenConfig = lockScreenConfig, + ) + } +} diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt index 0cc38dd355..81c37e6d69 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/setup/SetupPinPresenterTest.kt @@ -20,8 +20,10 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.appconfig.LockScreenConfig +import io.element.android.features.lockscreen.impl.fixtures.aLockScreenConfig +import io.element.android.features.lockscreen.impl.fixtures.aPinCodeManager import io.element.android.features.lockscreen.impl.pin.PinCodeManager -import io.element.android.features.lockscreen.impl.pin.createPinCodeManager import io.element.android.features.lockscreen.impl.pin.model.assertEmpty import io.element.android.features.lockscreen.impl.pin.model.assertText import io.element.android.features.lockscreen.impl.setup.validation.PinValidator @@ -108,9 +110,19 @@ class SetupPinPresenterTest { } } - private fun createSetupPinPresenter(callback: PinCodeManager.Callback): SetupPinPresenter { - val pinCodeManager = createPinCodeManager() + private fun createSetupPinPresenter( + callback: PinCodeManager.Callback, + lockScreenConfig: LockScreenConfig = aLockScreenConfig( + pinBlacklist = setOf(blacklistedPin) + ), + ): SetupPinPresenter { + val pinCodeManager = aPinCodeManager() pinCodeManager.addCallback(callback) - return SetupPinPresenter(PinValidator(setOf(blacklistedPin)), aBuildMeta(), pinCodeManager) + return SetupPinPresenter( + lockScreenConfig = lockScreenConfig, + pinValidator = PinValidator(lockScreenConfig), + buildMeta = aBuildMeta(), + pinCodeManager = pinCodeManager + ) } } diff --git a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt index 16e44b2af0..e6ba51c537 100644 --- a/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt +++ b/features/lockscreen/impl/src/test/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockPresenterTest.kt @@ -21,7 +21,7 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.lockscreen.impl.pin.PinCodeManager -import io.element.android.features.lockscreen.impl.pin.createPinCodeManager +import io.element.android.features.lockscreen.impl.fixtures.aPinCodeManager import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.pin.model.assertText import io.element.android.features.lockscreen.impl.unlock.keypad.PinKeypadModel @@ -147,7 +147,7 @@ class PinUnlockPresenterTest { scope: CoroutineScope, callback: PinCodeManager.Callback = object : PinCodeManager.Callback {}, ): PinUnlockPresenter { - val pinCodeManager = createPinCodeManager().apply { + val pinCodeManager = aPinCodeManager().apply { addCallback(callback) createPinCode(completePin) }