Rewrites NIP-55 to start offering better error handling.
This commit is contained in:
@@ -37,6 +37,7 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.utils.sha256.sha256
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.fail
|
||||
@@ -55,9 +56,12 @@ import kotlin.random.Random
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ImageUploadTesting {
|
||||
companion object {
|
||||
val accountSettings = AccountSettings(KeyPair())
|
||||
|
||||
val account =
|
||||
Account(
|
||||
AccountSettings(KeyPair()),
|
||||
settings = accountSettings,
|
||||
signer = NostrSignerInternal(accountSettings.keyPair),
|
||||
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
|
||||
)
|
||||
}
|
||||
|
||||
+8
-1
@@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal.ThreadFeedFi
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.verify
|
||||
import junit.framework.TestCase
|
||||
@@ -138,7 +139,13 @@ class ThreadDualAxisChartAssemblerTest {
|
||||
null,
|
||||
)
|
||||
|
||||
val account = Account(AccountSettings(KeyPair()), scope = CoroutineScope(Dispatchers.IO + SupervisorJob()))
|
||||
val keyPair = KeyPair()
|
||||
val account =
|
||||
Account(
|
||||
settings = AccountSettings(keyPair = keyPair),
|
||||
signer = NostrSignerInternal(keyPair),
|
||||
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
|
||||
)
|
||||
withContext(Dispatchers.Main) {
|
||||
val user = account.userProfile().flow()
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ import kotlin.coroutines.resume
|
||||
@Stable
|
||||
class Account(
|
||||
val settings: AccountSettings = AccountSettings(KeyPair()),
|
||||
val signer: NostrSigner = settings.createSigner(),
|
||||
val signer: NostrSigner,
|
||||
geolocationFlow: StateFlow<LocationState.LocationResult>,
|
||||
val cache: LocalCache,
|
||||
val client: NostrClient,
|
||||
|
||||
@@ -20,9 +20,8 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import android.util.Log
|
||||
import android.content.ContentResolver
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS
|
||||
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedDefinition
|
||||
@@ -39,6 +38,8 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent
|
||||
@@ -46,8 +47,9 @@ import com.vitorpamplona.quartz.nip51Lists.MuteListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.ExternalSignerLauncher
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.NostrSignerExternal
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.Permission
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.client.NostrSignerExternal
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo
|
||||
@@ -61,14 +63,6 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import java.util.Locale
|
||||
|
||||
val DefaultChannelSet =
|
||||
setOf(
|
||||
// Anigma's Nostr
|
||||
"25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb",
|
||||
// Amethyst's Group
|
||||
"42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5",
|
||||
)
|
||||
|
||||
val DefaultChannels =
|
||||
listOf(
|
||||
// Anigma's Nostr
|
||||
@@ -90,6 +84,17 @@ val DefaultDMRelayList = listOf(Constants.auth, Constants.oxchat, Constants.nos)
|
||||
|
||||
val DefaultSearchRelayList = setOf(Constants.band, Constants.wine, Constants.where, Constants.nostoday)
|
||||
|
||||
val DefaultSignerPermissions =
|
||||
listOf(
|
||||
Permission(CommandType.SIGN_EVENT, RelayAuthEvent.KIND),
|
||||
Permission(CommandType.SIGN_EVENT, DraftEvent.KIND),
|
||||
Permission(CommandType.NIP04_ENCRYPT),
|
||||
Permission(CommandType.NIP04_DECRYPT),
|
||||
Permission(CommandType.NIP44_DECRYPT),
|
||||
Permission(CommandType.NIP44_DECRYPT),
|
||||
Permission(CommandType.DECRYPT_ZAP_EVENT),
|
||||
)
|
||||
|
||||
// This has spaces to avoid mixing with a potential NIP-51 list with the same name.
|
||||
val GLOBAL_FOLLOWS = " Global "
|
||||
|
||||
@@ -147,25 +152,18 @@ class AccountSettings(
|
||||
|
||||
fun isWriteable(): Boolean = keyPair.privKey != null || externalSignerPackageName != null
|
||||
|
||||
fun createSigner() =
|
||||
fun createSigner(contentResolver: ContentResolver) =
|
||||
if (keyPair.privKey != null) {
|
||||
NostrSignerInternal(keyPair)
|
||||
} else {
|
||||
when (val packageName = externalSignerPackageName) {
|
||||
null -> NostrSignerInternal(keyPair)
|
||||
else -> {
|
||||
val externalSignerLauncher = ExternalSignerLauncher(keyPair.pubKey.toHexKey(), packageName)
|
||||
// TODO: How to handle the launcher here?
|
||||
try {
|
||||
externalSignerLauncher.registerLauncher(
|
||||
launcher = { },
|
||||
contentResolver = Amethyst.instance::contentResolverFn,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.d("AccountSettings", "Failed to initialize external signer", e)
|
||||
}
|
||||
NostrSignerExternal(keyPair.pubKey.toHexKey(), externalSignerLauncher)
|
||||
}
|
||||
else ->
|
||||
NostrSignerExternal(
|
||||
pubKey = keyPair.pubKey.toHexKey(),
|
||||
packageName = packageName,
|
||||
contentResolver = contentResolver,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,6 @@ package com.vitorpamplona.amethyst.service
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.net.toUri
|
||||
|
||||
object PackageUtils {
|
||||
@SuppressLint("QueryPermissionsNeeded")
|
||||
@@ -36,14 +34,4 @@ object PackageUtils {
|
||||
} != null
|
||||
|
||||
fun isOrbotInstalled(context: Context): Boolean = isPackageInstalled(context, "org.torproject.android")
|
||||
|
||||
fun isExternalSignerInstalled(context: Context): Boolean {
|
||||
val intent =
|
||||
Intent().apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
data = "nostrsigner:".toUri()
|
||||
}
|
||||
val infos = context.packageManager.queryIntentActivities(intent, 0)
|
||||
return infos.size > 0
|
||||
}
|
||||
}
|
||||
|
||||
+2
-21
@@ -24,7 +24,6 @@ import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
@@ -44,7 +43,6 @@ import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.toNpub
|
||||
import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftEvent
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.NostrSignerExternal
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent
|
||||
@@ -86,15 +84,7 @@ class EventNotificationConsumer(
|
||||
pushWrappedEvent: GiftWrapEvent,
|
||||
account: AccountSettings,
|
||||
) {
|
||||
// TODO: Modify the external launcher to launch as different users.
|
||||
// Right now it only registers if Amber has already approved this signature
|
||||
val signer = account.createSigner()
|
||||
if (signer is NostrSignerExternal) {
|
||||
signer.launcher.registerLauncher(
|
||||
launcher = { },
|
||||
contentResolver = Amethyst.instance::contentResolverFn,
|
||||
)
|
||||
}
|
||||
val signer = account.createSigner(applicationContext.contentResolver)
|
||||
|
||||
pushWrappedEvent.unwrapThrowing(signer) { notificationEvent ->
|
||||
consumeNotificationEvent(notificationEvent, signer, account)
|
||||
@@ -144,16 +134,7 @@ class EventNotificationConsumer(
|
||||
LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub)?.let { acc ->
|
||||
Log.d(TAG, "New Notification Testing if for ${it.npub}")
|
||||
try {
|
||||
// TODO: Modify the external launcher to launch as different users.
|
||||
// Right now it only registers if Amber has already approved this signature
|
||||
val signer = acc.createSigner()
|
||||
if (signer is NostrSignerExternal) {
|
||||
signer.launcher.registerLauncher(
|
||||
launcher = { },
|
||||
contentResolver = Amethyst.instance::contentResolverFn,
|
||||
)
|
||||
}
|
||||
|
||||
val signer = acc.createSigner(applicationContext.contentResolver)
|
||||
consumeNotificationEvent(event, signer, acc)
|
||||
matchAccount = true
|
||||
} catch (e: Exception) {
|
||||
|
||||
+1
-11
@@ -29,7 +29,6 @@ import com.vitorpamplona.amethyst.isDebug
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.NostrSignerExternal
|
||||
import com.vitorpamplona.quartz.utils.mapNotNullAsync
|
||||
import com.vitorpamplona.quartz.utils.tryAndWait
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -63,16 +62,7 @@ class RegisterAccounts(
|
||||
|
||||
return mapNotNullAsync(remainingTos) { info ->
|
||||
tryAndWait { continuation ->
|
||||
val signer = info.accountSettings.createSigner()
|
||||
// TODO: Modify the external launcher to launch as different users.
|
||||
// Right now it only registers if Amber has already approved this signature
|
||||
if (signer is NostrSignerExternal) {
|
||||
signer.launcher.registerLauncher(
|
||||
launcher = { },
|
||||
contentResolver = Amethyst.instance::contentResolverFn,
|
||||
)
|
||||
}
|
||||
|
||||
val signer = info.accountSettings.createSigner(Amethyst.instance.contentResolver)
|
||||
RelayAuthEvent.create(info.relays, notificationToken, signer) { result ->
|
||||
continuation.resume(result)
|
||||
}
|
||||
|
||||
+9
-1
@@ -160,7 +160,15 @@ class AccountViewModel(
|
||||
val app: Amethyst,
|
||||
) : ViewModel(),
|
||||
Dao {
|
||||
val account = Account(accountSettings, accountSettings.createSigner(), app.locationManager.geohashStateFlow, LocalCache, app.client, viewModelScope)
|
||||
val account =
|
||||
Account(
|
||||
accountSettings,
|
||||
accountSettings.createSigner(app.contentResolver),
|
||||
app.locationManager.geohashStateFlow,
|
||||
LocalCache,
|
||||
app.client,
|
||||
viewModelScope,
|
||||
)
|
||||
|
||||
val newNotesPreProcessor = PrecacheNewNotesProcessor(account, LocalCache)
|
||||
|
||||
|
||||
+13
-51
@@ -31,10 +31,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
@@ -44,8 +41,6 @@ import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils
|
||||
import com.vitorpamplona.amethyst.service.relayClient.authCommand.compose.RelayAuthSubscription
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.ui.MainActivity
|
||||
import com.vitorpamplona.amethyst.ui.components.getActivity
|
||||
import com.vitorpamplona.amethyst.ui.navigation.AppNavigation
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||
@@ -54,7 +49,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource.Chat
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.VideoFilterAssemblerSubscription
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.NostrSignerExternal
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.client.IActivityLauncher
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -156,7 +151,10 @@ fun NotificationRegistration(accountViewModel: AccountViewModel) {
|
||||
job?.cancel()
|
||||
job =
|
||||
scope.launch {
|
||||
PushNotificationUtils.checkAndInit(LocalPreferences.allSavedAccounts(), accountViewModel::okHttpClientForTrustedRelays)
|
||||
PushNotificationUtils.checkAndInit(
|
||||
LocalPreferences.allSavedAccounts(),
|
||||
accountViewModel::okHttpClientForTrustedRelays,
|
||||
)
|
||||
}
|
||||
|
||||
onPauseOrDispose {
|
||||
@@ -167,55 +165,25 @@ fun NotificationRegistration(accountViewModel: AccountViewModel) {
|
||||
|
||||
@Composable
|
||||
private fun ListenToExternalSignerIfNeeded(accountViewModel: AccountViewModel) {
|
||||
if (accountViewModel.account.signer is NostrSignerExternal) {
|
||||
val activity = getActivity() as MainActivity
|
||||
|
||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||
if (accountViewModel.account.signer is IActivityLauncher) {
|
||||
val launcher =
|
||||
rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult(),
|
||||
onResult = { result ->
|
||||
if (result.resultCode != Activity.RESULT_OK) {
|
||||
accountViewModel.toastManager.toast(
|
||||
R.string.sign_request_rejected,
|
||||
R.string.sign_request_rejected_description,
|
||||
)
|
||||
} else {
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
result.data?.let {
|
||||
accountViewModel.runOnIO {
|
||||
accountViewModel.account.signer.launcher
|
||||
.newResult(it)
|
||||
accountViewModel.account.signer.newResponse(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
DisposableEffect(accountViewModel, accountViewModel.account, launcher, activity, lifeCycleOwner) {
|
||||
val observer =
|
||||
LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
accountViewModel.account.signer.launcher.registerLauncher(
|
||||
launcher = {
|
||||
try {
|
||||
launcher.launch(it)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("Signer", "Error opening Signer app", e)
|
||||
accountViewModel.toastManager.toast(
|
||||
R.string.error_opening_external_signer,
|
||||
R.string.error_opening_external_signer_description,
|
||||
)
|
||||
}
|
||||
},
|
||||
contentResolver = Amethyst.instance::contentResolverFn,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val launcher: (Intent) -> Unit = {
|
||||
DisposableEffect(accountViewModel, accountViewModel.account, launcher) {
|
||||
val launcher: (Intent) -> Unit = { intent ->
|
||||
try {
|
||||
launcher.launch(it)
|
||||
launcher.launch(intent)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("Signer", "Error opening Signer app", e)
|
||||
@@ -226,15 +194,9 @@ private fun ListenToExternalSignerIfNeeded(accountViewModel: AccountViewModel) {
|
||||
}
|
||||
}
|
||||
|
||||
lifeCycleOwner.lifecycle.addObserver(observer)
|
||||
accountViewModel.account.signer.launcher.registerLauncher(
|
||||
launcher = launcher,
|
||||
contentResolver = Amethyst.instance::contentResolverFn,
|
||||
)
|
||||
accountViewModel.account.signer.registerForegroundLauncher(launcher)
|
||||
onDispose {
|
||||
accountViewModel.account.signer.launcher
|
||||
.clearLauncherIf(launcher)
|
||||
lifeCycleOwner.lifecycle.removeObserver(observer)
|
||||
accountViewModel.account.signer.unregisterForegroundLauncher(launcher)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -27,6 +27,8 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedOff.login.LoginPage
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedOff.signup.SignUpPage
|
||||
|
||||
@Composable
|
||||
fun LoginOrSignupScreen(
|
||||
|
||||
@@ -1,814 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedOff
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Visibility
|
||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.autofill.AutofillNode
|
||||
import androidx.compose.ui.autofill.AutofillType
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalAutofill
|
||||
import androidx.compose.ui.platform.LocalAutofillTree
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
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.VisualTransformation
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.util.Consumer
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.hashtags.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons
|
||||
import com.vitorpamplona.amethyst.service.PackageUtils
|
||||
import com.vitorpamplona.amethyst.ui.MainActivity
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadingAnimation
|
||||
import com.vitorpamplona.amethyst.ui.components.getActivity
|
||||
import com.vitorpamplona.amethyst.ui.painterRes
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.SimpleQrCodeScanner
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size0dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size50dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettings
|
||||
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.ExternalSignerLauncher
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.SignerType
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.UUID
|
||||
|
||||
@Preview(device = "spec:width=2160px,height=2340px,dpi=440")
|
||||
@Composable
|
||||
fun LoginPage() {
|
||||
val accountViewModel: AccountStateViewModel = viewModel()
|
||||
|
||||
ThemeComparisonRow(
|
||||
toPreview = {
|
||||
LoginPage(accountViewModel, true) {}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoginPage(
|
||||
accountStateViewModel: AccountStateViewModel,
|
||||
isFirstLogin: Boolean,
|
||||
newAccountKey: String? = null,
|
||||
onWantsToLogin: () -> Unit,
|
||||
) {
|
||||
val key = remember { mutableStateOf(TextFieldValue(newAccountKey ?: "")) }
|
||||
var errorMessage by remember { mutableStateOf("") }
|
||||
val acceptedTerms = remember { mutableStateOf(!isFirstLogin) }
|
||||
var termsAcceptanceIsRequired by remember { mutableStateOf("") }
|
||||
|
||||
val context = LocalContext.current
|
||||
val torSettings = remember { mutableStateOf(TorSettings()) }
|
||||
val isNFCOrQR = remember { mutableStateOf(false) }
|
||||
val isTemporary = remember { mutableStateOf(false) }
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
var loginWithExternalSigner by remember { mutableStateOf(false) }
|
||||
|
||||
var processingLogin by remember { mutableStateOf(false) }
|
||||
|
||||
val password = remember { mutableStateOf(TextFieldValue("")) }
|
||||
val needsPassword =
|
||||
remember {
|
||||
derivedStateOf {
|
||||
key.value.text.startsWith("ncryptsec1")
|
||||
}
|
||||
}
|
||||
|
||||
val passwordFocusRequester = remember { FocusRequester() }
|
||||
|
||||
if (loginWithExternalSigner) {
|
||||
PrepareExternalSignerReceiver { pubkey, packageName ->
|
||||
key.value = TextFieldValue(pubkey)
|
||||
if (!acceptedTerms.value) {
|
||||
termsAcceptanceIsRequired = stringRes(context, R.string.acceptance_of_terms_is_required)
|
||||
}
|
||||
|
||||
if (key.value.text.isBlank()) {
|
||||
errorMessage = stringRes(context, R.string.key_is_required)
|
||||
}
|
||||
|
||||
if (acceptedTerms.value && key.value.text.isNotBlank()) {
|
||||
accountStateViewModel.login(
|
||||
key = key.value.text,
|
||||
torSettings = torSettings.value,
|
||||
transientAccount = isTemporary.value,
|
||||
loginWithExternalSigner = true,
|
||||
packageName = packageName,
|
||||
) {
|
||||
errorMessage = stringRes(context, R.string.invalid_key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.imePadding()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(Size20dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Image(
|
||||
imageVector = CustomHashTagIcons.Amethyst,
|
||||
contentDescription = stringRes(R.string.app_logo),
|
||||
modifier = Modifier.size(150.dp),
|
||||
contentScale = ContentScale.Inside,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(Size40dp))
|
||||
|
||||
KeyTextField(
|
||||
value = key.value,
|
||||
onValueChange = { value, isQr ->
|
||||
key.value = value
|
||||
if (isQr) {
|
||||
isNFCOrQR.value = true
|
||||
isTemporary.value = true
|
||||
}
|
||||
if (errorMessage.isNotEmpty()) {
|
||||
errorMessage = ""
|
||||
}
|
||||
},
|
||||
) {
|
||||
if (!acceptedTerms.value) {
|
||||
termsAcceptanceIsRequired = stringRes(context, R.string.acceptance_of_terms_is_required)
|
||||
}
|
||||
|
||||
if (key.value.text.isBlank()) {
|
||||
errorMessage = stringRes(context, R.string.key_is_required)
|
||||
}
|
||||
|
||||
if (needsPassword.value && password.value.text.isBlank()) {
|
||||
errorMessage = stringRes(context, R.string.password_is_required)
|
||||
}
|
||||
|
||||
if (acceptedTerms.value && key.value.text.isNotBlank() && !(needsPassword.value && password.value.text.isBlank())) {
|
||||
processingLogin = true
|
||||
accountStateViewModel.login(
|
||||
key = key.value.text,
|
||||
password = password.value.text,
|
||||
torSettings = torSettings.value,
|
||||
transientAccount = isTemporary.value,
|
||||
) {
|
||||
processingLogin = false
|
||||
errorMessage =
|
||||
if (it != null) {
|
||||
stringRes(context, R.string.invalid_key_with_message, it)
|
||||
} else {
|
||||
stringRes(context, R.string.invalid_key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessage.isNotBlank()) {
|
||||
Text(
|
||||
text = errorMessage,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
if (needsPassword.value) {
|
||||
PasswordField(
|
||||
value = password.value,
|
||||
onValueChange = {
|
||||
password.value = it
|
||||
if (errorMessage.isNotEmpty()) {
|
||||
errorMessage = ""
|
||||
}
|
||||
},
|
||||
passwordFocusRequester,
|
||||
) {
|
||||
if (!acceptedTerms.value) {
|
||||
termsAcceptanceIsRequired = stringRes(context, R.string.acceptance_of_terms_is_required)
|
||||
}
|
||||
|
||||
if (key.value.text.isBlank()) {
|
||||
errorMessage = stringRes(context, R.string.key_is_required)
|
||||
}
|
||||
|
||||
if (needsPassword.value && password.value.text.isBlank()) {
|
||||
errorMessage = stringRes(context, R.string.password_is_required)
|
||||
}
|
||||
|
||||
if (acceptedTerms.value && key.value.text.isNotBlank() && !(needsPassword.value && password.value.text.isBlank())) {
|
||||
processingLogin = true
|
||||
accountStateViewModel.login(key.value.text, password.value.text, torSettings.value, isTemporary.value) {
|
||||
processingLogin = false
|
||||
errorMessage =
|
||||
if (it != null) {
|
||||
stringRes(context, R.string.invalid_key_with_message, it)
|
||||
} else {
|
||||
stringRes(context, R.string.invalid_key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
TorSettingsSetup(
|
||||
torSettings = torSettings.value,
|
||||
onCheckedChange = {
|
||||
torSettings.value = it
|
||||
},
|
||||
onError = {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
it,
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
if (isNFCOrQR.value) {
|
||||
OfferTemporaryAccount(
|
||||
checked = isTemporary.value,
|
||||
onCheckedChange = { isTemporary.value = it },
|
||||
)
|
||||
}
|
||||
|
||||
if (isFirstLogin) {
|
||||
AcceptTerms(
|
||||
checked = acceptedTerms.value,
|
||||
onCheckedChange = { acceptedTerms.value = it },
|
||||
)
|
||||
|
||||
if (termsAcceptanceIsRequired.isNotBlank()) {
|
||||
Text(
|
||||
text = termsAcceptanceIsRequired,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(Size10dp))
|
||||
|
||||
Box(modifier = Modifier.padding(Size40dp, 0.dp, Size40dp, 0.dp)) {
|
||||
LoginButton(
|
||||
enabled = acceptedTerms.value,
|
||||
processingLogin = processingLogin,
|
||||
onClick = {
|
||||
if (!acceptedTerms.value) {
|
||||
termsAcceptanceIsRequired =
|
||||
stringRes(context, R.string.acceptance_of_terms_is_required)
|
||||
}
|
||||
|
||||
if (key.value.text.isBlank()) {
|
||||
errorMessage = stringRes(context, R.string.key_is_required)
|
||||
}
|
||||
|
||||
if (needsPassword.value && password.value.text.isBlank()) {
|
||||
errorMessage = stringRes(context, R.string.password_is_required)
|
||||
}
|
||||
|
||||
if (acceptedTerms.value && key.value.text.isNotBlank() && !(needsPassword.value && password.value.text.isBlank())) {
|
||||
processingLogin = true
|
||||
accountStateViewModel.login(key.value.text, password.value.text, torSettings.value, isTemporary.value) {
|
||||
processingLogin = false
|
||||
errorMessage =
|
||||
if (it != null) {
|
||||
stringRes(context, R.string.invalid_key_with_message, it)
|
||||
} else {
|
||||
stringRes(context, R.string.invalid_key)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (PackageUtils.isExternalSignerInstalled(context)) {
|
||||
Box(modifier = Modifier.padding(Size40dp, Size20dp, Size40dp, Size0dp)) {
|
||||
LoginWithAmberButton(
|
||||
enabled = acceptedTerms.value,
|
||||
onClick = {
|
||||
if (!acceptedTerms.value) {
|
||||
termsAcceptanceIsRequired = stringRes(context, R.string.acceptance_of_terms_is_required)
|
||||
} else {
|
||||
loginWithExternalSigner = true
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(Size40dp))
|
||||
|
||||
Text(text = stringRes(R.string.don_t_have_an_account))
|
||||
|
||||
Spacer(modifier = Modifier.height(Size20dp))
|
||||
|
||||
Box(modifier = Modifier.padding(Size40dp, 0.dp, Size40dp, 0.dp)) {
|
||||
SignUpButton(onWantsToLogin)
|
||||
}
|
||||
}
|
||||
|
||||
OpenURIIfNotLoggedIn {
|
||||
key.value = TextFieldValue(it)
|
||||
acceptedTerms.value = true
|
||||
isNFCOrQR.value = true
|
||||
isTemporary.value = true
|
||||
if (it.startsWith("ncryptsec1")) {
|
||||
delay(300)
|
||||
passwordFocusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OfferTemporaryAccount(
|
||||
checked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
)
|
||||
|
||||
Text(stringRes(R.string.temporary_account))
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun PasswordField(
|
||||
value: TextFieldValue,
|
||||
onValueChange: (TextFieldValue) -> Unit,
|
||||
passwordFocusRequester: FocusRequester,
|
||||
onGo: () -> Unit,
|
||||
) {
|
||||
val autofillNodeKey =
|
||||
AutofillNode(
|
||||
autofillTypes = listOf(AutofillType.Password),
|
||||
onFill = { onValueChange(TextFieldValue(it)) },
|
||||
)
|
||||
|
||||
val autofillNodePassword =
|
||||
AutofillNode(
|
||||
autofillTypes = listOf(AutofillType.Password),
|
||||
onFill = { onValueChange(TextFieldValue(it)) },
|
||||
)
|
||||
|
||||
val autofill = LocalAutofill.current
|
||||
LocalAutofillTree.current += autofillNodeKey
|
||||
LocalAutofillTree.current += autofillNodePassword
|
||||
|
||||
var showCharsPassword by remember { mutableStateOf(false) }
|
||||
OutlinedTextField(
|
||||
modifier =
|
||||
Modifier
|
||||
.focusRequester(passwordFocusRequester)
|
||||
.onGloballyPositioned { coordinates ->
|
||||
autofillNodePassword.boundingBox = coordinates.boundsInWindow()
|
||||
}.onFocusChanged { focusState ->
|
||||
autofill?.run {
|
||||
if (focusState.isFocused) {
|
||||
requestAutofillForNode(autofillNodePassword)
|
||||
} else {
|
||||
cancelAutofillForNode(autofillNodePassword)
|
||||
}
|
||||
}
|
||||
},
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
autoCorrectEnabled = false,
|
||||
keyboardType = KeyboardType.Password,
|
||||
imeAction = ImeAction.Go,
|
||||
),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringRes(R.string.ncryptsec_password),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Row {
|
||||
IconButton(onClick = { showCharsPassword = !showCharsPassword }) {
|
||||
Icon(
|
||||
imageVector =
|
||||
if (showCharsPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
|
||||
contentDescription =
|
||||
if (showCharsPassword) {
|
||||
stringRes(R.string.show_password)
|
||||
} else {
|
||||
stringRes(
|
||||
R.string.hide_password,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
visualTransformation =
|
||||
if (showCharsPassword) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
keyboardActions =
|
||||
KeyboardActions(
|
||||
onGo = {
|
||||
onGo()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun KeyTextField(
|
||||
value: TextFieldValue,
|
||||
onValueChange: (TextFieldValue, throughQR: Boolean) -> Unit,
|
||||
onLogin: () -> Unit,
|
||||
) {
|
||||
var dialogOpen by remember { mutableStateOf(false) }
|
||||
|
||||
var showCharsKey by remember { mutableStateOf(false) }
|
||||
|
||||
val autofillNodeKey =
|
||||
AutofillNode(
|
||||
autofillTypes = listOf(AutofillType.Password),
|
||||
onFill = { onValueChange(TextFieldValue(it), false) },
|
||||
)
|
||||
|
||||
val autofillNodePassword =
|
||||
AutofillNode(
|
||||
autofillTypes = listOf(AutofillType.Password),
|
||||
onFill = { onValueChange(TextFieldValue(it), false) },
|
||||
)
|
||||
|
||||
val autofill = LocalAutofill.current
|
||||
LocalAutofillTree.current += autofillNodeKey
|
||||
LocalAutofillTree.current += autofillNodePassword
|
||||
|
||||
OutlinedTextField(
|
||||
modifier =
|
||||
Modifier
|
||||
.onGloballyPositioned { coordinates ->
|
||||
autofillNodeKey.boundingBox = coordinates.boundsInWindow()
|
||||
}.onFocusChanged { focusState ->
|
||||
autofill?.run {
|
||||
if (focusState.isFocused) {
|
||||
requestAutofillForNode(autofillNodeKey)
|
||||
} else {
|
||||
cancelAutofillForNode(autofillNodeKey)
|
||||
}
|
||||
}
|
||||
},
|
||||
value = value,
|
||||
onValueChange = { onValueChange(it, false) },
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
autoCorrectEnabled = false,
|
||||
keyboardType = KeyboardType.Password,
|
||||
imeAction = ImeAction.Go,
|
||||
),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringRes(R.string.nsec_npub_hex_private_key),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Row {
|
||||
IconButton(onClick = { showCharsKey = !showCharsKey }) {
|
||||
Icon(
|
||||
imageVector =
|
||||
if (showCharsKey) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
|
||||
contentDescription =
|
||||
if (showCharsKey) {
|
||||
stringRes(R.string.show_password)
|
||||
} else {
|
||||
stringRes(
|
||||
R.string.hide_password,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
leadingIcon = {
|
||||
if (dialogOpen) {
|
||||
SimpleQrCodeScanner {
|
||||
dialogOpen = false
|
||||
if (!it.isNullOrEmpty()) {
|
||||
onValueChange(TextFieldValue(it), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
IconButton(onClick = { dialogOpen = true }) {
|
||||
Icon(
|
||||
painter = painterRes(R.drawable.ic_qrcode, 5),
|
||||
contentDescription =
|
||||
stringRes(
|
||||
R.string.login_with_qr_code,
|
||||
),
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
},
|
||||
visualTransformation =
|
||||
if (showCharsKey) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
keyboardActions =
|
||||
KeyboardActions(
|
||||
onGo = {
|
||||
onLogin()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PrepareExternalSignerReceiver(onLogin: (pubkey: String, packageName: String) -> Unit) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val externalSignerLauncher = remember { ExternalSignerLauncher("", signerPackageName = "") }
|
||||
val id = remember { UUID.randomUUID().toString() }
|
||||
|
||||
val launcher =
|
||||
rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult(),
|
||||
onResult = { result ->
|
||||
if (result.resultCode != Activity.RESULT_OK) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
Toast
|
||||
.makeText(
|
||||
Amethyst.instance,
|
||||
"Sign request rejected",
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
} else {
|
||||
result.data?.let { externalSignerLauncher.newResult(it) }
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
val activity = getActivity() as MainActivity
|
||||
|
||||
DisposableEffect(launcher, activity, externalSignerLauncher) {
|
||||
val launcher: (Intent) -> Unit = {
|
||||
try {
|
||||
launcher.launch(it)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("Signer", "Error opening Signer app", e)
|
||||
scope.launch(Dispatchers.Main) {
|
||||
Toast
|
||||
.makeText(
|
||||
Amethyst.instance,
|
||||
R.string.error_opening_external_signer,
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
externalSignerLauncher.registerLauncher(
|
||||
launcher = launcher,
|
||||
contentResolver = Amethyst.instance::contentResolverFn,
|
||||
)
|
||||
onDispose {
|
||||
externalSignerLauncher.clearLauncherIf(launcher)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(externalSignerLauncher) {
|
||||
externalSignerLauncher.openSignerApp(
|
||||
"",
|
||||
SignerType.GET_PUBLIC_KEY,
|
||||
"",
|
||||
id,
|
||||
) { result ->
|
||||
val split = result.split("-")
|
||||
val pubkey = split.first()
|
||||
val packageName = if (split.size > 1) split[1] else ""
|
||||
|
||||
onLogin(pubkey, packageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OpenURIIfNotLoggedIn(onNewNIP19: suspend (String) -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val activity = context.getActivity()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var currentIntentNextPage by remember {
|
||||
val uri =
|
||||
activity.intent
|
||||
?.data
|
||||
?.toString()
|
||||
?.ifBlank { null }
|
||||
|
||||
activity.intent.data = null
|
||||
|
||||
mutableStateOf(uri)
|
||||
}
|
||||
|
||||
currentIntentNextPage?.let { intentNextPage ->
|
||||
var nip19 by remember {
|
||||
mutableStateOf(
|
||||
Nip19Parser.tryParseAndClean(currentIntentNextPage),
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(intentNextPage) {
|
||||
if (nip19 != null) {
|
||||
nip19?.let {
|
||||
scope.launch {
|
||||
onNewNIP19(it)
|
||||
}
|
||||
nip19 = null
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
stringRes(context, R.string.invalid_nip19_uri_description, intentNextPage),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
currentIntentNextPage = null
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(activity) {
|
||||
val consumer =
|
||||
Consumer<Intent> { intent ->
|
||||
val uri = intent.data?.toString()
|
||||
if (!uri.isNullOrBlank()) {
|
||||
val newNip19 = Nip19Parser.tryParseAndClean(uri)
|
||||
if (newNip19 != null) {
|
||||
scope.launch {
|
||||
onNewNIP19(newNip19)
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
delay(1000)
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
stringRes(context, R.string.invalid_nip19_uri_description, uri),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
activity.addOnNewIntentListener(consumer)
|
||||
onDispose { activity.removeOnNewIntentListener(consumer) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SignUpButton(onClick: () -> Unit) {
|
||||
OutlinedButton(
|
||||
onClick = onClick,
|
||||
shape = RoundedCornerShape(Size35dp),
|
||||
modifier = Modifier.height(50.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(R.string.sign_up),
|
||||
modifier = Modifier.padding(horizontal = Size40dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoginWithAmberButton(
|
||||
enabled: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Button(
|
||||
enabled = enabled,
|
||||
onClick = onClick,
|
||||
shape = RoundedCornerShape(Size35dp),
|
||||
modifier = Modifier.height(Size50dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(R.string.login_with_external_signer),
|
||||
modifier = Modifier.padding(horizontal = Size40dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoginButton(
|
||||
enabled: Boolean,
|
||||
processingLogin: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Button(
|
||||
enabled = enabled,
|
||||
onClick = onClick,
|
||||
shape = RoundedCornerShape(Size35dp),
|
||||
modifier = Modifier.height(Size50dp),
|
||||
) {
|
||||
Row(modifier = Modifier.padding(horizontal = Size40dp)) {
|
||||
if (processingLogin) {
|
||||
LoadingAnimation()
|
||||
Spacer(modifier = DoubleHorzSpacer)
|
||||
}
|
||||
Text(stringRes(R.string.login))
|
||||
}
|
||||
}
|
||||
}
|
||||
-4
@@ -36,7 +36,6 @@ import com.vitorpamplona.amethyst.ui.components.appendLink
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.tor.ConnectTorDialog
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettings
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorType
|
||||
|
||||
@Composable
|
||||
fun TorSettingsSetup(
|
||||
@@ -45,8 +44,6 @@ fun TorSettingsSetup(
|
||||
onError: (String) -> Unit,
|
||||
) {
|
||||
var connectOrbotDialogOpen by remember { mutableStateOf(false) }
|
||||
var activeTor by remember { mutableStateOf(false) }
|
||||
|
||||
val primary = MaterialTheme.colorScheme.primary
|
||||
|
||||
Text(
|
||||
@@ -63,7 +60,6 @@ fun TorSettingsSetup(
|
||||
torSettings = torSettings,
|
||||
onClose = { connectOrbotDialogOpen = false },
|
||||
onPost = { torSettings ->
|
||||
activeTor = torSettings.torType != TorType.OFF
|
||||
connectOrbotDialogOpen = false
|
||||
onCheckedChange(torSettings)
|
||||
},
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedOff.login
|
||||
|
||||
import android.app.Activity
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.DefaultSignerPermissions
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size0dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.PubKeyResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.client.ExternalSignerLogin
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun ExternalSignerButton(loginViewModel: LoginViewModel) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val launcher =
|
||||
rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult(),
|
||||
) { result ->
|
||||
scope.launch {
|
||||
val resultData = result.data
|
||||
if (result.resultCode == Activity.RESULT_OK && resultData != null) {
|
||||
val loginInfo = ExternalSignerLogin.parseResult(resultData)
|
||||
if (loginInfo is SignerResult.Successful<PubKeyResult>) {
|
||||
loginViewModel.updateKey(TextFieldValue(loginInfo.result.pubkey), false)
|
||||
loginViewModel.loginWithExternalSigner(loginInfo.result.packageName)
|
||||
}
|
||||
} else {
|
||||
loginViewModel.errorManager.error(R.string.sign_request_rejected2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.padding(Size40dp, Size20dp, Size40dp, Size0dp)) {
|
||||
LoginWithAmberButton(
|
||||
enabled = loginViewModel.acceptedTerms,
|
||||
onClick = {
|
||||
if (!loginViewModel.acceptedTerms) {
|
||||
loginViewModel.termsAcceptanceIsRequiredError = true
|
||||
} else {
|
||||
try {
|
||||
launcher.launch(ExternalSignerLogin.createIntent(DefaultSignerPermissions))
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.e("Signer", "Error opening Signer app", e)
|
||||
loginViewModel.errorManager.error(R.string.error_opening_external_signer)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedOff.login
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Visibility
|
||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.autofill.AutofillNode
|
||||
import androidx.compose.ui.autofill.AutofillType
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalAutofill
|
||||
import androidx.compose.ui.platform.LocalAutofillTree
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
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.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.painterRes
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.SimpleQrCodeScanner
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun KeyTextField(
|
||||
value: TextFieldValue,
|
||||
onValueChange: (TextFieldValue, throughQR: Boolean) -> Unit,
|
||||
onLogin: () -> Unit,
|
||||
) {
|
||||
var dialogOpen by remember { mutableStateOf(false) }
|
||||
|
||||
var showCharsKey by remember { mutableStateOf(false) }
|
||||
|
||||
val autofillNodeKey =
|
||||
AutofillNode(
|
||||
autofillTypes = listOf(AutofillType.Password),
|
||||
onFill = { onValueChange(TextFieldValue(it), false) },
|
||||
)
|
||||
|
||||
val autofillNodePassword =
|
||||
AutofillNode(
|
||||
autofillTypes = listOf(AutofillType.Password),
|
||||
onFill = { onValueChange(TextFieldValue(it), false) },
|
||||
)
|
||||
|
||||
val autofill = LocalAutofill.current
|
||||
LocalAutofillTree.current += autofillNodeKey
|
||||
LocalAutofillTree.current += autofillNodePassword
|
||||
|
||||
OutlinedTextField(
|
||||
modifier =
|
||||
Modifier
|
||||
.onGloballyPositioned { coordinates ->
|
||||
autofillNodeKey.boundingBox = coordinates.boundsInWindow()
|
||||
}.onFocusChanged { focusState ->
|
||||
autofill?.run {
|
||||
if (focusState.isFocused) {
|
||||
requestAutofillForNode(autofillNodeKey)
|
||||
} else {
|
||||
cancelAutofillForNode(autofillNodeKey)
|
||||
}
|
||||
}
|
||||
},
|
||||
value = value,
|
||||
onValueChange = { onValueChange(it, false) },
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
autoCorrectEnabled = false,
|
||||
keyboardType = KeyboardType.Password,
|
||||
imeAction = ImeAction.Go,
|
||||
),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringRes(R.string.nsec_npub_hex_private_key),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Row {
|
||||
IconButton(onClick = { showCharsKey = !showCharsKey }) {
|
||||
Icon(
|
||||
imageVector =
|
||||
if (showCharsKey) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
|
||||
contentDescription =
|
||||
if (showCharsKey) {
|
||||
stringRes(R.string.show_password)
|
||||
} else {
|
||||
stringRes(
|
||||
R.string.hide_password,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
leadingIcon = {
|
||||
if (dialogOpen) {
|
||||
SimpleQrCodeScanner {
|
||||
dialogOpen = false
|
||||
if (!it.isNullOrEmpty()) {
|
||||
onValueChange(TextFieldValue(it), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
IconButton(onClick = { dialogOpen = true }) {
|
||||
Icon(
|
||||
painter = painterRes(R.drawable.ic_qrcode, 5),
|
||||
contentDescription = stringRes(R.string.login_with_qr_code),
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
},
|
||||
visualTransformation =
|
||||
if (showCharsKey) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
keyboardActions =
|
||||
KeyboardActions(
|
||||
onGo = {
|
||||
onLogin()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedOff.login
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadingAnimation
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size50dp
|
||||
|
||||
@Composable
|
||||
fun LoginButton(
|
||||
enabled: Boolean,
|
||||
processingLogin: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Button(
|
||||
enabled = enabled,
|
||||
onClick = onClick,
|
||||
shape = RoundedCornerShape(Size35dp),
|
||||
modifier = Modifier.height(Size50dp),
|
||||
) {
|
||||
Row(modifier = Modifier.padding(horizontal = Size40dp)) {
|
||||
if (processingLogin) {
|
||||
LoadingAnimation()
|
||||
Spacer(modifier = DoubleHorzSpacer)
|
||||
}
|
||||
Text(stringRes(R.string.login))
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedOff.login
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
|
||||
class LoginErrorManager {
|
||||
interface IErrorMsg
|
||||
|
||||
class SingleErrorMsg(
|
||||
val errorResId: Int,
|
||||
) : IErrorMsg
|
||||
|
||||
class ParamsErrorMsg(
|
||||
val errorResId: Int,
|
||||
val params: Array<out String>,
|
||||
) : IErrorMsg
|
||||
|
||||
var error by mutableStateOf<IErrorMsg?>(null)
|
||||
|
||||
fun clearErrors() {
|
||||
error = null
|
||||
}
|
||||
|
||||
fun error(resourceId: Int) {
|
||||
error = SingleErrorMsg(resourceId)
|
||||
}
|
||||
|
||||
fun error(
|
||||
resourceId: Int,
|
||||
vararg params: String,
|
||||
) {
|
||||
error = ParamsErrorMsg(resourceId, params)
|
||||
}
|
||||
}
|
||||
+362
@@ -0,0 +1,362 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedOff.login
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Visibility
|
||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.autofill.AutofillNode
|
||||
import androidx.compose.ui.autofill.AutofillType
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalAutofill
|
||||
import androidx.compose.ui.platform.LocalAutofillTree
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
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.VisualTransformation
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.hashtags.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedOff.AcceptTerms
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedOff.TorSettingsSetup
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.client.isExternalSignerInstalled
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Preview(device = "spec:width=2160px,height=2340px,dpi=440")
|
||||
@Composable
|
||||
fun LoginPagePreview() {
|
||||
val accountViewModel: AccountStateViewModel = viewModel()
|
||||
|
||||
ThemeComparisonRow(
|
||||
toPreview = {
|
||||
LoginPage(accountViewModel, true) {}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoginPage(
|
||||
accountStateViewModel: AccountStateViewModel,
|
||||
isFirstLogin: Boolean,
|
||||
newAccountKey: String? = null,
|
||||
onWantsToLogin: () -> Unit,
|
||||
) {
|
||||
val loginViewModel: LoginViewModel = viewModel()
|
||||
loginViewModel.init(accountStateViewModel)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
loginViewModel.load(isFirstLogin, newAccountKey)
|
||||
}
|
||||
|
||||
LoginPage(loginViewModel, onWantsToLogin)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoginPage(
|
||||
loginViewModel: LoginViewModel,
|
||||
onWantsToLogin: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.imePadding()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(Size20dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Image(
|
||||
imageVector = CustomHashTagIcons.Amethyst,
|
||||
contentDescription = stringRes(R.string.app_logo),
|
||||
modifier = Modifier.size(150.dp),
|
||||
contentScale = ContentScale.Inside,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(Size40dp))
|
||||
|
||||
KeyTextField(
|
||||
value = loginViewModel.key,
|
||||
onValueChange = loginViewModel::updateKey,
|
||||
onLogin = loginViewModel::login,
|
||||
)
|
||||
|
||||
loginViewModel.errorManager.error?.let { error ->
|
||||
when (error) {
|
||||
is LoginErrorManager.SingleErrorMsg ->
|
||||
Text(
|
||||
text = stringRes(error.errorResId),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
is LoginErrorManager.ParamsErrorMsg ->
|
||||
Text(
|
||||
text = stringRes(error.errorResId, *error.params),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
PasswordField(loginViewModel)
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
TorSettingsSetup(
|
||||
torSettings = loginViewModel.torSettings,
|
||||
onCheckedChange = loginViewModel::updateTorSettings,
|
||||
onError = {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
it,
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
if (loginViewModel.offerTemporaryLogin) {
|
||||
OfferTemporaryAccount(
|
||||
checked = loginViewModel.isTemporary,
|
||||
onCheckedChange = { loginViewModel.isTemporary = it },
|
||||
)
|
||||
}
|
||||
|
||||
if (loginViewModel.isFirstLogin) {
|
||||
AcceptTerms(
|
||||
checked = loginViewModel.acceptedTerms,
|
||||
onCheckedChange = loginViewModel::updateAcceptedTerms,
|
||||
)
|
||||
|
||||
if (loginViewModel.termsAcceptanceIsRequiredError) {
|
||||
Text(
|
||||
text = stringRes(R.string.acceptance_of_terms_is_required),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(Size10dp))
|
||||
|
||||
Box(modifier = Modifier.padding(Size40dp, 0.dp, Size40dp, 0.dp)) {
|
||||
LoginButton(
|
||||
enabled = loginViewModel.acceptedTerms,
|
||||
processingLogin = loginViewModel.processingLogin,
|
||||
onClick = loginViewModel::login,
|
||||
)
|
||||
}
|
||||
|
||||
if (isExternalSignerInstalled(context)) {
|
||||
ExternalSignerButton(loginViewModel)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(Size40dp))
|
||||
|
||||
Text(text = stringRes(R.string.don_t_have_an_account))
|
||||
|
||||
Spacer(modifier = Modifier.height(Size20dp))
|
||||
|
||||
Box(modifier = Modifier.padding(Size40dp, 0.dp, Size40dp, 0.dp)) {
|
||||
SignUpButton(onWantsToLogin)
|
||||
}
|
||||
}
|
||||
|
||||
OpenURIIfNotLoggedIn { key ->
|
||||
loginViewModel.updateKey(TextFieldValue(key), true)
|
||||
loginViewModel.updateOfferTemporaryLogin(true)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OfferTemporaryAccount(
|
||||
checked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
)
|
||||
|
||||
Text(stringRes(R.string.temporary_account))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PasswordField(loginViewModel: LoginViewModel) {
|
||||
if (loginViewModel.needsPassword) {
|
||||
val passwordFocusRequester = remember { FocusRequester() }
|
||||
|
||||
PasswordField(
|
||||
value = loginViewModel.password,
|
||||
onValueChange = loginViewModel::updatePassword,
|
||||
passwordFocusRequester = passwordFocusRequester,
|
||||
onGo = loginViewModel::login,
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
delay(300)
|
||||
passwordFocusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun PasswordField(
|
||||
value: TextFieldValue,
|
||||
onValueChange: (TextFieldValue) -> Unit,
|
||||
passwordFocusRequester: FocusRequester,
|
||||
onGo: () -> Unit,
|
||||
) {
|
||||
val autofillNodeKey =
|
||||
AutofillNode(
|
||||
autofillTypes = listOf(AutofillType.Password),
|
||||
onFill = { onValueChange(TextFieldValue(it)) },
|
||||
)
|
||||
|
||||
val autofillNodePassword =
|
||||
AutofillNode(
|
||||
autofillTypes = listOf(AutofillType.Password),
|
||||
onFill = { onValueChange(TextFieldValue(it)) },
|
||||
)
|
||||
|
||||
val autofill = LocalAutofill.current
|
||||
LocalAutofillTree.current += autofillNodeKey
|
||||
LocalAutofillTree.current += autofillNodePassword
|
||||
|
||||
var showCharsPassword by remember { mutableStateOf(false) }
|
||||
OutlinedTextField(
|
||||
modifier =
|
||||
Modifier
|
||||
.focusRequester(passwordFocusRequester)
|
||||
.onGloballyPositioned { coordinates ->
|
||||
autofillNodePassword.boundingBox = coordinates.boundsInWindow()
|
||||
}.onFocusChanged { focusState ->
|
||||
autofill?.run {
|
||||
if (focusState.isFocused) {
|
||||
requestAutofillForNode(autofillNodePassword)
|
||||
} else {
|
||||
cancelAutofillForNode(autofillNodePassword)
|
||||
}
|
||||
}
|
||||
},
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
autoCorrectEnabled = false,
|
||||
keyboardType = KeyboardType.Password,
|
||||
imeAction = ImeAction.Go,
|
||||
),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringRes(R.string.ncryptsec_password),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Row {
|
||||
IconButton(onClick = { showCharsPassword = !showCharsPassword }) {
|
||||
Icon(
|
||||
imageVector =
|
||||
if (showCharsPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
|
||||
contentDescription =
|
||||
if (showCharsPassword) {
|
||||
stringRes(R.string.show_password)
|
||||
} else {
|
||||
stringRes(
|
||||
R.string.hide_password,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
visualTransformation =
|
||||
if (showCharsPassword) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
keyboardActions =
|
||||
KeyboardActions(
|
||||
onGo = {
|
||||
onGo()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedOff.login
|
||||
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettings
|
||||
|
||||
class LoginViewModel : ViewModel() {
|
||||
lateinit var accountStateViewModel: AccountStateViewModel
|
||||
|
||||
val errorManager = LoginErrorManager()
|
||||
|
||||
var key by mutableStateOf(TextFieldValue(""))
|
||||
var acceptedTerms by mutableStateOf(false)
|
||||
var termsAcceptanceIsRequiredError by mutableStateOf(false)
|
||||
|
||||
var torSettings by mutableStateOf(TorSettings())
|
||||
var offerTemporaryLogin by mutableStateOf(false)
|
||||
var isTemporary by mutableStateOf(false)
|
||||
|
||||
var processingLogin by mutableStateOf(false)
|
||||
|
||||
var password by mutableStateOf(TextFieldValue(""))
|
||||
val needsPassword by derivedStateOf {
|
||||
key.text.startsWith("ncryptsec1")
|
||||
}
|
||||
|
||||
var isFirstLogin by mutableStateOf(false)
|
||||
|
||||
fun init(accountStateViewModel: AccountStateViewModel) {
|
||||
this.accountStateViewModel = accountStateViewModel
|
||||
}
|
||||
|
||||
fun load(
|
||||
isFirstLogin: Boolean,
|
||||
newAccountKey: String?,
|
||||
) {
|
||||
this.isFirstLogin = isFirstLogin
|
||||
acceptedTerms = isFirstLogin
|
||||
if (newAccountKey != null) {
|
||||
key = TextFieldValue(newAccountKey)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateKey(
|
||||
value: TextFieldValue,
|
||||
throughQR: Boolean,
|
||||
) {
|
||||
key = value
|
||||
if (throughQR) {
|
||||
offerTemporaryLogin = true
|
||||
isTemporary = true
|
||||
}
|
||||
errorManager.clearErrors()
|
||||
}
|
||||
|
||||
fun updatePassword(newPassword: TextFieldValue) {
|
||||
password = newPassword
|
||||
errorManager.clearErrors()
|
||||
}
|
||||
|
||||
fun updateTorSettings(newTorSettings: TorSettings) {
|
||||
torSettings = newTorSettings
|
||||
}
|
||||
|
||||
fun updateAcceptedTerms(newAcceptedTerms: Boolean) {
|
||||
acceptedTerms = newAcceptedTerms
|
||||
if (newAcceptedTerms) {
|
||||
termsAcceptanceIsRequiredError = false
|
||||
}
|
||||
|
||||
errorManager.clearErrors()
|
||||
}
|
||||
|
||||
fun updateOfferTemporaryLogin(tempLogin: Boolean) {
|
||||
offerTemporaryLogin = tempLogin
|
||||
}
|
||||
|
||||
fun checkCanLogin(): Boolean {
|
||||
if (!acceptedTerms) {
|
||||
termsAcceptanceIsRequiredError = true
|
||||
return false
|
||||
}
|
||||
|
||||
if (key.text.isBlank()) {
|
||||
errorManager.error(R.string.key_is_required)
|
||||
return false
|
||||
}
|
||||
|
||||
if (needsPassword && password.text.isBlank()) {
|
||||
errorManager.error(R.string.password_is_required)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun login() {
|
||||
if (checkCanLogin()) {
|
||||
processingLogin = true
|
||||
accountStateViewModel.login(
|
||||
key = key.text,
|
||||
password = password.text,
|
||||
torSettings = torSettings,
|
||||
transientAccount = isTemporary,
|
||||
) {
|
||||
processingLogin = false
|
||||
if (it != null) {
|
||||
errorManager.error(R.string.invalid_key_with_message, it)
|
||||
} else {
|
||||
errorManager.error(R.string.invalid_key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loginWithExternalSigner(packageName: String) {
|
||||
if (checkCanLogin()) {
|
||||
processingLogin = true
|
||||
accountStateViewModel.login(
|
||||
key = key.text,
|
||||
torSettings = torSettings,
|
||||
transientAccount = isTemporary,
|
||||
loginWithExternalSigner = true,
|
||||
packageName = packageName,
|
||||
) {
|
||||
processingLogin = false
|
||||
errorManager.error(R.string.sign_request_rejected_description)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedOff.login
|
||||
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size50dp
|
||||
|
||||
@Composable
|
||||
fun LoginWithAmberButton(
|
||||
enabled: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Button(
|
||||
enabled = enabled,
|
||||
onClick = onClick,
|
||||
shape = RoundedCornerShape(Size35dp),
|
||||
modifier = Modifier.height(Size50dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(R.string.login_with_external_signer),
|
||||
modifier = Modifier.padding(horizontal = Size40dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedOff.login
|
||||
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.util.Consumer
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.components.getActivity
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun OpenURIIfNotLoggedIn(onNewNIP19: suspend (String) -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val activity = context.getActivity()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var currentIntentNextPage by remember {
|
||||
val uri =
|
||||
activity.intent
|
||||
?.data
|
||||
?.toString()
|
||||
?.ifBlank { null }
|
||||
|
||||
activity.intent.data = null
|
||||
|
||||
mutableStateOf(uri)
|
||||
}
|
||||
|
||||
currentIntentNextPage?.let { intentNextPage ->
|
||||
var nip19 by remember {
|
||||
mutableStateOf(
|
||||
Nip19Parser.tryParseAndClean(currentIntentNextPage),
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(intentNextPage) {
|
||||
if (nip19 != null) {
|
||||
nip19?.let {
|
||||
scope.launch {
|
||||
onNewNIP19(it)
|
||||
}
|
||||
nip19 = null
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
stringRes(context, R.string.invalid_nip19_uri_description, intentNextPage),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
currentIntentNextPage = null
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(activity) {
|
||||
val consumer =
|
||||
Consumer<Intent> { intent ->
|
||||
val uri = intent.data?.toString()
|
||||
if (!uri.isNullOrBlank()) {
|
||||
val newNip19 = Nip19Parser.tryParseAndClean(uri)
|
||||
if (newNip19 != null) {
|
||||
scope.launch {
|
||||
onNewNIP19(newNip19)
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
delay(1000)
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
stringRes(context, R.string.invalid_nip19_uri_description, uri),
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
activity.addOnNewIntentListener(consumer)
|
||||
onDispose { activity.removeOnNewIntentListener(consumer) }
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedOff.login
|
||||
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
||||
|
||||
@Composable
|
||||
fun SignUpButton(onClick: () -> Unit) {
|
||||
OutlinedButton(
|
||||
onClick = onClick,
|
||||
shape = RoundedCornerShape(Size35dp),
|
||||
modifier = Modifier.height(50.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(R.string.sign_up),
|
||||
modifier = Modifier.padding(horizontal = Size40dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedOff.signup
|
||||
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
||||
|
||||
@Composable
|
||||
fun LoginButton(onWantsToLogin: () -> Unit) {
|
||||
OutlinedButton(
|
||||
onClick = onWantsToLogin,
|
||||
shape = RoundedCornerShape(Size35dp),
|
||||
modifier = Modifier.height(50.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(R.string.login),
|
||||
modifier = Modifier.padding(horizontal = Size40dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedOff.signup
|
||||
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
||||
|
||||
@Composable
|
||||
fun SignUpButton(
|
||||
enabled: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Button(
|
||||
enabled = enabled,
|
||||
onClick = onClick,
|
||||
shape = RoundedCornerShape(Size35dp),
|
||||
modifier = Modifier.height(50.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(R.string.create_account),
|
||||
modifier = Modifier.padding(horizontal = Size40dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
+43
-89
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedOff
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedOff.signup
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.Image
|
||||
@@ -32,19 +32,14 @@ import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -53,7 +48,6 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
@@ -61,19 +55,20 @@ import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.hashtags.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedOff.AcceptTerms
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedOff.TorSettingsSetup
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedOff.login.LoginErrorManager
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettings
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Preview(device = "spec:width=2160px,height=2340px,dpi=440")
|
||||
@Composable
|
||||
fun SignUpPage() {
|
||||
fun SignUpPagePreview() {
|
||||
val accountViewModel: AccountStateViewModel = viewModel()
|
||||
|
||||
ThemeComparisonRow(
|
||||
@@ -88,13 +83,18 @@ fun SignUpPage(
|
||||
accountStateViewModel: AccountStateViewModel,
|
||||
onWantsToLogin: () -> Unit,
|
||||
) {
|
||||
val displayName = remember { mutableStateOf(TextFieldValue("")) }
|
||||
var errorMessage by remember { mutableStateOf("") }
|
||||
val acceptedTerms = remember { mutableStateOf(false) }
|
||||
var termsAcceptanceIsRequired by remember { mutableStateOf("") }
|
||||
val signUpViewModel: SignUpViewModel = viewModel()
|
||||
signUpViewModel.init(accountStateViewModel)
|
||||
|
||||
SignUpPage(signUpViewModel, onWantsToLogin)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SignUpPage(
|
||||
signUpViewModel: SignUpViewModel,
|
||||
onWantsToLogin: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val torSettings = remember { mutableStateOf(TorSettings()) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Column(
|
||||
@@ -125,8 +125,8 @@ fun SignUpPage(
|
||||
Spacer(modifier = Modifier.height(Size20dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = displayName.value,
|
||||
onValueChange = { displayName.value = it },
|
||||
value = signUpViewModel.displayName,
|
||||
onValueChange = signUpViewModel::updateDisplayName,
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
autoCorrectEnabled = false,
|
||||
@@ -142,49 +142,47 @@ fun SignUpPage(
|
||||
keyboardActions =
|
||||
KeyboardActions(
|
||||
onGo = {
|
||||
if (!acceptedTerms.value) {
|
||||
termsAcceptanceIsRequired =
|
||||
stringRes(context, R.string.acceptance_of_terms_is_required)
|
||||
}
|
||||
|
||||
if (displayName.value.text.isBlank()) {
|
||||
errorMessage = stringRes(context, R.string.name_is_required)
|
||||
}
|
||||
|
||||
if (acceptedTerms.value && displayName.value.text.isNotBlank()) {
|
||||
accountStateViewModel.newKey(torSettings.value, displayName.value.text)
|
||||
}
|
||||
signUpViewModel.signup()
|
||||
},
|
||||
),
|
||||
)
|
||||
if (errorMessage.isNotBlank()) {
|
||||
Text(
|
||||
text = errorMessage,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
|
||||
signUpViewModel.errorManager.error?.let { error ->
|
||||
when (error) {
|
||||
is LoginErrorManager.SingleErrorMsg ->
|
||||
Text(
|
||||
text = stringRes(error.errorResId),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
is LoginErrorManager.ParamsErrorMsg ->
|
||||
Text(
|
||||
text = stringRes(error.errorResId, *error.params),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
AcceptTerms(
|
||||
checked = acceptedTerms.value,
|
||||
onCheckedChange = { acceptedTerms.value = it },
|
||||
checked = signUpViewModel.acceptedTerms,
|
||||
onCheckedChange = signUpViewModel::updateAcceptedTerms,
|
||||
)
|
||||
|
||||
if (termsAcceptanceIsRequired.isNotBlank()) {
|
||||
if (signUpViewModel.termsAcceptanceIsRequiredError) {
|
||||
Text(
|
||||
text = termsAcceptanceIsRequired,
|
||||
text = stringRes(R.string.acceptance_of_terms_is_required),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
|
||||
TorSettingsSetup(
|
||||
torSettings = torSettings.value,
|
||||
onCheckedChange = {
|
||||
torSettings.value = it
|
||||
},
|
||||
torSettings = signUpViewModel.torSettings,
|
||||
onCheckedChange = signUpViewModel::updateTorSettings,
|
||||
onError = {
|
||||
scope.launch {
|
||||
Toast
|
||||
@@ -201,20 +199,8 @@ fun SignUpPage(
|
||||
|
||||
Box(modifier = Modifier.padding(Size40dp, 0.dp, Size40dp, 0.dp)) {
|
||||
SignUpButton(
|
||||
enabled = acceptedTerms.value,
|
||||
onClick = {
|
||||
if (!acceptedTerms.value) {
|
||||
termsAcceptanceIsRequired = stringRes(context, R.string.acceptance_of_terms_is_required)
|
||||
}
|
||||
|
||||
if (displayName.value.text.isBlank()) {
|
||||
errorMessage = stringRes(context, R.string.name_is_required)
|
||||
}
|
||||
|
||||
if (acceptedTerms.value && displayName.value.text.isNotBlank()) {
|
||||
accountStateViewModel.newKey(torSettings.value, displayName.value.text)
|
||||
}
|
||||
},
|
||||
enabled = signUpViewModel.acceptedTerms,
|
||||
onClick = signUpViewModel::signup,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -229,35 +215,3 @@ fun SignUpPage(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoginButton(onWantsToLogin: () -> Unit) {
|
||||
OutlinedButton(
|
||||
onClick = onWantsToLogin,
|
||||
shape = RoundedCornerShape(Size35dp),
|
||||
modifier = Modifier.height(50.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(R.string.login),
|
||||
modifier = Modifier.padding(horizontal = Size40dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SignUpButton(
|
||||
enabled: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Button(
|
||||
enabled = enabled,
|
||||
onClick = onClick,
|
||||
shape = RoundedCornerShape(Size35dp),
|
||||
modifier = Modifier.height(50.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(R.string.create_account),
|
||||
modifier = Modifier.padding(horizontal = Size40dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedOff.signup
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedOff.login.LoginErrorManager
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettings
|
||||
|
||||
class SignUpViewModel : ViewModel() {
|
||||
lateinit var accountStateViewModel: AccountStateViewModel
|
||||
|
||||
val errorManager = LoginErrorManager()
|
||||
|
||||
var displayName by mutableStateOf(TextFieldValue(""))
|
||||
|
||||
var acceptedTerms by mutableStateOf(false)
|
||||
var termsAcceptanceIsRequiredError by mutableStateOf(false)
|
||||
|
||||
var torSettings by mutableStateOf(TorSettings())
|
||||
|
||||
fun init(accountStateViewModel: AccountStateViewModel) {
|
||||
this.accountStateViewModel = accountStateViewModel
|
||||
}
|
||||
|
||||
fun updateDisplayName(value: TextFieldValue) {
|
||||
displayName = value
|
||||
errorManager.clearErrors()
|
||||
}
|
||||
|
||||
fun updateTorSettings(newTorSettings: TorSettings) {
|
||||
torSettings = newTorSettings
|
||||
}
|
||||
|
||||
fun updateAcceptedTerms(newAcceptedTerms: Boolean) {
|
||||
acceptedTerms = newAcceptedTerms
|
||||
if (newAcceptedTerms) {
|
||||
termsAcceptanceIsRequiredError = false
|
||||
}
|
||||
|
||||
errorManager.clearErrors()
|
||||
}
|
||||
|
||||
fun checkCanSignup(): Boolean {
|
||||
if (!acceptedTerms) {
|
||||
termsAcceptanceIsRequiredError = true
|
||||
return false
|
||||
}
|
||||
|
||||
if (displayName.text.isBlank()) {
|
||||
errorManager.error(R.string.name_is_required)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun signup() {
|
||||
if (checkCanSignup()) {
|
||||
accountStateViewModel.newKey(torSettings, displayName.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -836,6 +836,7 @@
|
||||
<string name="wallet_number">Wallet %1$s</string>
|
||||
<string name="error_opening_external_signer">Error opening signer app</string>
|
||||
<string name="error_opening_external_signer_description">The signer app could not be found. Check if the app hasn\'t been uninstalled</string>
|
||||
<string name="sign_request_rejected2">Sign request rejected</string>
|
||||
<string name="sign_request_rejected">Signer Application Rejected</string>
|
||||
<string name="sign_request_rejected_description">Make sure the signer application has authorized this transaction</string>
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.core
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.jackson.EventManualSerializer
|
||||
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
|
||||
@@ -58,6 +59,14 @@ open class Event(
|
||||
companion object {
|
||||
fun fromJson(json: String): Event = JsonMapper.fromJson(json)
|
||||
|
||||
fun fromJsonOrNull(json: String) =
|
||||
try {
|
||||
fromJson(json)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Event", "Unable to parse event JSON: $json", e)
|
||||
null
|
||||
}
|
||||
|
||||
fun build(
|
||||
kind: Int,
|
||||
content: String = "",
|
||||
|
||||
@@ -35,9 +35,13 @@ import com.vitorpamplona.quartz.nip47WalletConnect.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.RequestDeserializer
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Response
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.ResponseDeserializer
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.Permission
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.PermissionDeserializer
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.PermissionSerializer
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorDeserializer
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorSerializer
|
||||
import kotlin.jvm.java
|
||||
|
||||
class JsonMapper {
|
||||
companion object Companion {
|
||||
@@ -60,7 +64,9 @@ class JsonMapper {
|
||||
.addSerializer(BunkerRequest::class.java, BunkerRequest.BunkerRequestSerializer())
|
||||
.addDeserializer(BunkerRequest::class.java, BunkerRequest.BunkerRequestDeserializer())
|
||||
.addSerializer(BunkerResponse::class.java, BunkerResponse.BunkerResponseSerializer())
|
||||
.addDeserializer(BunkerResponse::class.java, BunkerResponse.BunkerResponseDeserializer()),
|
||||
.addDeserializer(BunkerResponse::class.java, BunkerResponse.BunkerResponseDeserializer())
|
||||
.addDeserializer(Permission::class.java, PermissionDeserializer())
|
||||
.addSerializer(Permission::class.java, PermissionSerializer()),
|
||||
)
|
||||
|
||||
fun fromJson(json: String): Event = mapper.readValue(json, Event::class.java)
|
||||
|
||||
@@ -47,25 +47,25 @@ abstract class NostrSigner(
|
||||
)
|
||||
|
||||
abstract fun nip04Encrypt(
|
||||
decryptedContent: String,
|
||||
plaintext: String,
|
||||
toPublicKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
)
|
||||
|
||||
abstract fun nip04Decrypt(
|
||||
encryptedContent: String,
|
||||
ciphertext: String,
|
||||
fromPublicKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
)
|
||||
|
||||
abstract fun nip44Encrypt(
|
||||
decryptedContent: String,
|
||||
plaintext: String,
|
||||
toPublicKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
)
|
||||
|
||||
abstract fun nip44Decrypt(
|
||||
encryptedContent: String,
|
||||
ciphertext: String,
|
||||
fromPublicKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
)
|
||||
|
||||
+8
-8
@@ -45,35 +45,35 @@ class NostrSignerInternal(
|
||||
}
|
||||
|
||||
override fun nip04Encrypt(
|
||||
decryptedContent: String,
|
||||
plaintext: String,
|
||||
toPublicKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
signerSync.nip04Encrypt(decryptedContent, toPublicKey)?.let { onReady(it) }
|
||||
signerSync.nip04Encrypt(plaintext, toPublicKey)?.let { onReady(it) }
|
||||
}
|
||||
|
||||
override fun nip04Decrypt(
|
||||
encryptedContent: String,
|
||||
ciphertext: String,
|
||||
fromPublicKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
signerSync.nip04Decrypt(encryptedContent, fromPublicKey)?.let { onReady(it) }
|
||||
signerSync.nip04Decrypt(ciphertext, fromPublicKey)?.let { onReady(it) }
|
||||
}
|
||||
|
||||
override fun nip44Encrypt(
|
||||
decryptedContent: String,
|
||||
plaintext: String,
|
||||
toPublicKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
signerSync.nip44Encrypt(decryptedContent, toPublicKey)?.let { onReady(it) }
|
||||
signerSync.nip44Encrypt(plaintext, toPublicKey)?.let { onReady(it) }
|
||||
}
|
||||
|
||||
override fun nip44Decrypt(
|
||||
encryptedContent: String,
|
||||
ciphertext: String,
|
||||
fromPublicKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
signerSync.nip44Decrypt(encryptedContent, fromPublicKey)?.let { onReady(it) }
|
||||
signerSync.nip44Decrypt(ciphertext, fromPublicKey)?.let { onReady(it) }
|
||||
}
|
||||
|
||||
override fun decryptZapEvent(
|
||||
|
||||
-438
@@ -1,438 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.util.LruCache
|
||||
import androidx.core.net.toUri
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
|
||||
|
||||
enum class SignerType {
|
||||
SIGN_EVENT,
|
||||
NIP04_ENCRYPT,
|
||||
NIP04_DECRYPT,
|
||||
NIP44_ENCRYPT,
|
||||
NIP44_DECRYPT,
|
||||
GET_PUBLIC_KEY,
|
||||
DECRYPT_ZAP_EVENT,
|
||||
DERIVE_KEY,
|
||||
}
|
||||
|
||||
class Permission(
|
||||
val type: String,
|
||||
val kind: Int? = null,
|
||||
) {
|
||||
fun toJson(): String = "{\"type\":\"${type}\",\"kind\":$kind}"
|
||||
}
|
||||
|
||||
class Result(
|
||||
@JsonProperty("package") val `package`: String?,
|
||||
@JsonProperty("signature") val signature: String?,
|
||||
@JsonProperty("result") val result: String?,
|
||||
@JsonProperty("id") val id: String?,
|
||||
) {
|
||||
companion object {
|
||||
val mapper: ObjectMapper =
|
||||
jacksonObjectMapper()
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||
.registerModule(
|
||||
SimpleModule().addDeserializer(Result::class.java, ResultDeserializer()),
|
||||
)
|
||||
|
||||
private class ResultDeserializer : StdDeserializer<Result>(Result::class.java) {
|
||||
override fun deserialize(
|
||||
jp: JsonParser,
|
||||
ctxt: DeserializationContext,
|
||||
): Result {
|
||||
val jsonObject: JsonNode = jp.codec.readTree(jp)
|
||||
return Result(
|
||||
jsonObject.get("package").asText().intern(),
|
||||
jsonObject.get("signature")?.asText()?.intern(),
|
||||
jsonObject.get("result")?.asText()?.intern(),
|
||||
jsonObject.get("id").asText().intern(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun fromJson(json: String): Result = mapper.readValue(json, Result::class.java)
|
||||
|
||||
/**
|
||||
* Parses the json with a string of events to an Array of Event objects.
|
||||
*/
|
||||
fun fromJsonArray(json: String): Array<Result> = mapper.readValue(json)
|
||||
}
|
||||
}
|
||||
|
||||
class ExternalSignerLauncher(
|
||||
private val currentUserPubKeyHex: String,
|
||||
val signerPackageName: String,
|
||||
) {
|
||||
private val contentCache = LruCache<String, (String) -> Unit>(50)
|
||||
|
||||
private var signerAppLauncher: ((Intent) -> Unit)? = null
|
||||
private var contentResolver: (() -> ContentResolver)? = null
|
||||
|
||||
/** Call this function when the launcher becomes available on activity, fragment or compose */
|
||||
fun registerLauncher(
|
||||
launcher: ((Intent) -> Unit),
|
||||
contentResolver: (() -> ContentResolver),
|
||||
) {
|
||||
this.signerAppLauncher = launcher
|
||||
this.contentResolver = contentResolver
|
||||
}
|
||||
|
||||
/** Call this function when the activity is destroyed or is about to be replaced. */
|
||||
fun clearLauncherIf(launcher: ((Intent) -> Unit)) {
|
||||
if (signerAppLauncher == launcher) {
|
||||
this.signerAppLauncher = null
|
||||
this.contentResolver = null
|
||||
}
|
||||
}
|
||||
|
||||
fun newResult(data: Intent) {
|
||||
val results = data.getStringExtra("results")
|
||||
if (results != null) {
|
||||
val localResults: Array<Result> = Result.fromJsonArray(results)
|
||||
localResults.forEach {
|
||||
val signature = it.result ?: it.signature ?: ""
|
||||
val packageName = it.`package`?.let { "-$it" } ?: ""
|
||||
val id = it.id ?: ""
|
||||
if (id.isNotBlank()) {
|
||||
val result = if (packageName.isNotBlank()) "$signature$packageName" else signature
|
||||
val contentCache = contentCache.get(id)
|
||||
contentCache?.invoke(result)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val signature = data.getStringExtra("result") ?: data.getStringExtra("signature") ?: ""
|
||||
val packageName = data.getStringExtra("package")?.let { "-$it" } ?: ""
|
||||
val id = data.getStringExtra("id") ?: ""
|
||||
if (id.isNotBlank()) {
|
||||
val result = if (packageName.isNotBlank()) "$signature$packageName" else signature
|
||||
val contentCache = contentCache.get(id)
|
||||
contentCache?.invoke(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun openSignerApp(
|
||||
data: String,
|
||||
type: SignerType,
|
||||
pubKey: HexKey,
|
||||
id: String,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
signerAppLauncher?.let {
|
||||
openSignerApp(
|
||||
data,
|
||||
type,
|
||||
it,
|
||||
pubKey,
|
||||
id,
|
||||
onReady,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun defaultPermissions(): String {
|
||||
val permissions =
|
||||
listOf(
|
||||
Permission(
|
||||
"sign_event",
|
||||
22242,
|
||||
),
|
||||
Permission(
|
||||
"sign_event",
|
||||
31234,
|
||||
),
|
||||
Permission(
|
||||
"nip04_encrypt",
|
||||
),
|
||||
Permission(
|
||||
"nip04_decrypt",
|
||||
),
|
||||
Permission(
|
||||
"nip44_encrypt",
|
||||
),
|
||||
Permission(
|
||||
"nip44_decrypt",
|
||||
),
|
||||
Permission(
|
||||
"decrypt_zap_event",
|
||||
),
|
||||
)
|
||||
val jsonArray = StringBuilder("[")
|
||||
permissions.forEachIndexed { index, permission ->
|
||||
jsonArray.append(permission.toJson())
|
||||
if (index < permissions.size - 1) {
|
||||
jsonArray.append(",")
|
||||
}
|
||||
}
|
||||
jsonArray.append("]")
|
||||
|
||||
return jsonArray.toString()
|
||||
}
|
||||
|
||||
private fun openSignerApp(
|
||||
data: String,
|
||||
type: SignerType,
|
||||
intentLauncher: (Intent) -> Unit,
|
||||
pubKey: HexKey,
|
||||
id: String,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
val intent = Intent(Intent.ACTION_VIEW, "nostrsigner:$data".toUri())
|
||||
val signerType =
|
||||
when (type) {
|
||||
SignerType.SIGN_EVENT -> "sign_event"
|
||||
SignerType.NIP04_ENCRYPT -> "nip04_encrypt"
|
||||
SignerType.NIP04_DECRYPT -> "nip04_decrypt"
|
||||
SignerType.NIP44_ENCRYPT -> "nip44_encrypt"
|
||||
SignerType.NIP44_DECRYPT -> "nip44_decrypt"
|
||||
SignerType.GET_PUBLIC_KEY -> "get_public_key"
|
||||
SignerType.DECRYPT_ZAP_EVENT -> "decrypt_zap_event"
|
||||
SignerType.DERIVE_KEY -> "derive_key"
|
||||
}
|
||||
intent.putExtra("type", signerType)
|
||||
intent.putExtra("pubKey", pubKey)
|
||||
intent.putExtra("id", id)
|
||||
if (type !== SignerType.GET_PUBLIC_KEY) {
|
||||
intent.putExtra("current_user", currentUserPubKeyHex)
|
||||
} else {
|
||||
intent.putExtra("permissions", defaultPermissions())
|
||||
}
|
||||
if (signerPackageName.isNotBlank()) {
|
||||
intent.`package` = signerPackageName
|
||||
}
|
||||
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
|
||||
contentCache.put(id, onReady)
|
||||
|
||||
intentLauncher(intent)
|
||||
}
|
||||
|
||||
fun openSigner(
|
||||
event: Event,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
getDataFromResolver(
|
||||
SignerType.SIGN_EVENT,
|
||||
arrayOf(event.toJson(), event.pubKey),
|
||||
).fold(
|
||||
onFailure = { },
|
||||
onSuccess = {
|
||||
if (it == null) {
|
||||
openSignerApp(
|
||||
event.toJson(),
|
||||
SignerType.SIGN_EVENT,
|
||||
"",
|
||||
event.id,
|
||||
onReady,
|
||||
)
|
||||
} else {
|
||||
onReady(it)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun getDataFromResolver(
|
||||
signerType: SignerType,
|
||||
data: Array<out String>,
|
||||
): kotlin.Result<String?> = getDataFromResolver(signerType, data, contentResolver)
|
||||
|
||||
private fun getDataFromResolver(
|
||||
signerType: SignerType,
|
||||
data: Array<out String>,
|
||||
contentResolver: (() -> ContentResolver)? = null,
|
||||
): kotlin.Result<String?> {
|
||||
val localData =
|
||||
if (signerType !== SignerType.GET_PUBLIC_KEY) {
|
||||
arrayOf(*data, currentUserPubKeyHex)
|
||||
} else {
|
||||
data
|
||||
}
|
||||
|
||||
try {
|
||||
contentResolver
|
||||
?.let { it() }
|
||||
?.query(
|
||||
"content://$signerPackageName.$signerType".toUri(),
|
||||
localData,
|
||||
"1",
|
||||
null,
|
||||
null,
|
||||
).use {
|
||||
if (it == null) {
|
||||
return kotlin.Result.success(null)
|
||||
}
|
||||
if (it.moveToFirst()) {
|
||||
if (it.getColumnIndex("rejected") > -1) {
|
||||
Log.d("getDataFromResolver", "Permission denied")
|
||||
return kotlin.Result.failure(Exception("Permission denied"))
|
||||
}
|
||||
var index = it.getColumnIndex("result")
|
||||
if (index < 0) {
|
||||
index = it.getColumnIndex("signature")
|
||||
}
|
||||
if (index < 0) {
|
||||
Log.d("getDataFromResolver", "column 'signature' not found")
|
||||
return kotlin.Result.success(null)
|
||||
}
|
||||
return kotlin.Result.success(it.getString(index))
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("ExternalSignerLauncher", "Failed to query the Signer app in the background", e)
|
||||
return kotlin.Result.success(null)
|
||||
}
|
||||
|
||||
return kotlin.Result.success(null)
|
||||
}
|
||||
|
||||
fun hashCodeFields(
|
||||
str1: String,
|
||||
onReady: (String) -> Unit,
|
||||
): Int {
|
||||
var result = str1.hashCode()
|
||||
result = 31 * result + onReady.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
fun hashCodeFields(
|
||||
str1: String,
|
||||
str2: String,
|
||||
onReady: (String) -> Unit,
|
||||
): Int {
|
||||
var result = str1.hashCode()
|
||||
result = 31 * result + str2.hashCode()
|
||||
result = 31 * result + onReady.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
fun decrypt(
|
||||
encryptedContent: String,
|
||||
pubKey: HexKey,
|
||||
signerType: SignerType = SignerType.NIP04_DECRYPT,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey)).fold(
|
||||
onFailure = { },
|
||||
onSuccess = {
|
||||
if (it == null) {
|
||||
openSignerApp(
|
||||
encryptedContent,
|
||||
signerType,
|
||||
pubKey,
|
||||
hashCodeFields(encryptedContent, pubKey, onReady).toString(),
|
||||
onReady,
|
||||
)
|
||||
} else {
|
||||
onReady(it)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun encrypt(
|
||||
decryptedContent: String,
|
||||
pubKey: HexKey,
|
||||
signerType: SignerType = SignerType.NIP04_ENCRYPT,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
getDataFromResolver(signerType, arrayOf(decryptedContent, pubKey)).fold(
|
||||
onFailure = { },
|
||||
onSuccess = {
|
||||
if (it == null) {
|
||||
openSignerApp(
|
||||
decryptedContent,
|
||||
signerType,
|
||||
pubKey,
|
||||
hashCodeFields(decryptedContent, pubKey, onReady).toString(),
|
||||
onReady,
|
||||
)
|
||||
} else {
|
||||
onReady(it)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun decryptZapEvent(
|
||||
event: LnZapRequestEvent,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
getDataFromResolver(SignerType.DECRYPT_ZAP_EVENT, arrayOf(event.toJson(), event.pubKey)).fold(
|
||||
onFailure = { },
|
||||
onSuccess = {
|
||||
if (it == null) {
|
||||
openSignerApp(
|
||||
event.toJson(),
|
||||
SignerType.DECRYPT_ZAP_EVENT,
|
||||
event.pubKey,
|
||||
event.id,
|
||||
onReady,
|
||||
)
|
||||
} else {
|
||||
onReady(it)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun deriveKey(
|
||||
nonce: HexKey,
|
||||
signerType: SignerType = SignerType.DERIVE_KEY,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
getDataFromResolver(signerType, arrayOf(nonce)).fold(
|
||||
onFailure = { },
|
||||
onSuccess = {
|
||||
if (it == null) {
|
||||
openSignerApp(
|
||||
nonce,
|
||||
signerType,
|
||||
"",
|
||||
hashCodeFields(nonce, onReady).toString(),
|
||||
onReady,
|
||||
)
|
||||
} else {
|
||||
onReady(it)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api
|
||||
|
||||
enum class CommandType(
|
||||
val code: String,
|
||||
) {
|
||||
SIGN_EVENT("sign_event"),
|
||||
NIP04_ENCRYPT("nip04_encrypt"),
|
||||
NIP04_DECRYPT("nip04_decrypt"),
|
||||
NIP44_ENCRYPT("nip44_encrypt"),
|
||||
NIP44_DECRYPT("nip44_decrypt"),
|
||||
GET_PUBLIC_KEY("get_public_key"),
|
||||
DECRYPT_ZAP_EVENT("decrypt_zap_event"),
|
||||
DERIVE_KEY("derive_key"),
|
||||
;
|
||||
|
||||
companion object Companion {
|
||||
fun parse(code: String): CommandType? =
|
||||
when (code) {
|
||||
SIGN_EVENT.code -> SIGN_EVENT
|
||||
NIP04_ENCRYPT.code -> NIP04_ENCRYPT
|
||||
NIP04_DECRYPT.code -> NIP04_DECRYPT
|
||||
NIP44_ENCRYPT.code -> NIP44_ENCRYPT
|
||||
NIP44_DECRYPT.code -> NIP44_DECRYPT
|
||||
GET_PUBLIC_KEY.code -> GET_PUBLIC_KEY
|
||||
DECRYPT_ZAP_EVENT.code -> DECRYPT_ZAP_EVENT
|
||||
DERIVE_KEY.code -> DERIVE_KEY
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
|
||||
|
||||
interface RequestAddressed
|
||||
|
||||
sealed class SignerResult<T : IResult> {
|
||||
class Successful<T : IResult>(
|
||||
val result: T,
|
||||
) : SignerResult<T>(),
|
||||
RequestAddressed
|
||||
|
||||
class Rejected<T : IResult> :
|
||||
SignerResult<T>(),
|
||||
RequestAddressed
|
||||
|
||||
class ReceivedButCouldNotPerform<T : IResult>(
|
||||
message: String? = null,
|
||||
) : SignerResult<T>(),
|
||||
RequestAddressed
|
||||
|
||||
class ReceivedButCouldNotParseEventFromResult<T : IResult>(
|
||||
val eventJson: String,
|
||||
) : SignerResult<T>(),
|
||||
RequestAddressed
|
||||
|
||||
class ReceivedButCouldNotVerifyResultingEvent<T : IResult>(
|
||||
val invalidEvent: Event,
|
||||
) : SignerResult<T>(),
|
||||
RequestAddressed
|
||||
|
||||
class ErrorExceptionCallingContentResolver<T : IResult>(
|
||||
val e: Exception? = null,
|
||||
) : SignerResult<T>()
|
||||
|
||||
class RequiresManualApproval<T : IResult> : SignerResult<T>()
|
||||
}
|
||||
|
||||
interface IResult
|
||||
|
||||
data class PubKeyResult(
|
||||
val pubkey: HexKey,
|
||||
val packageName: String,
|
||||
) : IResult
|
||||
|
||||
data class SignResult(
|
||||
val event: Event,
|
||||
) : IResult
|
||||
|
||||
data class EncryptionResult(
|
||||
val ciphertext: String,
|
||||
) : IResult
|
||||
|
||||
data class DecryptionResult(
|
||||
val plaintext: String,
|
||||
) : IResult
|
||||
|
||||
data class ZapEventDecryptionResult(
|
||||
val privateEvent: LnZapPrivateEvent,
|
||||
) : IResult
|
||||
|
||||
data class DerivationResult(
|
||||
val newPrivKey: HexKey,
|
||||
) : IResult
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries
|
||||
|
||||
import android.content.ContentResolver
|
||||
import androidx.core.net.toUri
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.verify
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.ZapEventDecryptionResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.getStringByName
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.query
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
|
||||
import kotlin.text.isNullOrBlank
|
||||
|
||||
class DecryptZapQuery(
|
||||
val loggedInUser: HexKey,
|
||||
val packageName: String,
|
||||
val contentResolver: ContentResolver,
|
||||
) {
|
||||
fun query(event: Event): SignerResult<ZapEventDecryptionResult> =
|
||||
contentResolver.query(
|
||||
"content://$packageName.${CommandType.DECRYPT_ZAP_EVENT}".toUri(),
|
||||
arrayOf(event.toJson(), event.pubKey, loggedInUser),
|
||||
) { cursor ->
|
||||
val decryptedEventAsJson = cursor.getStringByName("result")
|
||||
if (!decryptedEventAsJson.isNullOrBlank()) {
|
||||
if (decryptedEventAsJson.startsWith("{")) {
|
||||
val event = Event.fromJsonOrNull(decryptedEventAsJson) as? LnZapPrivateEvent
|
||||
if (event != null) {
|
||||
if (event.verify()) {
|
||||
SignerResult.Successful(ZapEventDecryptionResult(event))
|
||||
} else {
|
||||
SignerResult.ReceivedButCouldNotVerifyResultingEvent(event)
|
||||
}
|
||||
} else {
|
||||
SignerResult.ReceivedButCouldNotParseEventFromResult(decryptedEventAsJson)
|
||||
}
|
||||
} else {
|
||||
SignerResult.ReceivedButCouldNotPerform(decryptedEventAsJson)
|
||||
}
|
||||
} else {
|
||||
SignerResult.ReceivedButCouldNotPerform(decryptedEventAsJson)
|
||||
}
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries
|
||||
|
||||
import android.content.ContentResolver
|
||||
import androidx.core.net.toUri
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.DerivationResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.getStringByName
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.query
|
||||
|
||||
class DeriveKeyQuery(
|
||||
val loggedInUser: HexKey,
|
||||
val packageName: String,
|
||||
val contentResolver: ContentResolver,
|
||||
) {
|
||||
val uri = "content://$packageName.${CommandType.DERIVE_KEY}".toUri()
|
||||
|
||||
fun query(nonce: HexKey): SignerResult<DerivationResult> =
|
||||
contentResolver.query(
|
||||
uri,
|
||||
arrayOf(nonce, loggedInUser),
|
||||
) { cursor ->
|
||||
val newPrivateKey = cursor.getStringByName("result")
|
||||
if (!newPrivateKey.isNullOrBlank()) {
|
||||
SignerResult.Successful(DerivationResult(newPrivateKey))
|
||||
} else {
|
||||
SignerResult.ReceivedButCouldNotPerform()
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries
|
||||
|
||||
import android.content.ContentResolver
|
||||
import androidx.core.net.toUri
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.PubKeyResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.getStringByName
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.query
|
||||
|
||||
class LoginQuery(
|
||||
val packageName: String,
|
||||
val contentResolver: ContentResolver,
|
||||
) {
|
||||
companion object {
|
||||
val LOGIN = arrayOf("login")
|
||||
}
|
||||
|
||||
val uri = "content://$packageName.${CommandType.GET_PUBLIC_KEY}".toUri()
|
||||
|
||||
fun query(): SignerResult<PubKeyResult> =
|
||||
contentResolver.query(
|
||||
uri,
|
||||
LOGIN,
|
||||
) { cursor ->
|
||||
val pubkeyHex = cursor.getStringByName("result")
|
||||
if (!pubkeyHex.isNullOrBlank()) {
|
||||
SignerResult.Successful(PubKeyResult(pubkeyHex, packageName))
|
||||
} else {
|
||||
SignerResult.ReceivedButCouldNotPerform()
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries
|
||||
|
||||
import android.content.ContentResolver
|
||||
import androidx.core.net.toUri
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.DecryptionResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.getStringByName
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.query
|
||||
|
||||
class Nip04DecryptQuery(
|
||||
val loggedInUser: HexKey,
|
||||
val packageName: String,
|
||||
val contentResolver: ContentResolver,
|
||||
) {
|
||||
val uri = "content://$packageName.${CommandType.NIP04_DECRYPT}".toUri()
|
||||
|
||||
fun query(
|
||||
ciphertext: String,
|
||||
fromPubKey: HexKey,
|
||||
): SignerResult<DecryptionResult> =
|
||||
contentResolver.query(
|
||||
uri,
|
||||
arrayOf(ciphertext, fromPubKey, loggedInUser),
|
||||
) { cursor ->
|
||||
val plaintext = cursor.getStringByName("result")
|
||||
if (!plaintext.isNullOrBlank()) {
|
||||
SignerResult.Successful(DecryptionResult(plaintext))
|
||||
} else {
|
||||
SignerResult.ReceivedButCouldNotPerform()
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries
|
||||
|
||||
import android.content.ContentResolver
|
||||
import androidx.core.net.toUri
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.EncryptionResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.getStringByName
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.query
|
||||
|
||||
class Nip04EncryptQuery(
|
||||
val loggedInUser: HexKey,
|
||||
val packageName: String,
|
||||
val contentResolver: ContentResolver,
|
||||
) {
|
||||
val uri = "content://$packageName.${CommandType.NIP04_ENCRYPT}".toUri()
|
||||
|
||||
fun query(
|
||||
plaintext: String,
|
||||
toPubKey: HexKey,
|
||||
): SignerResult<EncryptionResult> =
|
||||
contentResolver.query(
|
||||
uri,
|
||||
arrayOf(plaintext, toPubKey, loggedInUser),
|
||||
) { cursor ->
|
||||
val ciphertext = cursor.getStringByName("result")
|
||||
if (!ciphertext.isNullOrBlank()) {
|
||||
SignerResult.Successful(EncryptionResult(ciphertext))
|
||||
} else {
|
||||
SignerResult.ReceivedButCouldNotPerform()
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries
|
||||
|
||||
import android.content.ContentResolver
|
||||
import androidx.core.net.toUri
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.DecryptionResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.getStringByName
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.query
|
||||
|
||||
class Nip44DecryptQuery(
|
||||
val loggedInUser: HexKey,
|
||||
val packageName: String,
|
||||
val contentResolver: ContentResolver,
|
||||
) {
|
||||
val uri = "content://$packageName.${CommandType.NIP44_DECRYPT}".toUri()
|
||||
|
||||
fun query(
|
||||
ciphertext: String,
|
||||
fromPubKey: HexKey,
|
||||
): SignerResult<DecryptionResult> =
|
||||
contentResolver.query(
|
||||
uri,
|
||||
arrayOf(ciphertext, fromPubKey, loggedInUser),
|
||||
) { cursor ->
|
||||
val plaintext = cursor.getStringByName("result")
|
||||
if (!plaintext.isNullOrBlank()) {
|
||||
SignerResult.Successful(DecryptionResult(plaintext))
|
||||
} else {
|
||||
SignerResult.ReceivedButCouldNotPerform()
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries
|
||||
|
||||
import android.content.ContentResolver
|
||||
import androidx.core.net.toUri
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.EncryptionResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.getStringByName
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.query
|
||||
|
||||
class Nip44EncryptQuery(
|
||||
val loggedInUser: HexKey,
|
||||
val packageName: String,
|
||||
val contentResolver: ContentResolver,
|
||||
) {
|
||||
val uri = "content://$packageName.${CommandType.NIP44_ENCRYPT}".toUri()
|
||||
|
||||
fun query(
|
||||
plaintext: String,
|
||||
toPubKey: HexKey,
|
||||
): SignerResult<EncryptionResult> =
|
||||
contentResolver.query(
|
||||
uri,
|
||||
arrayOf(plaintext, toPubKey, loggedInUser),
|
||||
) { cursor ->
|
||||
val ciphertext = cursor.getStringByName("result")
|
||||
if (!ciphertext.isNullOrBlank()) {
|
||||
SignerResult.Successful(EncryptionResult(ciphertext))
|
||||
} else {
|
||||
SignerResult.ReceivedButCouldNotPerform()
|
||||
}
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries
|
||||
|
||||
import android.content.ContentResolver
|
||||
import androidx.core.net.toUri
|
||||
import com.vitorpamplona.quartz.EventFactory
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.verify
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.getStringByName
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.query
|
||||
|
||||
class SignQuery(
|
||||
val loggedInUser: HexKey,
|
||||
val packageName: String,
|
||||
val contentResolver: ContentResolver,
|
||||
) {
|
||||
val uri = "content://$packageName.${CommandType.SIGN_EVENT}".toUri()
|
||||
|
||||
fun query(unsignedEvent: Event): SignerResult<SignResult> =
|
||||
contentResolver.query(
|
||||
uri,
|
||||
arrayOf(unsignedEvent.toJson(), unsignedEvent.pubKey, loggedInUser),
|
||||
) { cursor ->
|
||||
val eventJson = cursor.getStringByName("event")
|
||||
if (!eventJson.isNullOrBlank()) {
|
||||
if (eventJson.startsWith("{")) {
|
||||
val event = Event.fromJsonOrNull(eventJson)
|
||||
if (event != null) {
|
||||
if (event.verify()) {
|
||||
SignerResult.Successful(SignResult(event))
|
||||
} else {
|
||||
SignerResult.ReceivedButCouldNotVerifyResultingEvent(event)
|
||||
}
|
||||
} else {
|
||||
SignerResult.ReceivedButCouldNotParseEventFromResult(eventJson)
|
||||
}
|
||||
} else {
|
||||
SignerResult.ReceivedButCouldNotParseEventFromResult(eventJson)
|
||||
}
|
||||
} else {
|
||||
val signature = cursor.getStringByName("result")
|
||||
if (!signature.isNullOrBlank()) {
|
||||
val event =
|
||||
EventFactory.create(
|
||||
id = unsignedEvent.id,
|
||||
pubKey = unsignedEvent.pubKey,
|
||||
createdAt = unsignedEvent.createdAt,
|
||||
kind = unsignedEvent.kind,
|
||||
tags = unsignedEvent.tags,
|
||||
content = unsignedEvent.content,
|
||||
sig = signature,
|
||||
)
|
||||
if (event.verify()) {
|
||||
SignerResult.Successful(SignResult(event))
|
||||
} else {
|
||||
SignerResult.ReceivedButCouldNotVerifyResultingEvent(event)
|
||||
}
|
||||
} else {
|
||||
SignerResult.ReceivedButCouldNotPerform()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.IResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
|
||||
fun <T : IResult> ContentResolver.query(
|
||||
uri: Uri,
|
||||
projection: Array<out String>,
|
||||
map: (cursor: Cursor) -> SignerResult<T>,
|
||||
): SignerResult<T> =
|
||||
try {
|
||||
query(
|
||||
uri,
|
||||
projection,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
).use {
|
||||
if (it != null && it.moveToFirst()) {
|
||||
if (it.getColumnIndex("rejected") > -1) {
|
||||
SignerResult.Rejected()
|
||||
} else {
|
||||
map(it)
|
||||
}
|
||||
} else {
|
||||
SignerResult.RequiresManualApproval()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("ExternalSignerLauncher", "Failed to query the Signer app in the background", e)
|
||||
SignerResult.ErrorExceptionCallingContentResolver(e)
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils
|
||||
|
||||
import android.database.Cursor
|
||||
|
||||
fun Cursor.getStringByName(name: String): String? {
|
||||
val index = getColumnIndex(name)
|
||||
return if (index >= 0) {
|
||||
val result = getString(index)
|
||||
result.ifBlank { null }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.collection.LruCache
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.NewResultProcessor
|
||||
import com.vitorpamplona.quartz.utils.RandomInstance
|
||||
|
||||
class IntentRequestDatabase {
|
||||
private val awaitingRequests = LruCache<String, NewResultProcessor>(2000)
|
||||
private var appLauncher: ((Intent) -> Unit)? = null
|
||||
|
||||
/** Call this function when the launcher becomes available on activity, fragment or compose */
|
||||
fun registerForegroundLauncher(launcher: ((Intent) -> Unit)) {
|
||||
this.appLauncher = launcher
|
||||
}
|
||||
|
||||
/** Call this function when the activity is destroyed or is about to be replaced. */
|
||||
fun unregisterForegroundLauncher(launcher: ((Intent) -> Unit)) {
|
||||
if (this.appLauncher == launcher) {
|
||||
this.appLauncher = null
|
||||
}
|
||||
}
|
||||
|
||||
fun newResponse(data: Intent) {
|
||||
val callId = data.getStringExtra("id")
|
||||
if (callId != null) {
|
||||
awaitingRequests[callId]?.process(data)
|
||||
awaitingRequests.remove(callId)
|
||||
}
|
||||
}
|
||||
|
||||
fun launch(
|
||||
requestIntent: Intent,
|
||||
responseProcessor: NewResultProcessor,
|
||||
) {
|
||||
appLauncher?.let {
|
||||
val callId = RandomInstance.randomChars(32)
|
||||
awaitingRequests.put(callId, responseProcessor)
|
||||
|
||||
requestIntent.putExtra("id", callId)
|
||||
requestIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
it.invoke(requestIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground
|
||||
|
||||
import android.content.Intent
|
||||
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
interface NewResultProcessor {
|
||||
fun process(intent: Intent)
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.core.net.toUri
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
|
||||
|
||||
class DecryptZapRequest {
|
||||
companion object {
|
||||
fun assemble(
|
||||
event: LnZapRequestEvent,
|
||||
loggedInUser: HexKey,
|
||||
packageName: String,
|
||||
): Intent {
|
||||
val intent = Intent(Intent.ACTION_VIEW, "nostrsigner:${event.toJson()}".toUri())
|
||||
intent.`package` = packageName
|
||||
intent.putExtra("type", CommandType.NIP44_DECRYPT.code)
|
||||
intent.putExtra("current_user", loggedInUser)
|
||||
return intent
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.core.net.toUri
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType
|
||||
|
||||
class DeriveKeyRequest {
|
||||
companion object {
|
||||
fun assemble(
|
||||
nonce: HexKey,
|
||||
loggedInUser: HexKey,
|
||||
packageName: String,
|
||||
): Intent {
|
||||
val intent = Intent(Intent.ACTION_VIEW, "nostrsigner:$nonce".toUri())
|
||||
intent.`package` = packageName
|
||||
intent.putExtra("type", CommandType.DERIVE_KEY.code)
|
||||
intent.putExtra("current_user", loggedInUser)
|
||||
return intent
|
||||
}
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.core.net.toUri
|
||||
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
|
||||
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.Permission
|
||||
|
||||
class LoginRequest {
|
||||
companion object {
|
||||
val DefaultPermissions =
|
||||
listOf(
|
||||
Permission(CommandType.SIGN_EVENT, RelayAuthEvent.KIND),
|
||||
Permission(CommandType.NIP04_ENCRYPT),
|
||||
Permission(CommandType.NIP04_DECRYPT),
|
||||
Permission(CommandType.NIP44_DECRYPT),
|
||||
Permission(CommandType.NIP44_DECRYPT),
|
||||
Permission(CommandType.DECRYPT_ZAP_EVENT),
|
||||
)
|
||||
|
||||
fun assemble(permissions: List<Permission> = DefaultPermissions): Intent {
|
||||
val intent = Intent(Intent.ACTION_VIEW, "nostrsigner:".toUri())
|
||||
intent.putExtra("type", CommandType.GET_PUBLIC_KEY.code)
|
||||
intent.putExtra("permissions", JsonMapper.mapper.writeValueAsString(permissions))
|
||||
return intent
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.core.net.toUri
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType
|
||||
|
||||
class Nip04DecryptRequest {
|
||||
companion object {
|
||||
fun assemble(
|
||||
ciphertext: String,
|
||||
fromPubKey: HexKey,
|
||||
loggedInUser: HexKey,
|
||||
packageName: String,
|
||||
): Intent {
|
||||
val intent = Intent(Intent.ACTION_VIEW, "nostrsigner:$ciphertext".toUri())
|
||||
intent.`package` = packageName
|
||||
intent.putExtra("type", CommandType.NIP04_DECRYPT.code)
|
||||
intent.putExtra("pubKey", fromPubKey)
|
||||
intent.putExtra("current_user", loggedInUser)
|
||||
return intent
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.core.net.toUri
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType
|
||||
|
||||
class Nip04EncryptRequest {
|
||||
companion object {
|
||||
fun assemble(
|
||||
plaintext: String,
|
||||
toPubKey: HexKey,
|
||||
loggedInUser: HexKey,
|
||||
packageName: String,
|
||||
): Intent {
|
||||
val intent = Intent(Intent.ACTION_VIEW, "nostrsigner:$plaintext".toUri())
|
||||
intent.`package` = packageName
|
||||
intent.putExtra("type", CommandType.NIP04_ENCRYPT.code)
|
||||
intent.putExtra("pubKey", toPubKey)
|
||||
intent.putExtra("current_user", loggedInUser)
|
||||
return intent
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.core.net.toUri
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType
|
||||
|
||||
class Nip44DecryptRequest {
|
||||
companion object {
|
||||
fun assemble(
|
||||
ciphertext: String,
|
||||
fromPubKey: HexKey,
|
||||
loggedInUser: HexKey,
|
||||
packageName: String,
|
||||
): Intent {
|
||||
val intent = Intent(Intent.ACTION_VIEW, "nostrsigner:$ciphertext".toUri())
|
||||
intent.`package` = packageName
|
||||
intent.putExtra("type", CommandType.NIP44_DECRYPT.code)
|
||||
intent.putExtra("pubKey", fromPubKey)
|
||||
intent.putExtra("current_user", loggedInUser)
|
||||
return intent
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.core.net.toUri
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType
|
||||
|
||||
class Nip44EncryptRequest {
|
||||
companion object {
|
||||
fun assemble(
|
||||
plaintext: String,
|
||||
toPubKey: HexKey,
|
||||
loggedInUser: HexKey,
|
||||
packageName: String,
|
||||
): Intent {
|
||||
val intent = Intent(Intent.ACTION_VIEW, "nostrsigner:$plaintext".toUri())
|
||||
intent.`package` = packageName
|
||||
intent.putExtra("type", CommandType.NIP44_ENCRYPT.code)
|
||||
intent.putExtra("pubKey", toPubKey)
|
||||
intent.putExtra("current_user", loggedInUser)
|
||||
return intent
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.core.net.toUri
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType
|
||||
|
||||
class SignRequest {
|
||||
companion object {
|
||||
fun assemble(
|
||||
event: Event,
|
||||
loggedInUser: HexKey,
|
||||
packageName: String,
|
||||
): Intent {
|
||||
val intent = Intent(Intent.ACTION_VIEW, "nostrsigner:${event.toJson()}".toUri())
|
||||
intent.`package` = packageName
|
||||
intent.putExtra("type", CommandType.SIGN_EVENT.code)
|
||||
intent.putExtra("current_user", loggedInUser)
|
||||
return intent
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses
|
||||
|
||||
import android.content.Intent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.ZapEventDecryptionResult
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
|
||||
|
||||
class DecryptZapResponse {
|
||||
companion object {
|
||||
fun assemble(event: LnZapPrivateEvent): Intent {
|
||||
val intent = Intent()
|
||||
intent.putExtra("result", event.toJson())
|
||||
return intent
|
||||
}
|
||||
|
||||
fun parse(intent: Intent): SignerResult<ZapEventDecryptionResult> {
|
||||
val eventJson = intent.getStringExtra("result")
|
||||
return if (!eventJson.isNullOrBlank()) {
|
||||
if (eventJson.startsWith("{")) {
|
||||
val event = Event.fromJsonOrNull(eventJson) as? LnZapPrivateEvent
|
||||
if (event != null) {
|
||||
SignerResult.Successful(ZapEventDecryptionResult(event))
|
||||
} else {
|
||||
SignerResult.ReceivedButCouldNotParseEventFromResult(eventJson)
|
||||
}
|
||||
} else {
|
||||
SignerResult.ReceivedButCouldNotPerform()
|
||||
}
|
||||
} else {
|
||||
SignerResult.ReceivedButCouldNotPerform()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses
|
||||
|
||||
import android.content.Intent
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.DerivationResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
|
||||
class DeriveKeyResponse {
|
||||
companion object {
|
||||
fun parse(intent: Intent): SignerResult<DerivationResult> {
|
||||
val newPrivateKey = intent.getStringExtra("result")
|
||||
return if (newPrivateKey != null) {
|
||||
SignerResult.Successful(DerivationResult(newPrivateKey))
|
||||
} else {
|
||||
SignerResult.ReceivedButCouldNotPerform()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses
|
||||
|
||||
import android.content.Intent
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.PubKeyResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
|
||||
class LoginResponse {
|
||||
companion object {
|
||||
fun parse(intent: Intent): SignerResult<PubKeyResult> {
|
||||
val pubkey = intent.getStringExtra("result")
|
||||
val packageName = intent.getStringExtra("package")
|
||||
|
||||
return if (pubkey != null && packageName != null) {
|
||||
SignerResult.Successful(
|
||||
PubKeyResult(
|
||||
pubkey = pubkey,
|
||||
packageName = packageName,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
SignerResult.ReceivedButCouldNotPerform()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses
|
||||
|
||||
import android.content.Intent
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.DecryptionResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
|
||||
class Nip04DecryptResponse {
|
||||
companion object {
|
||||
fun parse(intent: Intent): SignerResult<DecryptionResult> {
|
||||
val ciphertext = intent.getStringExtra("result")
|
||||
return if (ciphertext != null) {
|
||||
SignerResult.Successful(DecryptionResult(ciphertext))
|
||||
} else {
|
||||
SignerResult.ReceivedButCouldNotPerform()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses
|
||||
|
||||
import android.content.Intent
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.EncryptionResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
|
||||
class Nip04EncryptResponse {
|
||||
companion object {
|
||||
fun parse(intent: Intent): SignerResult<EncryptionResult> {
|
||||
val ciphertext = intent.getStringExtra("result")
|
||||
return if (ciphertext != null) {
|
||||
SignerResult.Successful(EncryptionResult(ciphertext))
|
||||
} else {
|
||||
SignerResult.ReceivedButCouldNotPerform()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses
|
||||
|
||||
import android.content.Intent
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.DecryptionResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
|
||||
class Nip44DecryptResponse {
|
||||
companion object {
|
||||
fun parse(intent: Intent): SignerResult<DecryptionResult> {
|
||||
val ciphertext = intent.getStringExtra("result")
|
||||
return if (ciphertext != null) {
|
||||
SignerResult.Successful(DecryptionResult(ciphertext))
|
||||
} else {
|
||||
SignerResult.ReceivedButCouldNotPerform()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses
|
||||
|
||||
import android.content.Intent
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.EncryptionResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
|
||||
class Nip44EncryptResponse {
|
||||
companion object {
|
||||
fun parse(intent: Intent): SignerResult<EncryptionResult> {
|
||||
val ciphertext = intent.getStringExtra("result")
|
||||
return if (ciphertext != null) {
|
||||
SignerResult.Successful(EncryptionResult(ciphertext))
|
||||
} else {
|
||||
SignerResult.ReceivedButCouldNotPerform()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses
|
||||
|
||||
import android.content.Intent
|
||||
import com.vitorpamplona.quartz.EventFactory
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.verify
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
|
||||
class SignResponse {
|
||||
companion object {
|
||||
fun parse(
|
||||
intent: Intent,
|
||||
unsignedEvent: Event,
|
||||
): SignerResult<SignResult> {
|
||||
val eventJson = intent.getStringExtra("event")
|
||||
return if (eventJson != null) {
|
||||
if (eventJson.startsWith("{")) {
|
||||
val event = Event.fromJsonOrNull(eventJson)
|
||||
if (event != null) {
|
||||
if (event.verify()) {
|
||||
SignerResult.Successful(SignResult(event))
|
||||
} else {
|
||||
SignerResult.ReceivedButCouldNotVerifyResultingEvent(event)
|
||||
}
|
||||
} else {
|
||||
SignerResult.ReceivedButCouldNotParseEventFromResult(eventJson)
|
||||
}
|
||||
} else {
|
||||
SignerResult.ReceivedButCouldNotPerform(eventJson)
|
||||
}
|
||||
} else {
|
||||
val signature = intent.getStringExtra("result")
|
||||
if (signature != null && signature.length == 128) {
|
||||
val event =
|
||||
EventFactory.create(
|
||||
id = unsignedEvent.id,
|
||||
pubKey = unsignedEvent.pubKey,
|
||||
createdAt = unsignedEvent.createdAt,
|
||||
kind = unsignedEvent.kind,
|
||||
tags = unsignedEvent.tags,
|
||||
content = unsignedEvent.content,
|
||||
sig = signature,
|
||||
)
|
||||
if (event.verify()) {
|
||||
SignerResult.Successful(SignResult(event))
|
||||
} else {
|
||||
SignerResult.ReceivedButCouldNotVerifyResultingEvent(event)
|
||||
}
|
||||
} else {
|
||||
SignerResult.ReceivedButCouldNotPerform(signature)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors
|
||||
|
||||
import android.content.Intent
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.ZapEventDecryptionResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.NewResultProcessor
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.DecryptZapResponse
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
|
||||
|
||||
class DecryptZapResultProcessor(
|
||||
val onReady: (LnZapPrivateEvent) -> Unit,
|
||||
) : NewResultProcessor {
|
||||
override fun process(intent: Intent) {
|
||||
val foregroundResult = DecryptZapResponse.parse(intent)
|
||||
|
||||
if (foregroundResult is SignerResult.Successful<ZapEventDecryptionResult>) {
|
||||
onReady(foregroundResult.result.privateEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors
|
||||
|
||||
import android.content.Intent
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.DerivationResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.NewResultProcessor
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.DeriveKeyResponse
|
||||
|
||||
class DeriveKeyResultProcessor(
|
||||
val onReady: (String) -> Unit,
|
||||
) : NewResultProcessor {
|
||||
override fun process(intent: Intent) {
|
||||
val foregroundResult = DeriveKeyResponse.parse(intent)
|
||||
|
||||
if (foregroundResult is SignerResult.Successful<DerivationResult>) {
|
||||
onReady(foregroundResult.result.newPrivKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors
|
||||
|
||||
import android.content.Intent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.PubKeyResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.NewResultProcessor
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.LoginResponse
|
||||
|
||||
class LoginResultProcessor(
|
||||
val onReady: (HexKey, String) -> Unit,
|
||||
) : NewResultProcessor {
|
||||
override fun process(intent: Intent) {
|
||||
val foregroundResult = LoginResponse.parse(intent)
|
||||
|
||||
if (foregroundResult is SignerResult.Successful<PubKeyResult>) {
|
||||
onReady(foregroundResult.result.pubkey, foregroundResult.result.packageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors
|
||||
|
||||
import android.content.Intent
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.DecryptionResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.NewResultProcessor
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.Nip04DecryptResponse
|
||||
|
||||
class Nip04DecryptResultProcessor(
|
||||
val onReady: (String) -> Unit,
|
||||
) : NewResultProcessor {
|
||||
override fun process(intent: Intent) {
|
||||
val foregroundResult = Nip04DecryptResponse.parse(intent)
|
||||
|
||||
if (foregroundResult is SignerResult.Successful<DecryptionResult>) {
|
||||
onReady(foregroundResult.result.plaintext)
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors
|
||||
|
||||
import android.content.Intent
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.EncryptionResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.NewResultProcessor
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.Nip04EncryptResponse
|
||||
|
||||
class Nip04EncryptResultProcessor(
|
||||
val onReady: (String) -> Unit,
|
||||
) : NewResultProcessor {
|
||||
override fun process(intent: Intent) {
|
||||
val foregroundResult = Nip04EncryptResponse.parse(intent)
|
||||
|
||||
if (foregroundResult is SignerResult.Successful<EncryptionResult>) {
|
||||
onReady(foregroundResult.result.ciphertext)
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors
|
||||
|
||||
import android.content.Intent
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.DecryptionResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.NewResultProcessor
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.Nip44DecryptResponse
|
||||
|
||||
class Nip44DecryptResultProcessor(
|
||||
val onReady: (String) -> Unit,
|
||||
) : NewResultProcessor {
|
||||
override fun process(intent: Intent) {
|
||||
val foregroundResult = Nip44DecryptResponse.parse(intent)
|
||||
|
||||
if (foregroundResult is SignerResult.Successful<DecryptionResult>) {
|
||||
onReady(foregroundResult.result.plaintext)
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors
|
||||
|
||||
import android.content.Intent
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.EncryptionResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.NewResultProcessor
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.Nip44EncryptResponse
|
||||
|
||||
class Nip44EncryptResultProcessor(
|
||||
val onReady: (String) -> Unit,
|
||||
) : NewResultProcessor {
|
||||
override fun process(intent: Intent) {
|
||||
val foregroundResult = Nip44EncryptResponse.parse(intent)
|
||||
if (foregroundResult is SignerResult.Successful<EncryptionResult>) {
|
||||
onReady(foregroundResult.result.ciphertext)
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors
|
||||
|
||||
import android.content.Intent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.NewResultProcessor
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.SignResponse
|
||||
|
||||
class SignResultProcessor(
|
||||
val unsignedEvent: Event,
|
||||
val onReady: (Event) -> Unit,
|
||||
) : NewResultProcessor {
|
||||
override fun process(intent: Intent) {
|
||||
val foregroundResult = SignResponse.parse(intent, unsignedEvent)
|
||||
|
||||
if (foregroundResult is SignerResult.Successful<SignResult>) {
|
||||
onReady(foregroundResult.result.event)
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.permission
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType
|
||||
|
||||
class Permission(
|
||||
val type: CommandType?,
|
||||
val kind: Int? = null,
|
||||
) {
|
||||
fun toJson(): String = JsonMapper.mapper.writeValueAsString(this)
|
||||
|
||||
companion object {
|
||||
fun fromJson(json: String): Permission = JsonMapper.mapper.readValue(json, Permission::class.java)
|
||||
|
||||
fun fromJsonArray(json: String): Array<Permission> = JsonMapper.mapper.readValue(json, Array<Permission>::class.java)
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.permission
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType
|
||||
|
||||
class PermissionDeserializer : StdDeserializer<Permission>(Permission::class.java) {
|
||||
override fun deserialize(
|
||||
jp: JsonParser,
|
||||
ctxt: DeserializationContext,
|
||||
): Permission {
|
||||
val jsonObject: JsonNode = jp.codec.readTree(jp)
|
||||
return Permission(
|
||||
type = CommandType.parse(jsonObject.get("type").asText()),
|
||||
kind = jsonObject.get("kind")?.asInt(),
|
||||
)
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.api.permission
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.databind.SerializerProvider
|
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer
|
||||
|
||||
class PermissionSerializer : StdSerializer<Permission>(Permission::class.java) {
|
||||
override fun serialize(
|
||||
permission: Permission,
|
||||
gen: JsonGenerator,
|
||||
provider: SerializerProvider,
|
||||
) {
|
||||
gen.writeStartObject()
|
||||
permission.type.let { gen.writeStringField("type", it?.code) }
|
||||
if (permission.kind != null) {
|
||||
gen.writeNumberField("kind", permission.kind)
|
||||
} else {
|
||||
gen.writeNullField("kind")
|
||||
}
|
||||
gen.writeEndObject()
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.client
|
||||
|
||||
import android.content.Intent
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.PubKeyResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.LoginRequest
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.LoginResponse
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.Permission
|
||||
|
||||
object ExternalSignerLogin {
|
||||
fun createIntent(permissions: List<Permission> = LoginRequest.DefaultPermissions): Intent {
|
||||
val intent = LoginRequest.assemble(permissions)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
return intent
|
||||
}
|
||||
|
||||
fun parseResult(data: Intent): SignerResult<PubKeyResult> = LoginResponse.parse(data)
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.client
|
||||
|
||||
import android.content.Intent
|
||||
|
||||
interface IActivityLauncher {
|
||||
fun registerForegroundLauncher(launcher: ((Intent) -> Unit))
|
||||
|
||||
fun unregisterForegroundLauncher(launcher: ((Intent) -> Unit))
|
||||
|
||||
fun newResponse(data: Intent)
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.client
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.net.toUri
|
||||
|
||||
@SuppressLint("QueryPermissionsNeeded")
|
||||
fun isExternalSignerInstalled(context: Context): Boolean =
|
||||
context.packageManager
|
||||
.queryIntentActivities(
|
||||
Intent().apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
data = "nostrsigner:".toUri()
|
||||
},
|
||||
0,
|
||||
).isNotEmpty()
|
||||
+52
-72
@@ -18,23 +18,42 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.client
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.quartz.EventFactory
|
||||
import android.content.ContentResolver
|
||||
import android.content.Intent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.client.handlers.BackgroundRequestHandler
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.client.handlers.ForegroundRequestHandler
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
|
||||
|
||||
class NostrSignerExternal(
|
||||
pubKey: HexKey,
|
||||
val launcher: ExternalSignerLauncher,
|
||||
) : NostrSigner(pubKey) {
|
||||
packageName: String,
|
||||
contentResolver: ContentResolver,
|
||||
) : NostrSigner(pubKey),
|
||||
IActivityLauncher {
|
||||
override fun isWriteable(): Boolean = true
|
||||
|
||||
val backgroundQuery = BackgroundRequestHandler(pubKey, packageName, contentResolver)
|
||||
val foregroundQuery = ForegroundRequestHandler(pubKey, packageName)
|
||||
|
||||
override fun registerForegroundLauncher(launcher: ((Intent) -> Unit)) {
|
||||
this.foregroundQuery.launcher.registerForegroundLauncher(launcher)
|
||||
}
|
||||
|
||||
override fun unregisterForegroundLauncher(launcher: ((Intent) -> Unit)) {
|
||||
this.foregroundQuery.launcher.unregisterForegroundLauncher(launcher)
|
||||
}
|
||||
|
||||
override fun newResponse(data: Intent) {
|
||||
this.foregroundQuery.launcher.newResponse(data)
|
||||
}
|
||||
|
||||
override fun <T : Event> sign(
|
||||
createdAt: Long,
|
||||
kind: Int,
|
||||
@@ -42,7 +61,7 @@ class NostrSignerExternal(
|
||||
content: String,
|
||||
onReady: (T) -> Unit,
|
||||
) {
|
||||
val event =
|
||||
val unsignedEvent =
|
||||
Event(
|
||||
id = EventHasher.hashId(pubKey, createdAt, kind, tags, content),
|
||||
pubKey = pubKey,
|
||||
@@ -53,109 +72,70 @@ class NostrSignerExternal(
|
||||
sig = "",
|
||||
)
|
||||
|
||||
launcher.openSigner(event) { signature ->
|
||||
if (signature.startsWith("{")) {
|
||||
val localEvent = Event.fromJson(signature)
|
||||
(
|
||||
EventFactory.create(
|
||||
localEvent.id,
|
||||
localEvent.pubKey,
|
||||
localEvent.createdAt,
|
||||
localEvent.kind,
|
||||
localEvent.tags,
|
||||
localEvent.content,
|
||||
localEvent.sig,
|
||||
) as? T?
|
||||
)?.let { onReady(it) }
|
||||
} else {
|
||||
(
|
||||
EventFactory.create(
|
||||
event.id,
|
||||
event.pubKey,
|
||||
event.createdAt,
|
||||
event.kind,
|
||||
event.tags,
|
||||
event.content,
|
||||
signature.split("-")[0],
|
||||
) as? T?
|
||||
)?.let { onReady(it) }
|
||||
}
|
||||
val newOnReady: (Event) -> Unit = { result ->
|
||||
(result as? T)?.let(onReady)
|
||||
}
|
||||
|
||||
if (!backgroundQuery.sign(unsignedEvent, newOnReady)) {
|
||||
foregroundQuery.sign(unsignedEvent, newOnReady)
|
||||
}
|
||||
}
|
||||
|
||||
override fun nip04Encrypt(
|
||||
decryptedContent: String,
|
||||
plaintext: String,
|
||||
toPublicKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
launcher.encrypt(
|
||||
decryptedContent,
|
||||
toPublicKey,
|
||||
SignerType.NIP04_ENCRYPT,
|
||||
onReady,
|
||||
)
|
||||
if (!backgroundQuery.nip04Encrypt(plaintext, toPublicKey, onReady)) {
|
||||
foregroundQuery.nip04Encrypt(plaintext, toPublicKey, onReady)
|
||||
}
|
||||
}
|
||||
|
||||
override fun nip04Decrypt(
|
||||
encryptedContent: String,
|
||||
ciphertext: String,
|
||||
fromPublicKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
launcher.decrypt(
|
||||
encryptedContent,
|
||||
fromPublicKey,
|
||||
SignerType.NIP04_DECRYPT,
|
||||
onReady,
|
||||
)
|
||||
if (!backgroundQuery.nip04Decrypt(ciphertext, fromPublicKey, onReady)) {
|
||||
foregroundQuery.nip04Decrypt(ciphertext, fromPublicKey, onReady)
|
||||
}
|
||||
}
|
||||
|
||||
override fun nip44Encrypt(
|
||||
decryptedContent: String,
|
||||
plaintext: String,
|
||||
toPublicKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
launcher.encrypt(
|
||||
decryptedContent,
|
||||
toPublicKey,
|
||||
SignerType.NIP44_ENCRYPT,
|
||||
onReady,
|
||||
)
|
||||
if (!backgroundQuery.nip44Encrypt(plaintext, toPublicKey, onReady)) {
|
||||
foregroundQuery.nip44Encrypt(plaintext, toPublicKey, onReady)
|
||||
}
|
||||
}
|
||||
|
||||
override fun nip44Decrypt(
|
||||
encryptedContent: String,
|
||||
ciphertext: String,
|
||||
fromPublicKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
) {
|
||||
launcher.decrypt(
|
||||
encryptedContent,
|
||||
fromPublicKey,
|
||||
SignerType.NIP44_DECRYPT,
|
||||
onReady,
|
||||
)
|
||||
if (!backgroundQuery.nip44Decrypt(ciphertext, fromPublicKey, onReady)) {
|
||||
foregroundQuery.nip44Decrypt(ciphertext, fromPublicKey, onReady)
|
||||
}
|
||||
}
|
||||
|
||||
override fun deriveKey(
|
||||
nonce: HexKey,
|
||||
onReady: (HexKey) -> Unit,
|
||||
) {
|
||||
launcher.deriveKey(
|
||||
nonce,
|
||||
SignerType.DERIVE_KEY,
|
||||
onReady,
|
||||
)
|
||||
if (!backgroundQuery.deriveKey(nonce, onReady)) {
|
||||
foregroundQuery.deriveKey(nonce, onReady)
|
||||
}
|
||||
}
|
||||
|
||||
override fun decryptZapEvent(
|
||||
event: LnZapRequestEvent,
|
||||
onReady: (LnZapPrivateEvent) -> Unit,
|
||||
) {
|
||||
launcher.decryptZapEvent(event) { jsonEvent ->
|
||||
try {
|
||||
(Event.fromJson(jsonEvent) as? LnZapPrivateEvent)?.let { onReady(it) }
|
||||
} catch (e: Exception) {
|
||||
Log.e("NostrExternalSigner", "Unable to parse returned decrypted Zap: $jsonEvent")
|
||||
}
|
||||
if (!backgroundQuery.decryptZapEvent(event, onReady)) {
|
||||
foregroundQuery.decryptZapEvent(event, onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.client.handlers
|
||||
|
||||
import android.content.ContentResolver
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.DecryptionResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.DerivationResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.EncryptionResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.PubKeyResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.RequestAddressed
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.ZapEventDecryptionResult
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.DecryptZapQuery
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.DeriveKeyQuery
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.LoginQuery
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.Nip04DecryptQuery
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.Nip04EncryptQuery
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.Nip44DecryptQuery
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.Nip44EncryptQuery
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.SignQuery
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
|
||||
|
||||
class BackgroundRequestHandler(
|
||||
loggedInUser: HexKey,
|
||||
packageName: String,
|
||||
contentResolver: ContentResolver,
|
||||
) {
|
||||
val login = LoginQuery(packageName, contentResolver)
|
||||
val sign = SignQuery(loggedInUser, packageName, contentResolver)
|
||||
val nip04Encrypt = Nip04EncryptQuery(loggedInUser, packageName, contentResolver)
|
||||
val nip04Decrypt = Nip04DecryptQuery(loggedInUser, packageName, contentResolver)
|
||||
val nip44Encrypt = Nip44EncryptQuery(loggedInUser, packageName, contentResolver)
|
||||
val nip44Decrypt = Nip44DecryptQuery(loggedInUser, packageName, contentResolver)
|
||||
val decryptZap = DecryptZapQuery(loggedInUser, packageName, contentResolver)
|
||||
val deriveKey = DeriveKeyQuery(loggedInUser, packageName, contentResolver)
|
||||
|
||||
fun login(onReady: (HexKey) -> Unit): Boolean {
|
||||
val backgroundResult = login.query()
|
||||
|
||||
if (backgroundResult is SignerResult.Successful<PubKeyResult>) {
|
||||
onReady(backgroundResult.result.pubkey)
|
||||
}
|
||||
|
||||
return backgroundResult is RequestAddressed
|
||||
}
|
||||
|
||||
fun sign(
|
||||
unsignedEvent: Event,
|
||||
onReady: (Event) -> Unit,
|
||||
): Boolean {
|
||||
val backgroundResult = sign.query(unsignedEvent)
|
||||
|
||||
if (backgroundResult is SignerResult.Successful<SignResult>) {
|
||||
onReady(backgroundResult.result.event)
|
||||
}
|
||||
|
||||
return backgroundResult is RequestAddressed
|
||||
}
|
||||
|
||||
fun nip04Encrypt(
|
||||
plaintext: String,
|
||||
toPubKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
): Boolean {
|
||||
val backgroundResult = nip04Encrypt.query(plaintext, toPubKey)
|
||||
|
||||
if (backgroundResult is SignerResult.Successful<EncryptionResult>) {
|
||||
onReady(backgroundResult.result.ciphertext)
|
||||
}
|
||||
|
||||
return backgroundResult is RequestAddressed
|
||||
}
|
||||
|
||||
fun nip04Decrypt(
|
||||
ciphertext: String,
|
||||
fromPubKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
): Boolean {
|
||||
val backgroundResult = nip04Decrypt.query(ciphertext, fromPubKey)
|
||||
|
||||
if (backgroundResult is SignerResult.Successful<DecryptionResult>) {
|
||||
onReady(backgroundResult.result.plaintext)
|
||||
}
|
||||
|
||||
return backgroundResult is RequestAddressed
|
||||
}
|
||||
|
||||
fun nip44Encrypt(
|
||||
plaintext: String,
|
||||
toPubKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
): Boolean {
|
||||
val backgroundResult = nip44Encrypt.query(plaintext, toPubKey)
|
||||
|
||||
if (backgroundResult is SignerResult.Successful<EncryptionResult>) {
|
||||
onReady(backgroundResult.result.ciphertext)
|
||||
}
|
||||
|
||||
return backgroundResult is RequestAddressed
|
||||
}
|
||||
|
||||
fun nip44Decrypt(
|
||||
ciphertext: String,
|
||||
fromPubKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
): Boolean {
|
||||
val backgroundResult = nip44Decrypt.query(ciphertext, fromPubKey)
|
||||
|
||||
if (backgroundResult is SignerResult.Successful<DecryptionResult>) {
|
||||
onReady(backgroundResult.result.plaintext)
|
||||
}
|
||||
|
||||
return backgroundResult is RequestAddressed
|
||||
}
|
||||
|
||||
fun decryptZapEvent(
|
||||
event: LnZapRequestEvent,
|
||||
onReady: (LnZapPrivateEvent) -> Unit,
|
||||
): Boolean {
|
||||
val backgroundResult = decryptZap.query(event)
|
||||
|
||||
if (backgroundResult is SignerResult.Successful<ZapEventDecryptionResult>) {
|
||||
onReady(backgroundResult.result.privateEvent)
|
||||
}
|
||||
|
||||
return backgroundResult is RequestAddressed
|
||||
}
|
||||
|
||||
fun deriveKey(
|
||||
nonce: HexKey,
|
||||
onReady: (HexKey) -> Unit,
|
||||
): Boolean {
|
||||
val backgroundResult = deriveKey.query(nonce)
|
||||
|
||||
if (backgroundResult is SignerResult.Successful<DerivationResult>) {
|
||||
onReady(backgroundResult.result.newPrivKey)
|
||||
}
|
||||
|
||||
return backgroundResult is RequestAddressed
|
||||
}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip55AndroidSigner.client.handlers
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.IntentRequestDatabase
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.DecryptZapRequest
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.DeriveKeyRequest
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.Nip04DecryptRequest
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.Nip04EncryptRequest
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.Nip44DecryptRequest
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.Nip44EncryptRequest
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.SignRequest
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors.DecryptZapResultProcessor
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors.DeriveKeyResultProcessor
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors.Nip04DecryptResultProcessor
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors.Nip04EncryptResultProcessor
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors.Nip44DecryptResultProcessor
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors.Nip44EncryptResultProcessor
|
||||
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors.SignResultProcessor
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
|
||||
|
||||
class ForegroundRequestHandler(
|
||||
val loggedInUser: HexKey,
|
||||
val packageName: String,
|
||||
) {
|
||||
val launcher = IntentRequestDatabase()
|
||||
|
||||
fun sign(
|
||||
unsignedEvent: Event,
|
||||
onReady: (Event) -> Unit,
|
||||
) = launcher.launch(
|
||||
SignRequest.assemble(unsignedEvent, loggedInUser, packageName),
|
||||
SignResultProcessor(unsignedEvent, onReady),
|
||||
)
|
||||
|
||||
fun nip04Encrypt(
|
||||
plaintext: String,
|
||||
toPubKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
) = launcher.launch(
|
||||
Nip04EncryptRequest.assemble(plaintext, toPubKey, loggedInUser, packageName),
|
||||
Nip04EncryptResultProcessor(onReady),
|
||||
)
|
||||
|
||||
fun nip04Decrypt(
|
||||
ciphertext: String,
|
||||
fromPubKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
) = launcher.launch(
|
||||
Nip04DecryptRequest.assemble(ciphertext, fromPubKey, loggedInUser, packageName),
|
||||
Nip04DecryptResultProcessor(onReady),
|
||||
)
|
||||
|
||||
fun nip44Encrypt(
|
||||
plaintext: String,
|
||||
toPubKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
) = launcher.launch(
|
||||
Nip44EncryptRequest.assemble(plaintext, toPubKey, loggedInUser, packageName),
|
||||
Nip44EncryptResultProcessor(onReady),
|
||||
)
|
||||
|
||||
fun nip44Decrypt(
|
||||
ciphertext: String,
|
||||
fromPubKey: HexKey,
|
||||
onReady: (String) -> Unit,
|
||||
) = launcher.launch(
|
||||
Nip44DecryptRequest.assemble(ciphertext, fromPubKey, loggedInUser, packageName),
|
||||
Nip44DecryptResultProcessor(onReady),
|
||||
)
|
||||
|
||||
fun decryptZapEvent(
|
||||
event: LnZapRequestEvent,
|
||||
onReady: (LnZapPrivateEvent) -> Unit,
|
||||
) = launcher.launch(
|
||||
DecryptZapRequest.assemble(event, loggedInUser, packageName),
|
||||
DecryptZapResultProcessor(onReady),
|
||||
)
|
||||
|
||||
fun deriveKey(
|
||||
nonce: HexKey,
|
||||
onReady: (HexKey) -> Unit,
|
||||
) = launcher.launch(
|
||||
DeriveKeyRequest.assemble(nonce, loggedInUser, packageName),
|
||||
DeriveKeyResultProcessor(onReady),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user