Files
Amber/app/src/main/java/com/greenart7c3/nostrsigner/MainViewModel.kt
T
greenart7c3andClaude Opus 5.5 54f9e2eebd Detect web requests from browsers other than Chrome
IntentUtils only treated a nostrsigner: request as coming from a web page
when Chrome's Browser.EXTRA_APPLICATION_ID extra was present. Browsers like
DuckDuckGo don't set it, so their URL-encoded requests were parsed as intent
extras and failed with "Unknown signer type".

The request is now parsed from the URI when Chrome's extra is present, or,
if there is no native `type` extra, when the intent has the BROWSABLE
category, the referrer is a browser (http(s) referrer or a package that
handles https links), or the URI carries a `type=` query parameter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:49:18 -03:00

172 lines
7.2 KiB
Kotlin

package com.greenart7c3.nostrsigner
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController
import com.greenart7c3.nostrsigner.service.BunkerRequestUtils
import com.greenart7c3.nostrsigner.service.IntentUtils
import com.greenart7c3.nostrsigner.ui.AccountStateViewModel
import com.greenart7c3.nostrsigner.ui.navigation.Route
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
import com.vitorpamplona.quartz.nip19Bech32.toNpub
import com.vitorpamplona.quartz.utils.Hex
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Stable
@SuppressLint("StaticFieldLeak")
class MainViewModel(val context: Context) : ViewModel() {
var navController: NavHostController? = null
// Intent IDs added by this ViewModel instance — used by onDestroy to remove only
// the intents owned by this activity, avoiding races with a new SignerActivity
// instance that may have already added its own intents.
val addedIntentIds = mutableSetOf<String>()
fun getAccount(userFromIntent: String?): String? {
val currentAccount = LocalPreferences.currentAccount(context)
try {
if (!userFromIntent.isNullOrBlank()) {
if (userFromIntent.startsWith("npub")) {
if (LocalPreferences.containsAccount(context, userFromIntent)) {
AmberLog.d(Amber.TAG, "getAccount: $userFromIntent")
return userFromIntent
}
} else {
val localNpub = Hex.decode(userFromIntent).toNpub()
if (LocalPreferences.containsAccount(context, localNpub)) {
AmberLog.d(Amber.TAG, "getAccount: $localNpub")
return localNpub
}
}
}
val pubKeys =
IntentUtils.intents.value.mapNotNull {
it.event?.pubKey
}.filter { it.isNotBlank() } + BunkerRequestUtils.getBunkerRequests().mapNotNull {
when (val parsed = Nip19Parser.uriToRoute(it.currentAccount)?.entity) {
is NPub -> parsed.hex
else -> null
}
}
if (pubKeys.isEmpty()) {
if (currentAccount != null && LocalPreferences.containsAccount(context, currentAccount)) {
AmberLog.d(Amber.TAG, "getAccount: $currentAccount")
return currentAccount
}
val acc = LocalPreferences.allSavedAccounts(context).firstOrNull()
if (acc != null) {
AmberLog.d(Amber.TAG, "getAccount: ${acc.npub}")
return acc.npub
} else {
AmberLog.d(Amber.TAG, "getAccount: null")
return null
}
}
val npub = Hex.decode(pubKeys.first()).toNpub()
AmberLog.d(Amber.TAG, "getAccount: $npub")
return npub
} catch (e: Exception) {
AmberLog.e(Amber.TAG, "Error getting account", e)
if (currentAccount != null && LocalPreferences.containsAccount(context, currentAccount)) {
AmberLog.d(Amber.TAG, "getAccount: $currentAccount")
return currentAccount
}
val acc = LocalPreferences.allSavedAccounts(context).firstOrNull()
if (acc != null) {
AmberLog.d(Amber.TAG, "getAccount: ${acc.npub}")
return acc.npub
} else {
AmberLog.d(Amber.TAG, "getAccount: null")
return null
}
}
}
fun showBunkerRequests(accountStateViewModel: AccountStateViewModel? = null) {
val requests =
BunkerRequestUtils.getBunkerRequests().map {
it.copy()
}
viewModelScope.launch(Dispatchers.IO) {
if (requests.isNotEmpty()) {
val npub = getAccount(null)
val currentAccount = LocalPreferences.currentAccount(context)
if (currentAccount != null && npub != null && currentAccount != npub && npub.isNotBlank()) {
if (npub.startsWith("npub")) {
AmberLog.d(Amber.TAG, "Switching account to $npub")
if (LocalPreferences.containsAccount(context, npub)) {
accountStateViewModel?.switchUser(npub, Route.IncomingRequest.route)
}
} else {
val localNpub = Hex.decode(npub).toNpub()
AmberLog.d(Amber.TAG, "Switching account to $localNpub")
if (LocalPreferences.containsAccount(context, localNpub)) {
accountStateViewModel?.switchUser(localNpub, Route.IncomingRequest.route)
}
}
}
}
}
}
fun onNewIntent(
intent: Intent,
callingPackage: String?,
referrer: Uri? = null,
) {
viewModelScope.launch(Dispatchers.IO) {
val account = LocalPreferences.loadFromEncryptedStorage(context)
account?.let { acc ->
val intentData = IntentUtils.getIntentData(context, intent, callingPackage, intent.getStringExtra("route"), acc, referrer)
if (intentData != null) {
IntentUtils.addAll(listOf(intentData))
addedIntentIds.add(intentData.id)
// The calling app is visible to us now; capture its icon/name.
IntentUtils.persistNativeAppMetadata(context, acc, callingPackage)
}
intent.getStringExtra("route")?.let { route ->
viewModelScope.launch(Dispatchers.Main) {
var error = true
var count = 0
while (error && count < 10) {
delay(100.milliseconds)
count++
try {
if (route == Route.UpdateSettings.route) {
navController?.navigate(Route.Settings.route) {
popUpTo(0)
}
navController?.navigate(Route.UpdateSettings.route)
} else {
navController?.navigate(route) {
popUpTo(0)
}
}
error = false
} catch (e: Exception) {
AmberLog.e(Amber.TAG, "Error navigating to $route", e)
}
}
}
}
}
}
}
}