-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
implement authentication screen on Android and iOS
- Loading branch information
Showing
25 changed files
with
644 additions
and
75 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,2 @@ | ||
This version brings new features to app: | ||
- manually reorder items with drag & drop gesture when sorting is disabled | ||
- require user authentication if accessible |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
155 changes: 155 additions & 0 deletions
155
shared/src/androidMain/kotlin/ml/dev/kotlin/openotp/util/BiometryUtil.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
package ml.dev.kotlin.openotp.util | ||
|
||
import android.annotation.SuppressLint | ||
import android.content.Context | ||
import androidx.biometric.BiometricManager | ||
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK | ||
import androidx.biometric.BiometricPrompt | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.LaunchedEffect | ||
import androidx.compose.ui.platform.LocalContext | ||
import androidx.compose.ui.platform.LocalLifecycleOwner | ||
import androidx.core.content.ContextCompat | ||
import androidx.fragment.app.Fragment | ||
import androidx.fragment.app.FragmentActivity | ||
import androidx.fragment.app.FragmentManager | ||
import androidx.lifecycle.Lifecycle | ||
import androidx.lifecycle.LifecycleObserver | ||
import androidx.lifecycle.LifecycleOwner | ||
import androidx.lifecycle.OnLifecycleEvent | ||
import java.util.concurrent.Executor | ||
import kotlin.coroutines.suspendCoroutine | ||
|
||
actual class BiometryAuthenticator( | ||
private val applicationContext: Context, | ||
) { | ||
private var fragmentManager: FragmentManager? = null | ||
|
||
fun bind(lifecycle: Lifecycle, fragmentManager: FragmentManager) { | ||
this.fragmentManager = fragmentManager | ||
|
||
val observer = object : LifecycleObserver { | ||
|
||
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) | ||
fun onDestroyed(source: LifecycleOwner) { | ||
this@BiometryAuthenticator.fragmentManager = null | ||
source.lifecycle.removeObserver(this) | ||
} | ||
} | ||
lifecycle.addObserver(observer) | ||
} | ||
|
||
actual suspend fun checkBiometryAuthentication( | ||
requestTitle: String, | ||
requestReason: String, | ||
): Boolean { | ||
val resolverFragment: ResolverFragment = getResolverFragment() | ||
|
||
return suspendCoroutine { continuation -> | ||
var resumed = false | ||
resolverFragment.showBiometricPrompt( | ||
requestTitle = requestTitle, | ||
requestReason = requestReason, | ||
) { | ||
if (!resumed) { | ||
continuation.resumeWith(it) | ||
resumed = true | ||
} | ||
} | ||
} | ||
} | ||
|
||
actual fun isBiometricAvailable(): Boolean { | ||
val manager: BiometricManager = BiometricManager.from(applicationContext) | ||
return manager.canAuthenticate(BIOMETRIC_WEAK) == BiometricManager.BIOMETRIC_SUCCESS | ||
} | ||
|
||
private fun getResolverFragment(): ResolverFragment { | ||
val fragmentManager = fragmentManager | ||
?: error("can't check biometry without active window") | ||
|
||
val currentFragment = fragmentManager | ||
.findFragmentByTag(BIOMETRY_RESOLVER_FRAGMENT_TAG) | ||
|
||
return if (currentFragment != null) { | ||
currentFragment as ResolverFragment | ||
} else { | ||
ResolverFragment().apply { | ||
fragmentManager | ||
.beginTransaction() | ||
.add(this, BIOMETRY_RESOLVER_FRAGMENT_TAG) | ||
.commitNow() | ||
} | ||
} | ||
} | ||
|
||
class ResolverFragment : Fragment() { | ||
private lateinit var executor: Executor | ||
private lateinit var biometricPrompt: BiometricPrompt | ||
private lateinit var promptInfo: BiometricPrompt.PromptInfo | ||
|
||
init { | ||
retainInstance = true | ||
} | ||
|
||
fun showBiometricPrompt( | ||
requestTitle: String, | ||
requestReason: String, | ||
callback: (Result<Boolean>) -> Unit, | ||
) { | ||
val context = requireContext() | ||
|
||
executor = ContextCompat.getMainExecutor(context) | ||
|
||
biometricPrompt = BiometricPrompt(this, executor, | ||
object : BiometricPrompt.AuthenticationCallback() { | ||
@SuppressLint("RestrictedApi") | ||
override fun onAuthenticationError( | ||
errorCode: Int, | ||
errString: CharSequence, | ||
) { | ||
super.onAuthenticationError(errorCode, errString) | ||
if (errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON || | ||
errorCode == BiometricPrompt.ERROR_USER_CANCELED | ||
) { | ||
callback.invoke(Result.success(false)) | ||
} else { | ||
callback.invoke(Result.failure(Exception(errString.toString()))) | ||
} | ||
} | ||
|
||
override fun onAuthenticationSucceeded( | ||
result: BiometricPrompt.AuthenticationResult, | ||
) { | ||
super.onAuthenticationSucceeded(result) | ||
callback.invoke(Result.success(true)) | ||
} | ||
} | ||
) | ||
|
||
promptInfo = BiometricPrompt.PromptInfo.Builder() | ||
.setTitle(requestTitle) | ||
.setSubtitle(requestReason) | ||
.setDeviceCredentialAllowed(true) | ||
.build() | ||
|
||
biometricPrompt.authenticate(promptInfo) | ||
} | ||
} | ||
|
||
companion object { | ||
private const val BIOMETRY_RESOLVER_FRAGMENT_TAG = "BiometryControllerResolver" | ||
} | ||
} | ||
|
||
@Composable | ||
actual fun BindBiometryAuthenticatorEffect(biometryAuthenticator: BiometryAuthenticator) { | ||
val lifecycleOwner = LocalLifecycleOwner.current | ||
val context = LocalContext.current | ||
|
||
LaunchedEffect(biometryAuthenticator, lifecycleOwner, context) { | ||
val fragmentManager = (context as FragmentActivity).supportFragmentManager | ||
biometryAuthenticator.bind(lifecycleOwner.lifecycle, fragmentManager) | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.