feat(multi-account): extract SignerType, AccountInfo, AccountStorage to commons

Phase 1 of multi-account support. Move account types to commons/commonMain
so both Android and Desktop share the same data model:

- SignerType sealed class (Internal, Remote, ViewOnly)
- AccountInfo data class (npub, signerType, isTransient)
- AccountStorage interface (loadAccounts, saveAccount, etc.)

Desktop AccountManager now imports SignerType from commons instead of
defining its own. All existing tests updated with new import path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
nrobi144
2026-04-24 07:29:52 +03:00
parent 3bfeac885a
commit aa33b7d271
9 changed files with 130 additions and 10 deletions
@@ -0,0 +1,34 @@
/*
* 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.commons.model.account
import androidx.compose.runtime.Immutable
/**
* Lightweight account metadata for account listing and switching.
* Does NOT contain private keys — those are stored in SecureKeyStorage.
*/
@Immutable
data class AccountInfo(
val npub: String,
val signerType: SignerType,
val isTransient: Boolean = false,
)
@@ -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.amethyst.commons.model.account
/**
* Platform-agnostic interface for persisting account metadata.
* Implementations:
* - Android: LocalPreferences (EncryptedSharedPreferences)
* - Desktop: DesktopAccountStorage (encrypted JSON file)
*
* Does NOT handle private key storage — that's SecureKeyStorage's job.
* Constructor-injected, not expect/actual.
*/
interface AccountStorage {
/** Load all saved account metadata */
suspend fun loadAccounts(): List<AccountInfo>
/** Save or update account metadata */
suspend fun saveAccount(info: AccountInfo)
/** Delete account metadata by npub */
suspend fun deleteAccount(npub: String)
/** Get the npub of the currently active account, or null if none */
suspend fun currentAccount(): String?
/** Set which account is currently active */
suspend fun setCurrentAccount(npub: String)
}
@@ -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.amethyst.commons.model.account
import androidx.compose.runtime.Immutable
/**
* Represents how an account's signing is handled.
* Sealed class (not enum) because Remote carries additional data.
*/
@Immutable
sealed class SignerType {
/** Account has a local private key (nsec) */
data object Internal : SignerType()
/** Account uses a NIP-46 remote signer (bunker) */
@Immutable
data class Remote(
val bunkerUri: String,
) : SignerType()
/** Account is view-only (npub only, no signing capability) */
data object ViewOnly : SignerType()
}