Merge branch 'vitorpamplona:main' into main
This commit is contained in:
+2
-2
@@ -12,8 +12,8 @@ android {
|
|||||||
applicationId "com.vitorpamplona.amethyst"
|
applicationId "com.vitorpamplona.amethyst"
|
||||||
minSdk 26
|
minSdk 26
|
||||||
targetSdk 33
|
targetSdk 33
|
||||||
versionCode 107
|
versionCode 108
|
||||||
versionName "0.30.0"
|
versionName "0.30.1"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import com.google.gson.reflect.TypeToken
|
|||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.RelaySetupInfo
|
import com.vitorpamplona.amethyst.model.RelaySetupInfo
|
||||||
import com.vitorpamplona.amethyst.model.toByteArray
|
import com.vitorpamplona.amethyst.model.toByteArray
|
||||||
import com.vitorpamplona.amethyst.service.model.Contact
|
|
||||||
import com.vitorpamplona.amethyst.service.model.ContactListEvent
|
import com.vitorpamplona.amethyst.service.model.ContactListEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.Event
|
import com.vitorpamplona.amethyst.service.model.Event
|
||||||
import com.vitorpamplona.amethyst.service.model.Event.Companion.getRefinedEvent
|
import com.vitorpamplona.amethyst.service.model.Event.Companion.getRefinedEvent
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.Nip47URI
|
||||||
import fr.acinq.secp256k1.Hex
|
import fr.acinq.secp256k1.Hex
|
||||||
import nostr.postr.Persona
|
import nostr.postr.Persona
|
||||||
import nostr.postr.toHex
|
import nostr.postr.toHex
|
||||||
@@ -215,7 +215,7 @@ object LocalPreferences {
|
|||||||
|
|
||||||
val zapPaymentRequestServer = try {
|
val zapPaymentRequestServer = try {
|
||||||
getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)?.let {
|
getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)?.let {
|
||||||
gson.fromJson(it, Contact::class.java)
|
gson.fromJson(it, Nip47URI::class.java)
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.service.relays.FeedType
|
|||||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||||
import com.vitorpamplona.amethyst.service.relays.RelayPool
|
import com.vitorpamplona.amethyst.service.relays.RelayPool
|
||||||
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.Nip47URI
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
@@ -56,7 +57,7 @@ class Account(
|
|||||||
var languagePreferences: Map<String, String> = mapOf(),
|
var languagePreferences: Map<String, String> = mapOf(),
|
||||||
var translateTo: String = Locale.getDefault().language,
|
var translateTo: String = Locale.getDefault().language,
|
||||||
var zapAmountChoices: List<Long> = listOf(500L, 1000L, 5000L),
|
var zapAmountChoices: List<Long> = listOf(500L, 1000L, 5000L),
|
||||||
var zapPaymentRequest: Contact? = null,
|
var zapPaymentRequest: Nip47URI? = null,
|
||||||
var hideDeleteRequestDialog: Boolean = false,
|
var hideDeleteRequestDialog: Boolean = false,
|
||||||
var hideBlockAlertDialog: Boolean = false,
|
var hideBlockAlertDialog: Boolean = false,
|
||||||
var backupContactList: ContactListEvent? = null
|
var backupContactList: ContactListEvent? = null
|
||||||
@@ -169,7 +170,7 @@ class Account(
|
|||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
zapPaymentRequest?.let {
|
zapPaymentRequest?.let {
|
||||||
val event = LnZapPaymentRequestEvent.create(lnInvoice, it.pubKeyHex, loggedIn.privKey!!)
|
val event = LnZapPaymentRequestEvent.create(lnInvoice, it.pubKeyHex, it.secret?.toByteArray() ?: loggedIn.privKey!!)
|
||||||
|
|
||||||
Client.send(event, it.relayUri)
|
Client.send(event, it.relayUri)
|
||||||
}
|
}
|
||||||
@@ -572,7 +573,7 @@ class Account(
|
|||||||
saveable.invalidateData()
|
saveable.invalidateData()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changeZapPaymentRequest(newServer: Contact?) {
|
fun changeZapPaymentRequest(newServer: Nip47URI?) {
|
||||||
zapPaymentRequest = newServer
|
zapPaymentRequest = newServer
|
||||||
live.invalidateData()
|
live.invalidateData()
|
||||||
saveable.invalidateData()
|
saveable.invalidateData()
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ package com.vitorpamplona.amethyst.ui.note
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.vitorpamplona.amethyst.model.decodePublicKey
|
import com.vitorpamplona.amethyst.model.decodePublicKey
|
||||||
import com.vitorpamplona.amethyst.model.toHexKey
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
import com.vitorpamplona.amethyst.service.model.Contact
|
|
||||||
|
data class Nip47URI(val pubKeyHex: String, val relayUri: String?, val secret: String?)
|
||||||
|
|
||||||
// Rename to the corect nip number when ready.
|
// Rename to the corect nip number when ready.
|
||||||
object Nip47 {
|
object Nip47 {
|
||||||
fun parse(uri: String): Contact? {
|
fun parse(uri: String): Nip47URI {
|
||||||
// nostrwalletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io&metadata=%7B%22name%22%3A%22Example%22%7D
|
// nostrwalletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io&metadata=%7B%22name%22%3A%22Example%22%7D
|
||||||
|
|
||||||
val url = Uri.parse(uri)
|
val url = Uri.parse(uri)
|
||||||
@@ -25,7 +26,8 @@ object Nip47 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val relay = url.getQueryParameter("relay")
|
val relay = url.getQueryParameter("relay")
|
||||||
|
val secret = url.getQueryParameter("secret")
|
||||||
|
|
||||||
return Contact(pubkeyHex, relay)
|
return Nip47URI(pubkeyHex, relay, secret)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,27 @@ open class Event(
|
|||||||
override fun isTaggedHashes(hashtags: Set<String>) = tags.any { it.getOrNull(0) == "t" && it.getOrNull(1)?.lowercase() in hashtags }
|
override fun isTaggedHashes(hashtags: Set<String>) = tags.any { it.getOrNull(0) == "t" && it.getOrNull(1)?.lowercase() in hashtags }
|
||||||
override fun firstIsTaggedHashes(hashtags: Set<String>) = tags.firstOrNull { it.getOrNull(0) == "t" && it.getOrNull(1)?.lowercase() in hashtags }?.getOrNull(1)
|
override fun firstIsTaggedHashes(hashtags: Set<String>) = tags.firstOrNull { it.getOrNull(0) == "t" && it.getOrNull(1)?.lowercase() in hashtags }?.getOrNull(1)
|
||||||
|
|
||||||
|
override fun getPoWRank(): Int {
|
||||||
|
var rank = 0
|
||||||
|
for (i in 0..id.length) {
|
||||||
|
if (id[i] == '0') {
|
||||||
|
rank += 4
|
||||||
|
} else if (id[i] in '4'..'7') {
|
||||||
|
rank += 1
|
||||||
|
break
|
||||||
|
} else if (id[i] in '2'..'3') {
|
||||||
|
rank += 2
|
||||||
|
break
|
||||||
|
} else if (id[i] == '1') {
|
||||||
|
rank += 3
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rank
|
||||||
|
}
|
||||||
|
|
||||||
override fun getReward(): BigDecimal? {
|
override fun getReward(): BigDecimal? {
|
||||||
return try {
|
return try {
|
||||||
tags.filter { it.firstOrNull() == "reward" }.mapNotNull { BigDecimal(it.getOrNull(1)) }.firstOrNull()
|
tags.filter { it.firstOrNull() == "reward" }.mapNotNull { BigDecimal(it.getOrNull(1)) }.firstOrNull()
|
||||||
|
|||||||
@@ -32,4 +32,5 @@ interface EventInterface {
|
|||||||
fun hashtags(): List<String>
|
fun hashtags(): List<String>
|
||||||
|
|
||||||
fun getReward(): BigDecimal?
|
fun getReward(): BigDecimal?
|
||||||
|
fun getPoWRank(): Int
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,8 @@ class Relay(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onMessage(webSocket: WebSocket, text: String) {
|
override fun onMessage(webSocket: WebSocket, text: String) {
|
||||||
|
eventDownloadCounterInBytes += text.bytesUsedInMemory()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val msg = Event.gson.fromJson(text, JsonElement::class.java).asJsonArray
|
val msg = Event.gson.fromJson(text, JsonElement::class.java).asJsonArray
|
||||||
val type = msg[0].asString
|
val type = msg[0].asString
|
||||||
@@ -90,14 +92,7 @@ class Relay(
|
|||||||
when (type) {
|
when (type) {
|
||||||
"EVENT" -> {
|
"EVENT" -> {
|
||||||
// Log.w("Relay", "Relay onEVENT $url, $channel")
|
// Log.w("Relay", "Relay onEVENT $url, $channel")
|
||||||
eventDownloadCounterInBytes += text.bytesUsedInMemory()
|
listeners.forEach { it.onEvent(this@Relay, channel, Event.fromJson(msg[2], Client.lenient)) }
|
||||||
val event = Event.fromJson(msg[2], Client.lenient)
|
|
||||||
|
|
||||||
if (event.kind == 23195 || event.kind == 23196) {
|
|
||||||
println("AAAAA ${event.toJson()}")
|
|
||||||
}
|
|
||||||
|
|
||||||
listeners.forEach { it.onEvent(this@Relay, channel, event) }
|
|
||||||
}
|
}
|
||||||
"EOSE" -> listeners.forEach {
|
"EOSE" -> listeners.forEach {
|
||||||
// Log.w("Relay", "Relay onEOSE $url, $channel")
|
// Log.w("Relay", "Relay onEOSE $url, $channel")
|
||||||
@@ -105,7 +100,6 @@ class Relay(
|
|||||||
}
|
}
|
||||||
"NOTICE" -> listeners.forEach {
|
"NOTICE" -> listeners.forEach {
|
||||||
// Log.w("Relay", "Relay onNotice $url, $channel")
|
// Log.w("Relay", "Relay onNotice $url, $channel")
|
||||||
// "channel" being the second string in the string array ...
|
|
||||||
it.onError(this@Relay, channel, Error("Relay sent notice: " + channel))
|
it.onError(this@Relay, channel, Error("Relay sent notice: " + channel))
|
||||||
}
|
}
|
||||||
"OK" -> listeners.forEach {
|
"OK" -> listeners.forEach {
|
||||||
|
|||||||
+7
-6
@@ -43,8 +43,10 @@ fun ExpandableRichTextViewer(
|
|||||||
var showFullText by remember { mutableStateOf(false) }
|
var showFullText by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// Cuts the text in the first space after 350
|
// Cuts the text in the first space after 350
|
||||||
val firstSpaceAfterCut = content.indexOf(' ', SHORT_TEXT_LENGTH)
|
val firstSpaceAfterCut = content.indexOf(' ', SHORT_TEXT_LENGTH).let { if (it < 0) content.length else it }
|
||||||
val whereToCut = if (firstSpaceAfterCut < 0) SHORT_TEXT_LENGTH else firstSpaceAfterCut
|
val firstNewLineAfterCut = content.indexOf('\n', SHORT_TEXT_LENGTH).let { if (it < 0) content.length else it }
|
||||||
|
|
||||||
|
val whereToCut = minOf(firstSpaceAfterCut, firstNewLineAfterCut)
|
||||||
|
|
||||||
val text = if (showFullText) {
|
val text = if (showFullText) {
|
||||||
content
|
content
|
||||||
@@ -52,24 +54,23 @@ fun ExpandableRichTextViewer(
|
|||||||
content.take(whereToCut)
|
content.take(whereToCut)
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(contentAlignment = Alignment.BottomCenter) {
|
Box {
|
||||||
// CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
|
|
||||||
RichTextViewer(
|
RichTextViewer(
|
||||||
text,
|
text,
|
||||||
canPreview,
|
canPreview,
|
||||||
modifier,
|
modifier.align(Alignment.TopStart),
|
||||||
tags,
|
tags,
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
accountViewModel,
|
accountViewModel,
|
||||||
navController
|
navController
|
||||||
)
|
)
|
||||||
// }
|
|
||||||
|
|
||||||
if (content.length > whereToCut && !showFullText) {
|
if (content.length > whereToCut && !showFullText) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(
|
.background(
|
||||||
brush = Brush.verticalGradient(
|
brush = Brush.verticalGradient(
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import android.content.Intent
|
|||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.CutCornerShape
|
import androidx.compose.foundation.shape.CutCornerShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@@ -13,7 +15,6 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.filled.Bolt
|
import androidx.compose.material.icons.filled.Bolt
|
||||||
import androidx.compose.material.icons.filled.ExpandMore
|
import androidx.compose.material.icons.filled.ExpandMore
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.material.icons.outlined.Bolt
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -36,6 +37,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import androidx.core.graphics.get
|
import androidx.core.graphics.get
|
||||||
@@ -74,6 +76,7 @@ import com.vitorpamplona.amethyst.ui.theme.Following
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
import kotlin.math.ceil
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -308,6 +311,11 @@ fun NoteCompose(
|
|||||||
if (baseReward != null) {
|
if (baseReward != null) {
|
||||||
DisplayReward(baseReward, baseNote, account, navController)
|
DisplayReward(baseReward, baseNote, account, navController)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val pow = noteEvent.getPoWRank()
|
||||||
|
if (pow > 1) {
|
||||||
|
DisplayPoW(pow)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -567,6 +575,20 @@ fun DisplayUncitedHashtags(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DisplayPoW(
|
||||||
|
pow: Int
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"PoW-$pow",
|
||||||
|
color = MaterialTheme.colors.primary.copy(
|
||||||
|
alpha = 0.52f
|
||||||
|
),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DisplayReward(
|
fun DisplayReward(
|
||||||
baseReward: BigDecimal,
|
baseReward: BigDecimal,
|
||||||
@@ -797,28 +819,35 @@ private fun RelayBadges(baseNote: Note) {
|
|||||||
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val relaysToDisplay = if (expanded) noteRelays else noteRelays.take(3)
|
val relaysToDisplay = (if (expanded) noteRelays else noteRelays.take(3)).toList()
|
||||||
|
val height = (ceil(relaysToDisplay.size / 3.0f) * 17).dp
|
||||||
|
|
||||||
val uri = LocalUriHandler.current
|
val uri = LocalUriHandler.current
|
||||||
|
|
||||||
FlowRow(Modifier.padding(top = 10.dp, start = 5.dp, end = 4.dp)) {
|
Spacer(Modifier.height(10.dp))
|
||||||
relaysToDisplay.forEach {
|
|
||||||
val url = it.removePrefix("wss://").removePrefix("ws://")
|
LazyVerticalGrid(
|
||||||
Box(
|
columns = GridCells.Fixed(3),
|
||||||
Modifier
|
contentPadding = PaddingValues(start = 4.dp, end = 4.dp),
|
||||||
.size(15.dp)
|
modifier = Modifier.height(height),
|
||||||
.padding(1.dp)
|
userScrollEnabled = false
|
||||||
) {
|
) {
|
||||||
|
items(relaysToDisplay.size) {
|
||||||
|
val url = relaysToDisplay[it].removePrefix("wss://").removePrefix("ws://")
|
||||||
|
|
||||||
|
Box(Modifier.padding(1.dp).size(15.dp)) {
|
||||||
RobohashFallbackAsyncImage(
|
RobohashFallbackAsyncImage(
|
||||||
robot = "https://$url/favicon.ico",
|
robot = "https://$url/favicon.ico",
|
||||||
model = "https://$url/favicon.ico",
|
model = "https://$url/favicon.ico",
|
||||||
contentDescription = stringResource(R.string.relay_icon),
|
contentDescription = stringResource(R.string.relay_icon),
|
||||||
colorFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) }),
|
colorFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) }),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize(1f)
|
.width(13.dp)
|
||||||
|
.height(13.dp)
|
||||||
.clip(shape = CircleShape)
|
.clip(shape = CircleShape)
|
||||||
|
// .border(1.dp, Color.Red)
|
||||||
.background(MaterialTheme.colors.background)
|
.background(MaterialTheme.colors.background)
|
||||||
.clickable(onClick = { uri.openUri("https://" + url) })
|
.clickable(onClick = { uri.openUri("https://$url") })
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.note
|
package com.vitorpamplona.amethyst.ui.note
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.KeyguardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.biometric.BiometricManager
|
||||||
|
import androidx.biometric.BiometricPrompt
|
||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -12,8 +23,10 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.Button
|
import androidx.compose.material.Button
|
||||||
import androidx.compose.material.ButtonDefaults
|
import androidx.compose.material.ButtonDefaults
|
||||||
import androidx.compose.material.Divider
|
import androidx.compose.material.Divider
|
||||||
@@ -23,12 +36,14 @@ import androidx.compose.material.MaterialTheme
|
|||||||
import androidx.compose.material.OutlinedTextField
|
import androidx.compose.material.OutlinedTextField
|
||||||
import androidx.compose.material.Surface
|
import androidx.compose.material.Surface
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Visibility
|
||||||
|
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -36,9 +51,12 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@@ -48,11 +66,15 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.service.model.Contact
|
import com.vitorpamplona.amethyst.model.decodePublicKey
|
||||||
|
import com.vitorpamplona.amethyst.model.toHexKey
|
||||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||||
import com.vitorpamplona.amethyst.ui.actions.SaveButton
|
import com.vitorpamplona.amethyst.ui.actions.SaveButton
|
||||||
import com.vitorpamplona.amethyst.ui.qrcode.SimpleQrCodeScanner
|
import com.vitorpamplona.amethyst.ui.qrcode.SimpleQrCodeScanner
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.getFragmentActivity
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope as rememberCoroutineScope
|
||||||
|
|
||||||
class UpdateZapAmountViewModel : ViewModel() {
|
class UpdateZapAmountViewModel : ViewModel() {
|
||||||
private var account: Account? = null
|
private var account: Account? = null
|
||||||
@@ -61,12 +83,14 @@ class UpdateZapAmountViewModel : ViewModel() {
|
|||||||
var amountSet by mutableStateOf(listOf<Long>())
|
var amountSet by mutableStateOf(listOf<Long>())
|
||||||
var walletConnectRelay by mutableStateOf(TextFieldValue(""))
|
var walletConnectRelay by mutableStateOf(TextFieldValue(""))
|
||||||
var walletConnectPubkey by mutableStateOf(TextFieldValue(""))
|
var walletConnectPubkey by mutableStateOf(TextFieldValue(""))
|
||||||
|
var walletConnectSecret by mutableStateOf(TextFieldValue(""))
|
||||||
|
|
||||||
fun load(account: Account) {
|
fun load(account: Account) {
|
||||||
this.account = account
|
this.account = account
|
||||||
this.amountSet = account.zapAmountChoices
|
this.amountSet = account.zapAmountChoices
|
||||||
this.walletConnectPubkey = account.zapPaymentRequest?.pubKeyHex?.let { TextFieldValue(it) } ?: TextFieldValue("")
|
this.walletConnectPubkey = account.zapPaymentRequest?.pubKeyHex?.let { TextFieldValue(it) } ?: TextFieldValue("")
|
||||||
this.walletConnectRelay = account.zapPaymentRequest?.relayUri?.let { TextFieldValue(it) } ?: TextFieldValue("")
|
this.walletConnectRelay = account.zapPaymentRequest?.relayUri?.let { TextFieldValue(it) } ?: TextFieldValue("")
|
||||||
|
this.walletConnectSecret = account.zapPaymentRequest?.secret?.let { TextFieldValue(it) } ?: TextFieldValue("")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toListOfAmounts(commaSeparatedAmounts: String): List<Long> {
|
fun toListOfAmounts(commaSeparatedAmounts: String): List<Long> {
|
||||||
@@ -90,7 +114,20 @@ class UpdateZapAmountViewModel : ViewModel() {
|
|||||||
account?.changeZapAmounts(amountSet)
|
account?.changeZapAmounts(amountSet)
|
||||||
|
|
||||||
if (walletConnectRelay.text.isNotBlank() && walletConnectPubkey.text.isNotBlank()) {
|
if (walletConnectRelay.text.isNotBlank() && walletConnectPubkey.text.isNotBlank()) {
|
||||||
account?.changeZapPaymentRequest(Contact(walletConnectPubkey.text, walletConnectRelay.text))
|
val unverifiedPrivKey = walletConnectSecret.text.ifBlank { null }
|
||||||
|
val privKey = try {
|
||||||
|
unverifiedPrivKey?.let { decodePublicKey(it).toHexKey() }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
account?.changeZapPaymentRequest(
|
||||||
|
Nip47URI(
|
||||||
|
walletConnectPubkey.text,
|
||||||
|
walletConnectRelay.text.ifBlank { null },
|
||||||
|
privKey
|
||||||
|
)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
account?.changeZapPaymentRequest(null)
|
account?.changeZapPaymentRequest(null)
|
||||||
}
|
}
|
||||||
@@ -106,7 +143,8 @@ class UpdateZapAmountViewModel : ViewModel() {
|
|||||||
return (
|
return (
|
||||||
amountSet != account?.zapAmountChoices ||
|
amountSet != account?.zapAmountChoices ||
|
||||||
walletConnectPubkey.text != (account?.zapPaymentRequest?.pubKeyHex ?: "") ||
|
walletConnectPubkey.text != (account?.zapPaymentRequest?.pubKeyHex ?: "") ||
|
||||||
walletConnectRelay.text != (account?.zapPaymentRequest?.relayUri ?: "")
|
walletConnectRelay.text != (account?.zapPaymentRequest?.relayUri ?: "") ||
|
||||||
|
walletConnectSecret.text != (account?.zapPaymentRequest?.secret ?: "")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,6 +190,9 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account) {
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
Column(modifier = Modifier.animateContentSize()) {
|
Column(modifier = Modifier.animateContentSize()) {
|
||||||
FlowRow(
|
FlowRow(
|
||||||
@@ -170,7 +211,11 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account) {
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
"⚡ ${showAmount(amountInSats.toBigDecimal().setScale(1))} ✖",
|
"⚡ ${
|
||||||
|
showAmount(
|
||||||
|
amountInSats.toBigDecimal().setScale(1)
|
||||||
|
)
|
||||||
|
} ✖",
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
@@ -232,7 +277,10 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account) {
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(stringResource(id = R.string.wallet_connect_service), Modifier.weight(1f))
|
Text(
|
||||||
|
stringResource(id = R.string.wallet_connect_service),
|
||||||
|
Modifier.weight(1f)
|
||||||
|
)
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
qrScanning = true
|
qrScanning = true
|
||||||
}) {
|
}) {
|
||||||
@@ -266,12 +314,17 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account) {
|
|||||||
try {
|
try {
|
||||||
val contact = Nip47.parse(it)
|
val contact = Nip47.parse(it)
|
||||||
if (contact != null) {
|
if (contact != null) {
|
||||||
postViewModel.walletConnectPubkey = TextFieldValue(contact.pubKeyHex)
|
postViewModel.walletConnectPubkey =
|
||||||
postViewModel.walletConnectRelay = TextFieldValue(contact.relayUri ?: "")
|
TextFieldValue(contact.pubKeyHex)
|
||||||
|
postViewModel.walletConnectRelay =
|
||||||
|
TextFieldValue(contact.relayUri ?: "")
|
||||||
|
postViewModel.walletConnectSecret =
|
||||||
|
TextFieldValue(contact.secret ?: "")
|
||||||
}
|
}
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, e.message, Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -325,7 +378,156 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account) {
|
|||||||
singleLine = true
|
singleLine = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var showPassword by remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val keyguardLauncher =
|
||||||
|
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
showPassword = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val authTitle = stringResource(id = R.string.wallet_connect_service_show_secret)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 5.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
label = { Text(text = stringResource(R.string.wallet_connect_service_secret)) },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
value = postViewModel.walletConnectSecret,
|
||||||
|
onValueChange = { postViewModel.walletConnectSecret = it },
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
autoCorrect = false,
|
||||||
|
keyboardType = KeyboardType.Password,
|
||||||
|
imeAction = ImeAction.Go
|
||||||
|
),
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.wallet_connect_service_secret_placeholder),
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = {
|
||||||
|
if (!showPassword) {
|
||||||
|
authenticate(authTitle, context, scope, keyguardLauncher) {
|
||||||
|
showPassword = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showPassword = false
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (showPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
|
||||||
|
contentDescription = if (showPassword) {
|
||||||
|
stringResource(R.string.show_password)
|
||||||
|
} else {
|
||||||
|
stringResource(
|
||||||
|
R.string.hide_password
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun authenticate(
|
||||||
|
title: String,
|
||||||
|
context: Context,
|
||||||
|
scope: CoroutineScope,
|
||||||
|
keyguardLauncher: ManagedActivityResultLauncher<Intent, ActivityResult>,
|
||||||
|
onApproved: () -> Unit
|
||||||
|
) {
|
||||||
|
val fragmentContext = context.getFragmentActivity()!!
|
||||||
|
val keyguardManager =
|
||||||
|
fragmentContext.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||||
|
|
||||||
|
if (!keyguardManager.isDeviceSecure) {
|
||||||
|
onApproved()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
fun keyguardPrompt() {
|
||||||
|
val intent = keyguardManager.createConfirmDeviceCredentialIntent(
|
||||||
|
context.getString(R.string.app_name_release),
|
||||||
|
title
|
||||||
|
)
|
||||||
|
|
||||||
|
keyguardLauncher.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
|
||||||
|
keyguardPrompt()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val biometricManager = BiometricManager.from(context)
|
||||||
|
val authenticators = BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||||
|
|
||||||
|
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||||
|
.setTitle(context.getString(R.string.app_name_release))
|
||||||
|
.setSubtitle(title)
|
||||||
|
.setAllowedAuthenticators(authenticators)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val biometricPrompt = BiometricPrompt(
|
||||||
|
fragmentContext,
|
||||||
|
object : BiometricPrompt.AuthenticationCallback() {
|
||||||
|
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||||
|
super.onAuthenticationError(errorCode, errString)
|
||||||
|
|
||||||
|
when (errorCode) {
|
||||||
|
BiometricPrompt.ERROR_NEGATIVE_BUTTON -> keyguardPrompt()
|
||||||
|
BiometricPrompt.ERROR_LOCKOUT -> keyguardPrompt()
|
||||||
|
else ->
|
||||||
|
scope.launch {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
"${context.getString(R.string.biometric_error)}: $errString",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthenticationFailed() {
|
||||||
|
super.onAuthenticationFailed()
|
||||||
|
scope.launch {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
context.getString(R.string.biometric_authentication_failed),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||||
|
super.onAuthenticationSucceeded(result)
|
||||||
|
onApproved()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
when (biometricManager.canAuthenticate(authenticators)) {
|
||||||
|
BiometricManager.BIOMETRIC_SUCCESS -> biometricPrompt.authenticate(promptInfo)
|
||||||
|
else -> keyguardPrompt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -70,14 +69,6 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
|
|||||||
verticalArrangement = Arrangement.SpaceBetween
|
verticalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
if (presenting) {
|
if (presenting) {
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 30.dp, vertical = 10.dp)
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) {
|
Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) {
|
||||||
RobohashAsyncImageProxy(
|
RobohashAsyncImageProxy(
|
||||||
@@ -95,14 +86,11 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
|
|||||||
Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) {
|
Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) {
|
||||||
Text(
|
Text(
|
||||||
user.bestDisplayName() ?: "",
|
user.bestDisplayName() ?: "",
|
||||||
modifier = Modifier.padding(top = 7.dp),
|
modifier = Modifier.padding(top = 5.dp),
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = 18.sp
|
fontSize = 18.sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) {
|
|
||||||
Text(" @${user.bestUsername()}", color = Color.LightGray)
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
|||||||
+9
-93
@@ -1,20 +1,12 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.KeyguardManager
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.biometric.BiometricManager
|
|
||||||
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
|
|
||||||
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
|
||||||
import androidx.biometric.BiometricPrompt
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -54,6 +46,7 @@ import com.halilibo.richtext.ui.resolveDefaults
|
|||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.authenticate
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import nostr.postr.toNsec
|
import nostr.postr.toNsec
|
||||||
@@ -122,7 +115,14 @@ private fun NSecCopyButton(
|
|||||||
Button(
|
Button(
|
||||||
modifier = Modifier.padding(horizontal = 3.dp),
|
modifier = Modifier.padding(horizontal = 3.dp),
|
||||||
onClick = {
|
onClick = {
|
||||||
authenticatedCopyNSec(context, scope, account, clipboardManager, keyguardLauncher)
|
authenticate(
|
||||||
|
title = context.getString(R.string.copy_my_secret_key),
|
||||||
|
context = context,
|
||||||
|
scope = scope,
|
||||||
|
keyguardLauncher = keyguardLauncher
|
||||||
|
) {
|
||||||
|
copyNSec(context, scope, account, clipboardManager)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
shape = RoundedCornerShape(20.dp),
|
shape = RoundedCornerShape(20.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
@@ -151,90 +151,6 @@ fun Context.getFragmentActivity(): FragmentActivity? {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun authenticatedCopyNSec(
|
|
||||||
context: Context,
|
|
||||||
scope: CoroutineScope,
|
|
||||||
account: Account,
|
|
||||||
clipboardManager: ClipboardManager,
|
|
||||||
keyguardLauncher: ManagedActivityResultLauncher<Intent, ActivityResult>
|
|
||||||
) {
|
|
||||||
val fragmentContext = context.getFragmentActivity()!!
|
|
||||||
val keyguardManager =
|
|
||||||
fragmentContext.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
|
||||||
|
|
||||||
if (!keyguardManager.isDeviceSecure) {
|
|
||||||
copyNSec(context, scope, account, clipboardManager)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
fun keyguardPrompt() {
|
|
||||||
val intent = keyguardManager.createConfirmDeviceCredentialIntent(
|
|
||||||
context.getString(R.string.app_name_release),
|
|
||||||
context.getString(R.string.copy_my_secret_key)
|
|
||||||
)
|
|
||||||
|
|
||||||
keyguardLauncher.launch(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
|
|
||||||
keyguardPrompt()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val biometricManager = BiometricManager.from(context)
|
|
||||||
val authenticators = BIOMETRIC_STRONG or DEVICE_CREDENTIAL
|
|
||||||
|
|
||||||
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
|
||||||
.setTitle(context.getString(R.string.app_name_release))
|
|
||||||
.setSubtitle(context.getString(R.string.copy_my_secret_key))
|
|
||||||
.setAllowedAuthenticators(authenticators)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val biometricPrompt = BiometricPrompt(
|
|
||||||
fragmentContext,
|
|
||||||
object : BiometricPrompt.AuthenticationCallback() {
|
|
||||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
|
||||||
super.onAuthenticationError(errorCode, errString)
|
|
||||||
|
|
||||||
when (errorCode) {
|
|
||||||
BiometricPrompt.ERROR_NEGATIVE_BUTTON -> keyguardPrompt()
|
|
||||||
BiometricPrompt.ERROR_LOCKOUT -> keyguardPrompt()
|
|
||||||
else ->
|
|
||||||
scope.launch {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
"${context.getString(R.string.biometric_error)}: $errString",
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAuthenticationFailed() {
|
|
||||||
super.onAuthenticationFailed()
|
|
||||||
scope.launch {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
context.getString(R.string.biometric_authentication_failed),
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
|
||||||
super.onAuthenticationSucceeded(result)
|
|
||||||
copyNSec(context, scope, account, clipboardManager)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
when (biometricManager.canAuthenticate(authenticators)) {
|
|
||||||
BiometricManager.BIOMETRIC_SUCCESS -> biometricPrompt.authenticate(promptInfo)
|
|
||||||
else -> keyguardPrompt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyNSec(
|
private fun copyNSec(
|
||||||
context: Context,
|
context: Context,
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class AccountViewModel(private val account: Account) : ViewModel() {
|
|||||||
|
|
||||||
// Awaits for the event to come back to LocalCache.
|
// Awaits for the event to come back to LocalCache.
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
delay(1000)
|
delay(5000)
|
||||||
onProgress(0f)
|
onProgress(0f)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -259,9 +259,12 @@
|
|||||||
<string name="remove_from_public_bookmarks">Remove from Public Bookmarks</string>
|
<string name="remove_from_public_bookmarks">Remove from Public Bookmarks</string>
|
||||||
|
|
||||||
<string name="wallet_connect_service">Wallet Connect Service</string>
|
<string name="wallet_connect_service">Wallet Connect Service</string>
|
||||||
<string name="wallet_connect_service_explainer">Uses your private key to pay zaps without leaving the app. Anyone with access to your Nostr private key will be able to spend your wallet\'s balance. Only keep funds you are ok to lose and use a private relay if possible. The relay operator can see your payments metadata.</string>
|
<string name="wallet_connect_service_explainer">Authorizes a Nostr Secret to pay zaps without leaving the app. Keep the secret safe and use a private relay if possible</string>
|
||||||
<string name="wallet_connect_service_pubkey">Wallet Connect Pubkey</string>
|
<string name="wallet_connect_service_pubkey">Wallet Connect Pubkey</string>
|
||||||
<string name="wallet_connect_service_relay">Wallet Connect Relay</string>
|
<string name="wallet_connect_service_relay">Wallet Connect Relay</string>
|
||||||
|
<string name="wallet_connect_service_secret">Wallet Connect Secret</string>
|
||||||
|
<string name="wallet_connect_service_show_secret">Show secret key</string>
|
||||||
|
<string name="wallet_connect_service_secret_placeholder">nsec / hex private key</string>
|
||||||
|
|
||||||
<string name="pledge_amount_in_sats">Pledge Amount in Sats</string>
|
<string name="pledge_amount_in_sats">Pledge Amount in Sats</string>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user