feat: basic api, captcha, and login flow
whew, that's a lot
This commit is contained in:
parent
946429a2f5
commit
a2fb59c6f8
25 changed files with 605 additions and 50 deletions
|
|
@ -5,12 +5,7 @@ import androidx.activity.ComponentActivity
|
|||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.navigation3.ui.defaultPredictivePopTransitionSpec
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metrox.viewmodel.LocalMetroViewModelFactory
|
||||
import dev.zacsweers.metrox.viewmodel.MetroViewModelFactory
|
||||
import moe.lava.neon.ui.App
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,133 @@
|
|||
package moe.lava.neon.ui
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.hcaptcha.sdk.HCaptcha
|
||||
import com.hcaptcha.sdk.HCaptchaConfig
|
||||
import com.hcaptcha.sdk.HCaptchaSize
|
||||
import com.hcaptcha.sdk.HCaptchaTokenResponse
|
||||
import com.hcaptcha.sdk.HCaptchaVerifyParams
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.lava.neon.core.api.ApiClient
|
||||
import moe.lava.neon.core.api.captcha.CaptchaResponse
|
||||
|
||||
private val logger = Logger.withTag("neon.ui.app/captcha")
|
||||
|
||||
private const val EXTRA_SITE_KEY = "extra_site_key"
|
||||
private const val EXTRA_RQ_DATA = "extra_rq_data"
|
||||
private const val EXTRA_IS_INVISIBLE = "extra_is_invisible"
|
||||
private const val EXTRA_RESULT_TOKEN = "extra_result_token"
|
||||
private const val EXTRA_RESULT_ERROR = "extra_result_error"
|
||||
|
||||
@Composable
|
||||
actual fun CaptchaBinder(api: ApiClient) {
|
||||
val context = LocalContext.current
|
||||
val queue = MutableSharedFlow<Pair<String, CaptchaResponse>>()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val launcher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
val rq = result.data?.getStringExtra(EXTRA_RQ_DATA)
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
val token = result.data?.getStringExtra(EXTRA_RESULT_TOKEN)
|
||||
scope.launch {
|
||||
if (rq == null) {
|
||||
logger.e { "Captcha failed: No rq in result" }
|
||||
queue.emit("" to CaptchaResponse.Failed(Throwable("No rq?")))
|
||||
} else if (token == null) {
|
||||
logger.d { "Captcha failed: No token in result" }
|
||||
queue.emit(rq to CaptchaResponse.Failed(Throwable("No token returned?")))
|
||||
} else {
|
||||
logger.d { "Captcha success; token $token" }
|
||||
queue.emit(rq to CaptchaResponse.Success(token))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val error = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
result.data?.getSerializableExtra(EXTRA_RESULT_ERROR, Throwable::class.java)
|
||||
} else {
|
||||
result.data?.getSerializableExtra(EXTRA_RESULT_ERROR) as? Throwable
|
||||
}
|
||||
logger.e(error) { "Captcha failed" }
|
||||
scope.launch {
|
||||
queue.emit((rq ?: "") to CaptchaResponse.Failed(error ?: Throwable("No error returned")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
api.setCaptchaHandler { captcha ->
|
||||
val intent = Intent(context, HCaptchaActivity::class.java).apply {
|
||||
putExtra(EXTRA_SITE_KEY, captcha.captchaSitekey)
|
||||
putExtra(EXTRA_RQ_DATA, captcha.captchaRqdata)
|
||||
putExtra(EXTRA_IS_INVISIBLE, captcha.shouldServeInvisible)
|
||||
}
|
||||
launcher.launch(intent)
|
||||
queue
|
||||
.first { (rqdata) -> rqdata == captcha.captchaRqdata }
|
||||
.second
|
||||
}
|
||||
}
|
||||
|
||||
class HCaptchaActivity : AppCompatActivity() {
|
||||
private val hCaptcha = HCaptcha.getClient(this)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val siteKey = intent.getStringExtra(EXTRA_SITE_KEY)
|
||||
?: return finishWithError("", IllegalArgumentException("Missing site key"))
|
||||
|
||||
val rqData = intent.getStringExtra(EXTRA_RQ_DATA)
|
||||
?: return finishWithError("", IllegalArgumentException("Missing rq data"))
|
||||
|
||||
val isInvisible = intent.getBooleanExtra(EXTRA_IS_INVISIBLE, false)
|
||||
|
||||
val config = HCaptchaConfig.builder()
|
||||
.siteKey(siteKey)
|
||||
.size(if (isInvisible) HCaptchaSize.INVISIBLE else HCaptchaSize.NORMAL)
|
||||
.build()
|
||||
|
||||
val params = HCaptchaVerifyParams.builder()
|
||||
.rqdata(rqData)
|
||||
.build()
|
||||
|
||||
hCaptcha
|
||||
.addOnOpenListener { logger.d { "hCaptcha is now visible." } }
|
||||
.addOnSuccessListener { finishWithSuccess(rqData, it) }
|
||||
.addOnFailureListener { finishWithError(rqData, it) }
|
||||
|
||||
hCaptcha.setup(config).verifyWithHCaptcha(params)
|
||||
}
|
||||
|
||||
private fun finishWithSuccess(rq: String, res: HCaptchaTokenResponse) {
|
||||
logger.d { "Captcha success; token ${res.tokenResult}" }
|
||||
val resultIntent = Intent().apply {
|
||||
putExtra(EXTRA_RQ_DATA, rq)
|
||||
putExtra(EXTRA_RESULT_TOKEN, res.tokenResult)
|
||||
}
|
||||
setResult(RESULT_OK, resultIntent)
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun finishWithError(rq: String, exception: Throwable) {
|
||||
logger.e(exception) { "Captcha failed" }
|
||||
val resultIntent = Intent().apply {
|
||||
putExtra(EXTRA_RQ_DATA, rq)
|
||||
putExtra(EXTRA_RESULT_ERROR, exception)
|
||||
}
|
||||
setResult(RESULT_CANCELED, resultIntent)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue