feat(android): update theme to dark gray & gold, fix JoinKeygen/CoSign flows
Theme changes: - Replace green theme with dark gray & gold color scheme - Primary color: Gold (#D4AF37) - Background: Dark gray (#1A1A1A) - Surface: Medium gray (#2D2D2D) - Disable dynamic colors to enforce custom theme - Default to dark theme for best visual impact - Update success indicators from green to gold across screens JoinKeygen flow fixes (100% Electron compatible): - Add onResetState callback for proper state reset - Cancel in confirm/joining/progress resets to input state (stays on page) - Two-step flow: joinKeygenSessionViaGrpc + executeKeygenAsJoiner - Wait for session_started event before executing keygen CoSign flow fixes (100% Electron compatible): - Add onResetState callback and QR scanner support - Add three-button layout (Cancel, Back, Join) in select_share step - Two-step flow: joinSignSessionViaGrpc + executeSignAsJoiner - If session already in_progress, trigger sign immediately (Solution B) - Wait for session_started event otherwise Repository changes: - Add joinKeygenSessionViaGrpc and executeKeygenAsJoiner methods - Add joinSignSessionViaGrpc and executeSignAsJoiner methods - Add JoinKeygenViaGrpcResult and JoinSignViaGrpcResult data classes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
2b0920f9b1
commit
d8be40b8b0
|
|
@ -249,6 +249,9 @@ fun TssPartyApp(
|
||||||
viewModel.cancelSession()
|
viewModel.cancelSession()
|
||||||
viewModel.clearError()
|
viewModel.clearError()
|
||||||
viewModel.resetSessionState()
|
viewModel.resetSessionState()
|
||||||
|
navController.navigate(BottomNavItem.Wallets.route) {
|
||||||
|
popUpTo(BottomNavItem.Wallets.route) { inclusive = true }
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onBackToHome = {
|
onBackToHome = {
|
||||||
viewModel.resetSessionState()
|
viewModel.resetSessionState()
|
||||||
|
|
@ -290,6 +293,16 @@ fun TssPartyApp(
|
||||||
viewModel.joinKeygen(inviteCode, password)
|
viewModel.joinKeygen(inviteCode, password)
|
||||||
},
|
},
|
||||||
onCancel = {
|
onCancel = {
|
||||||
|
// Cancel from input screen - navigate away
|
||||||
|
viewModel.cancelSession()
|
||||||
|
viewModel.clearError()
|
||||||
|
viewModel.resetJoinKeygenState()
|
||||||
|
navController.navigate(BottomNavItem.Wallets.route) {
|
||||||
|
popUpTo(BottomNavItem.Wallets.route) { inclusive = true }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onResetState = {
|
||||||
|
// Reset from confirm/joining/progress screens - stay on page
|
||||||
viewModel.cancelSession()
|
viewModel.cancelSession()
|
||||||
viewModel.clearError()
|
viewModel.clearError()
|
||||||
viewModel.resetJoinKeygenState()
|
viewModel.resetJoinKeygenState()
|
||||||
|
|
@ -335,6 +348,16 @@ fun TssPartyApp(
|
||||||
viewModel.joinSign(inviteCode, shareId, password)
|
viewModel.joinSign(inviteCode, shareId, password)
|
||||||
},
|
},
|
||||||
onCancel = {
|
onCancel = {
|
||||||
|
// Cancel from input screen - navigate away
|
||||||
|
viewModel.cancelSession()
|
||||||
|
viewModel.clearError()
|
||||||
|
viewModel.resetCoSignState()
|
||||||
|
navController.navigate(BottomNavItem.Wallets.route) {
|
||||||
|
popUpTo(BottomNavItem.Wallets.route) { inclusive = true }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onResetState = {
|
||||||
|
// Reset from select_share/joining/signing screens - stay on page
|
||||||
viewModel.cancelSession()
|
viewModel.cancelSession()
|
||||||
viewModel.clearError()
|
viewModel.clearError()
|
||||||
viewModel.resetCoSignState()
|
viewModel.resetCoSignState()
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,8 @@ class GrpcClient @Inject constructor() {
|
||||||
partyIndex = response.partyIndex,
|
partyIndex = response.partyIndex,
|
||||||
participants = participants,
|
participants = participants,
|
||||||
messageHash = if (sessionInfo.messageHash.isEmpty) null
|
messageHash = if (sessionInfo.messageHash.isEmpty) null
|
||||||
else Base64.encodeToString(sessionInfo.messageHash.toByteArray(), Base64.NO_WRAP)
|
else Base64.encodeToString(sessionInfo.messageHash.toByteArray(), Base64.NO_WRAP),
|
||||||
|
sessionStatus = if (sessionInfo.status.isNullOrEmpty()) null else sessionInfo.status
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -305,7 +306,8 @@ data class JoinSessionData(
|
||||||
val thresholdT: Int,
|
val thresholdT: Int,
|
||||||
val partyIndex: Int,
|
val partyIndex: Int,
|
||||||
val participants: List<Participant>,
|
val participants: List<Participant>,
|
||||||
val messageHash: String?
|
val messageHash: String?,
|
||||||
|
val sessionStatus: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ class TssRepository @Inject constructor(
|
||||||
|
|
||||||
private var partyId: String = UUID.randomUUID().toString()
|
private var partyId: String = UUID.randomUUID().toString()
|
||||||
private var messageCollectionJob: Job? = null
|
private var messageCollectionJob: Job? = null
|
||||||
|
private var sessionEventJob: Job? = null
|
||||||
|
|
||||||
// Account service URL (configurable via settings)
|
// Account service URL (configurable via settings)
|
||||||
private var accountServiceUrl: String = "https://rwaapi.szaiai.com"
|
private var accountServiceUrl: String = "https://rwaapi.szaiai.com"
|
||||||
|
|
@ -65,17 +66,65 @@ class TssRepository @Inject constructor(
|
||||||
*/
|
*/
|
||||||
fun disconnect() {
|
fun disconnect() {
|
||||||
messageCollectionJob?.cancel()
|
messageCollectionJob?.cancel()
|
||||||
|
sessionEventJob?.cancel()
|
||||||
grpcClient.disconnect()
|
grpcClient.disconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Session event callback (set by ViewModel)
|
||||||
|
private var sessionEventCallback: ((SessionEventData) -> Unit)? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register this party with the router
|
* Register this party with the router
|
||||||
|
* Also subscribes to session events (matching Electron behavior)
|
||||||
*/
|
*/
|
||||||
suspend fun registerParty(): String {
|
suspend fun registerParty(): String {
|
||||||
grpcClient.registerParty(partyId, "temporary", "1.0.0")
|
grpcClient.registerParty(partyId, "temporary", "1.0.0")
|
||||||
|
|
||||||
|
// Subscribe to session events immediately after registration (like Electron does)
|
||||||
|
startSessionEventSubscription()
|
||||||
|
|
||||||
return partyId
|
return partyId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start session event subscription (called after registration)
|
||||||
|
*/
|
||||||
|
private fun startSessionEventSubscription() {
|
||||||
|
sessionEventJob?.cancel()
|
||||||
|
sessionEventJob = CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
grpcClient.subscribeSessionEvents(partyId).collect { event ->
|
||||||
|
android.util.Log.d("TssRepository", "Session event received: ${event.eventType} for session ${event.sessionId}")
|
||||||
|
|
||||||
|
// Check if this event is for our active session
|
||||||
|
val activeSession = _currentSession.value
|
||||||
|
if (activeSession != null && event.sessionId == activeSession.sessionId) {
|
||||||
|
when (event.eventType) {
|
||||||
|
"session_started" -> {
|
||||||
|
android.util.Log.d("TssRepository", "Session started event for our session, triggering keygen")
|
||||||
|
// Notify callback
|
||||||
|
sessionEventCallback?.invoke(event)
|
||||||
|
}
|
||||||
|
"party_joined", "participant_joined" -> {
|
||||||
|
android.util.Log.d("TssRepository", "Party joined our session")
|
||||||
|
sessionEventCallback?.invoke(event)
|
||||||
|
}
|
||||||
|
"all_joined" -> {
|
||||||
|
android.util.Log.d("TssRepository", "All parties joined our session")
|
||||||
|
sessionEventCallback?.invoke(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set session event callback (called by ViewModel)
|
||||||
|
*/
|
||||||
|
fun setSessionEventCallback(callback: (SessionEventData) -> Unit) {
|
||||||
|
sessionEventCallback = callback
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get share count for startup check
|
* Get share count for startup check
|
||||||
*/
|
*/
|
||||||
|
|
@ -94,26 +143,34 @@ class TssRepository @Inject constructor(
|
||||||
/**
|
/**
|
||||||
* Create a new keygen session (as initiator)
|
* Create a new keygen session (as initiator)
|
||||||
* Calls account-service API: POST /api/v1/co-managed/sessions
|
* Calls account-service API: POST /api/v1/co-managed/sessions
|
||||||
|
* Then auto-joins the session via gRPC (matching Electron behavior)
|
||||||
*/
|
*/
|
||||||
suspend fun createKeygenSession(
|
suspend fun createKeygenSession(
|
||||||
walletName: String,
|
walletName: String,
|
||||||
thresholdT: Int,
|
thresholdT: Int,
|
||||||
thresholdN: Int,
|
thresholdN: Int,
|
||||||
participantName: String
|
participantName: String
|
||||||
): Result<String> {
|
): Result<CreateKeygenSessionResult> {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val jsonMediaType = "application/json; charset=utf-8".toMediaType()
|
val jsonMediaType = "application/json; charset=utf-8".toMediaType()
|
||||||
|
|
||||||
// Build request body matching account-service API
|
// Calculate persistent and external counts (matching Electron)
|
||||||
|
// persistent = n - t (platform backup parties)
|
||||||
|
// external = t (user-held parties)
|
||||||
|
val persistentCount = thresholdN - thresholdT
|
||||||
|
val externalCount = thresholdT
|
||||||
|
|
||||||
|
// Build request body matching account-service API (same as Electron)
|
||||||
val requestBody = com.google.gson.JsonObject().apply {
|
val requestBody = com.google.gson.JsonObject().apply {
|
||||||
addProperty("wallet_name", walletName)
|
addProperty("wallet_name", walletName)
|
||||||
addProperty("threshold_t", thresholdT)
|
addProperty("threshold_t", thresholdT)
|
||||||
addProperty("threshold_n", thresholdN)
|
addProperty("threshold_n", thresholdN)
|
||||||
addProperty("initiator_party_id", partyId)
|
addProperty("initiator_party_id", partyId)
|
||||||
addProperty("initiator_name", participantName)
|
addProperty("initiator_name", participantName)
|
||||||
addProperty("persistent_count", 0) // All external participants
|
addProperty("persistent_count", persistentCount)
|
||||||
addProperty("external_count", thresholdN)
|
addProperty("external_count", externalCount)
|
||||||
|
addProperty("expires_in_seconds", 86400) // 24 hours, same as Electron
|
||||||
}.toString()
|
}.toString()
|
||||||
|
|
||||||
val request = okhttp3.Request.Builder()
|
val request = okhttp3.Request.Builder()
|
||||||
|
|
@ -142,14 +199,83 @@ class TssRepository @Inject constructor(
|
||||||
|
|
||||||
// Parse response
|
// Parse response
|
||||||
val json = com.google.gson.JsonParser.parseString(responseBody).asJsonObject
|
val json = com.google.gson.JsonParser.parseString(responseBody).asJsonObject
|
||||||
|
val sessionId = json.get("session_id").asString
|
||||||
val inviteCode = json.get("invite_code").asString
|
val inviteCode = json.get("invite_code").asString
|
||||||
val joinToken = json.get("join_token")?.asString
|
|
||||||
?: json.get("join_tokens")?.asJsonObject?.entrySet()?.firstOrNull()?.value?.asString
|
// Get join token - prioritize join_tokens map with partyId, then wildcard, then single join_token
|
||||||
|
val joinTokensObj = json.get("join_tokens")?.asJsonObject
|
||||||
|
val joinToken = joinTokensObj?.get(partyId)?.asString
|
||||||
|
?: joinTokensObj?.get("*")?.asString
|
||||||
|
?: json.get("join_token")?.asString
|
||||||
?: ""
|
?: ""
|
||||||
|
|
||||||
// Return invite code in format: inviteCode (the API returns a ready-to-use invite code)
|
android.util.Log.d("TssRepository", "Session created: sessionId=$sessionId, inviteCode=$inviteCode, joinToken length=${joinToken.length}")
|
||||||
// The invite code can be used directly - joinToken is for direct session joining
|
|
||||||
Result.success(inviteCode)
|
// Auto-join session via gRPC (matching Electron behavior)
|
||||||
|
var partyIndex = 0
|
||||||
|
var sessionAlreadyInProgress = false
|
||||||
|
if (joinToken.isNotEmpty()) {
|
||||||
|
android.util.Log.d("TssRepository", "Initiator auto-joining session via gRPC...")
|
||||||
|
val joinResult = grpcClient.joinSession(sessionId, partyId, joinToken)
|
||||||
|
|
||||||
|
if (joinResult.isSuccess) {
|
||||||
|
val joinData = joinResult.getOrThrow()
|
||||||
|
partyIndex = joinData.partyIndex
|
||||||
|
|
||||||
|
android.util.Log.d("TssRepository", "Initiator joined session: partyIndex=$partyIndex, sessionStatus=${joinData.sessionStatus}")
|
||||||
|
|
||||||
|
// Update session state
|
||||||
|
val session = TssSession(
|
||||||
|
sessionId = sessionId,
|
||||||
|
sessionType = SessionType.KEYGEN,
|
||||||
|
thresholdT = thresholdT,
|
||||||
|
thresholdN = thresholdN,
|
||||||
|
participants = listOf(Participant(partyId, partyIndex, participantName)),
|
||||||
|
status = SessionStatus.WAITING,
|
||||||
|
inviteCode = inviteCode
|
||||||
|
)
|
||||||
|
_currentSession.value = session
|
||||||
|
_sessionStatus.value = SessionStatus.WAITING
|
||||||
|
|
||||||
|
// Start message subscription for this session
|
||||||
|
startMessageRouting(sessionId, partyIndex)
|
||||||
|
|
||||||
|
// Check if session is already in_progress (Solution B from Electron)
|
||||||
|
// This handles the case where all parties join before we check
|
||||||
|
if (joinData.sessionStatus == "in_progress") {
|
||||||
|
android.util.Log.d("TssRepository", "Session already in_progress, will trigger keygen immediately")
|
||||||
|
sessionAlreadyInProgress = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
android.util.Log.w("TssRepository", "Initiator failed to join session via gRPC: ${joinResult.exceptionOrNull()?.message}")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
android.util.Log.w("TssRepository", "No join token found for initiator partyId: $partyId")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If session is already in_progress, notify callback immediately
|
||||||
|
if (sessionAlreadyInProgress) {
|
||||||
|
val eventData = SessionEventData(
|
||||||
|
eventId = "immediate_start",
|
||||||
|
eventType = "session_started",
|
||||||
|
sessionId = sessionId,
|
||||||
|
thresholdN = thresholdN,
|
||||||
|
thresholdT = thresholdT,
|
||||||
|
selectedParties = listOf(partyId),
|
||||||
|
joinTokens = emptyMap(),
|
||||||
|
messageHash = null
|
||||||
|
)
|
||||||
|
sessionEventCallback?.invoke(eventData)
|
||||||
|
}
|
||||||
|
|
||||||
|
Result.success(CreateKeygenSessionResult(
|
||||||
|
sessionId = sessionId,
|
||||||
|
inviteCode = inviteCode,
|
||||||
|
partyIndex = partyIndex,
|
||||||
|
walletName = walletName,
|
||||||
|
thresholdT = thresholdT,
|
||||||
|
thresholdN = thresholdN
|
||||||
|
))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("TssRepository", "Create keygen session failed", e)
|
android.util.Log.e("TssRepository", "Create keygen session failed", e)
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
|
|
@ -289,9 +415,327 @@ class TssRepository @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Join a keygen session and execute keygen protocol
|
* Join a keygen session via gRPC only (matching Electron's grpc:joinSession)
|
||||||
* First calls account-service to get session info and join token, then joins via gRPC
|
* This does NOT start keygen - that happens when session_started event is received
|
||||||
|
*
|
||||||
|
* Flow (matching Electron):
|
||||||
|
* 1. Call grpcClient.joinSession(sessionId, partyId, joinToken)
|
||||||
|
* 2. Set up activeKeygenSession state
|
||||||
|
* 3. Prepare for keygen (subscribe to messages)
|
||||||
|
* 4. Return - keygen will start when session_started event is received
|
||||||
*/
|
*/
|
||||||
|
suspend fun joinKeygenSessionViaGrpc(
|
||||||
|
sessionId: String,
|
||||||
|
joinToken: String,
|
||||||
|
walletName: String,
|
||||||
|
thresholdT: Int,
|
||||||
|
thresholdN: Int
|
||||||
|
): Result<JoinKeygenViaGrpcResult> {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
android.util.Log.d("TssRepository", "Joining keygen session via gRPC: sessionId=$sessionId, joinToken length=${joinToken.length}")
|
||||||
|
|
||||||
|
// Join session via gRPC (matching Electron's grpcClient.joinSession)
|
||||||
|
val joinResult = grpcClient.joinSession(sessionId, partyId, joinToken)
|
||||||
|
if (joinResult.isFailure) {
|
||||||
|
android.util.Log.e("TssRepository", "gRPC join failed", joinResult.exceptionOrNull())
|
||||||
|
return@withContext Result.failure(joinResult.exceptionOrNull()!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
val sessionData = joinResult.getOrThrow()
|
||||||
|
val myPartyIndex = sessionData.partyIndex
|
||||||
|
|
||||||
|
android.util.Log.d("TssRepository", "gRPC join successful: partyIndex=$myPartyIndex, sessionStatus=${sessionData.sessionStatus}")
|
||||||
|
|
||||||
|
// Build participants list (matching Electron's logic)
|
||||||
|
val participants = sessionData.participants.toMutableList()
|
||||||
|
participants.add(Participant(partyId, myPartyIndex, "我"))
|
||||||
|
participants.sortBy { it.partyIndex }
|
||||||
|
|
||||||
|
// Set up active keygen session state (matching Electron's activeKeygenSession)
|
||||||
|
val session = TssSession(
|
||||||
|
sessionId = sessionId,
|
||||||
|
sessionType = SessionType.KEYGEN,
|
||||||
|
thresholdT = thresholdT,
|
||||||
|
thresholdN = thresholdN,
|
||||||
|
participants = participants,
|
||||||
|
status = SessionStatus.WAITING,
|
||||||
|
inviteCode = null
|
||||||
|
)
|
||||||
|
_currentSession.value = session
|
||||||
|
_sessionStatus.value = SessionStatus.WAITING
|
||||||
|
|
||||||
|
// Start message subscription for this session (matching Electron's prepareForKeygen)
|
||||||
|
startMessageRouting(sessionId, myPartyIndex)
|
||||||
|
|
||||||
|
android.util.Log.d("TssRepository", "Session state set, waiting for session_started event")
|
||||||
|
|
||||||
|
Result.success(JoinKeygenViaGrpcResult(
|
||||||
|
partyIndex = myPartyIndex,
|
||||||
|
sessionStatus = sessionData.sessionStatus
|
||||||
|
))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("TssRepository", "Join keygen session via gRPC failed", e)
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute keygen as joiner (called when session_started event is received)
|
||||||
|
* This is the second part of the join flow, triggered by session_started event
|
||||||
|
*/
|
||||||
|
suspend fun executeKeygenAsJoiner(
|
||||||
|
sessionId: String,
|
||||||
|
partyIndex: Int,
|
||||||
|
thresholdT: Int,
|
||||||
|
thresholdN: Int,
|
||||||
|
password: String
|
||||||
|
): Result<ShareRecord> = coroutineScope {
|
||||||
|
try {
|
||||||
|
val session = _currentSession.value
|
||||||
|
if (session == null || session.sessionId != sessionId) {
|
||||||
|
return@coroutineScope Result.failure(Exception("No active session or session mismatch"))
|
||||||
|
}
|
||||||
|
|
||||||
|
android.util.Log.d("TssRepository", "Executing keygen as joiner: sessionId=$sessionId, partyIndex=$partyIndex")
|
||||||
|
|
||||||
|
// Start TSS keygen
|
||||||
|
val startResult = tssNativeBridge.startKeygen(
|
||||||
|
sessionId = sessionId,
|
||||||
|
partyId = partyId,
|
||||||
|
partyIndex = partyIndex,
|
||||||
|
thresholdT = thresholdT,
|
||||||
|
thresholdN = thresholdN,
|
||||||
|
participants = session.participants,
|
||||||
|
password = password
|
||||||
|
)
|
||||||
|
|
||||||
|
if (startResult.isFailure) {
|
||||||
|
return@coroutineScope Result.failure(startResult.exceptionOrNull()!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
_sessionStatus.value = SessionStatus.IN_PROGRESS
|
||||||
|
|
||||||
|
// Mark ready
|
||||||
|
grpcClient.markPartyReady(sessionId, partyId)
|
||||||
|
|
||||||
|
// Wait for keygen result
|
||||||
|
val keygenResult = tssNativeBridge.waitForKeygenResult(password)
|
||||||
|
if (keygenResult.isFailure) {
|
||||||
|
_sessionStatus.value = SessionStatus.FAILED
|
||||||
|
return@coroutineScope Result.failure(keygenResult.exceptionOrNull()!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = keygenResult.getOrThrow()
|
||||||
|
|
||||||
|
// Derive address from public key
|
||||||
|
val publicKeyBytes = Base64.decode(result.publicKey, Base64.NO_WRAP)
|
||||||
|
val address = AddressUtils.deriveKavaAddress(publicKeyBytes)
|
||||||
|
|
||||||
|
// Save share record
|
||||||
|
val shareEntity = ShareRecordEntity(
|
||||||
|
sessionId = sessionId,
|
||||||
|
publicKey = result.publicKey,
|
||||||
|
encryptedShare = result.encryptedShare,
|
||||||
|
thresholdT = thresholdT,
|
||||||
|
thresholdN = thresholdN,
|
||||||
|
partyIndex = partyIndex,
|
||||||
|
address = address
|
||||||
|
)
|
||||||
|
val id = shareRecordDao.insertShare(shareEntity)
|
||||||
|
|
||||||
|
// Report completion
|
||||||
|
grpcClient.reportCompletion(sessionId, partyId, publicKeyBytes)
|
||||||
|
|
||||||
|
_sessionStatus.value = SessionStatus.COMPLETED
|
||||||
|
|
||||||
|
android.util.Log.d("TssRepository", "Keygen as joiner completed: address=$address")
|
||||||
|
|
||||||
|
Result.success(shareEntity.copy(id = id).toShareRecord())
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("TssRepository", "Execute keygen as joiner failed", e)
|
||||||
|
_sessionStatus.value = SessionStatus.FAILED
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Join a sign session via gRPC only (matching Electron's cosign:joinSession flow)
|
||||||
|
* This does NOT start signing - that happens when session_started event is received
|
||||||
|
*
|
||||||
|
* Flow (matching Electron):
|
||||||
|
* 1. Validate share exists and password is correct
|
||||||
|
* 2. Call grpcClient.joinSession(sessionId, partyId, joinToken)
|
||||||
|
* 3. Set up activeCoSignSession state
|
||||||
|
* 4. Start message subscription (prepareForSign)
|
||||||
|
* 5. If session is already in_progress, return that status so caller can trigger sign
|
||||||
|
* 6. Otherwise return - sign will start when session_started event is received
|
||||||
|
*/
|
||||||
|
suspend fun joinSignSessionViaGrpc(
|
||||||
|
sessionId: String,
|
||||||
|
joinToken: String,
|
||||||
|
shareId: Long,
|
||||||
|
password: String,
|
||||||
|
walletName: String,
|
||||||
|
messageHash: String,
|
||||||
|
thresholdT: Int,
|
||||||
|
thresholdN: Int,
|
||||||
|
parties: List<Participant>
|
||||||
|
): Result<JoinSignViaGrpcResult> {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
android.util.Log.d("TssRepository", "Joining sign session via gRPC: sessionId=$sessionId")
|
||||||
|
|
||||||
|
// Validate share exists (matching Electron)
|
||||||
|
val shareEntity = shareRecordDao.getShareById(shareId)
|
||||||
|
?: return@withContext Result.failure(Exception("Share not found"))
|
||||||
|
|
||||||
|
// Note: Password is verified during actual sign execution, same as Electron
|
||||||
|
|
||||||
|
// Join session via gRPC (matching Electron's grpcClient.joinSession)
|
||||||
|
val joinResult = grpcClient.joinSession(sessionId, partyId, joinToken)
|
||||||
|
if (joinResult.isFailure) {
|
||||||
|
android.util.Log.e("TssRepository", "gRPC sign join failed", joinResult.exceptionOrNull())
|
||||||
|
return@withContext Result.failure(joinResult.exceptionOrNull()!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
val sessionData = joinResult.getOrThrow()
|
||||||
|
val myPartyIndex = shareEntity.partyIndex // Use party index from our share
|
||||||
|
|
||||||
|
android.util.Log.d("TssRepository", "gRPC sign join successful: partyIndex=$myPartyIndex, sessionStatus=${sessionData.sessionStatus}")
|
||||||
|
|
||||||
|
// Build participants list (matching Electron's logic)
|
||||||
|
// Prefer using parties from validateInviteCode (complete list)
|
||||||
|
val participants = if (parties.isNotEmpty()) {
|
||||||
|
parties.toMutableList()
|
||||||
|
} else {
|
||||||
|
// Fallback: use other_parties + self
|
||||||
|
val list = sessionData.participants.toMutableList()
|
||||||
|
list.add(Participant(partyId, myPartyIndex, "我"))
|
||||||
|
list.sortBy { it.partyIndex }
|
||||||
|
list
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up active sign session state (matching Electron's activeCoSignSession)
|
||||||
|
val session = TssSession(
|
||||||
|
sessionId = sessionId,
|
||||||
|
sessionType = SessionType.SIGN,
|
||||||
|
thresholdT = thresholdT,
|
||||||
|
thresholdN = thresholdN,
|
||||||
|
participants = participants,
|
||||||
|
status = SessionStatus.WAITING,
|
||||||
|
inviteCode = null,
|
||||||
|
messageHash = messageHash
|
||||||
|
)
|
||||||
|
_currentSession.value = session
|
||||||
|
_sessionStatus.value = SessionStatus.WAITING
|
||||||
|
|
||||||
|
// Start message subscription (matching Electron's prepareForSign)
|
||||||
|
startMessageRouting(sessionId, myPartyIndex)
|
||||||
|
|
||||||
|
android.util.Log.d("TssRepository", "Sign session state set, waiting for session_started event or in_progress status")
|
||||||
|
|
||||||
|
Result.success(JoinSignViaGrpcResult(
|
||||||
|
partyIndex = myPartyIndex,
|
||||||
|
sessionStatus = sessionData.sessionStatus,
|
||||||
|
shareId = shareId
|
||||||
|
))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("TssRepository", "Join sign session via gRPC failed", e)
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute sign as joiner (called when session_started event is received or session is already in_progress)
|
||||||
|
* This is the second part of the join sign flow
|
||||||
|
*/
|
||||||
|
suspend fun executeSignAsJoiner(
|
||||||
|
sessionId: String,
|
||||||
|
partyIndex: Int,
|
||||||
|
thresholdT: Int,
|
||||||
|
thresholdN: Int,
|
||||||
|
messageHash: String,
|
||||||
|
shareId: Long,
|
||||||
|
password: String
|
||||||
|
): Result<SignResult> = coroutineScope {
|
||||||
|
try {
|
||||||
|
val session = _currentSession.value
|
||||||
|
if (session == null || session.sessionId != sessionId) {
|
||||||
|
return@coroutineScope Result.failure(Exception("No active session or session mismatch"))
|
||||||
|
}
|
||||||
|
|
||||||
|
android.util.Log.d("TssRepository", "Executing sign as joiner: sessionId=$sessionId, partyIndex=$partyIndex")
|
||||||
|
|
||||||
|
// Get share record
|
||||||
|
val shareEntity = shareRecordDao.getShareById(shareId)
|
||||||
|
?: return@coroutineScope Result.failure(Exception("Share not found"))
|
||||||
|
|
||||||
|
// Update session status
|
||||||
|
_sessionStatus.value = SessionStatus.IN_PROGRESS
|
||||||
|
|
||||||
|
// Get all participants from session
|
||||||
|
val allParticipants = session.participants
|
||||||
|
|
||||||
|
android.util.Log.d("TssRepository", "Starting TSS sign with ${allParticipants.size} participants, thresholdT=$thresholdT")
|
||||||
|
|
||||||
|
// Start TSS sign
|
||||||
|
val startResult = tssNativeBridge.startSign(
|
||||||
|
sessionId = sessionId,
|
||||||
|
partyId = partyId,
|
||||||
|
partyIndex = partyIndex,
|
||||||
|
thresholdT = thresholdT,
|
||||||
|
thresholdN = shareEntity.thresholdN, // Use original N from keygen
|
||||||
|
participants = allParticipants,
|
||||||
|
messageHash = messageHash,
|
||||||
|
shareData = shareEntity.encryptedShare,
|
||||||
|
password = password
|
||||||
|
)
|
||||||
|
|
||||||
|
if (startResult.isFailure) {
|
||||||
|
_sessionStatus.value = SessionStatus.FAILED
|
||||||
|
return@coroutineScope Result.failure(startResult.exceptionOrNull()!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark ready
|
||||||
|
grpcClient.markPartyReady(sessionId, partyId)
|
||||||
|
|
||||||
|
// Wait for sign result
|
||||||
|
val signResult = tssNativeBridge.waitForSignResult()
|
||||||
|
if (signResult.isFailure) {
|
||||||
|
_sessionStatus.value = SessionStatus.FAILED
|
||||||
|
return@coroutineScope Result.failure(signResult.exceptionOrNull()!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = signResult.getOrThrow()
|
||||||
|
|
||||||
|
// Report completion
|
||||||
|
val signatureBytes = android.util.Base64.decode(result.signature, android.util.Base64.NO_WRAP)
|
||||||
|
grpcClient.reportCompletion(sessionId, partyId, signature = signatureBytes)
|
||||||
|
|
||||||
|
_sessionStatus.value = SessionStatus.COMPLETED
|
||||||
|
messageCollectionJob?.cancel()
|
||||||
|
|
||||||
|
android.util.Log.d("TssRepository", "Sign as joiner completed: signature=${result.signature.take(20)}...")
|
||||||
|
|
||||||
|
Result.success(result)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("TssRepository", "Execute sign as joiner failed", e)
|
||||||
|
_sessionStatus.value = SessionStatus.FAILED
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Join a keygen session and execute keygen protocol (DEPRECATED - old flow)
|
||||||
|
* First calls account-service to get session info and join token, then joins via gRPC
|
||||||
|
* @deprecated Use joinKeygenSessionViaGrpc + executeKeygenAsJoiner instead
|
||||||
|
*/
|
||||||
|
@Deprecated("Use joinKeygenSessionViaGrpc + executeKeygenAsJoiner for Electron-compatible flow")
|
||||||
suspend fun joinKeygenSession(
|
suspend fun joinKeygenSession(
|
||||||
inviteCode: String,
|
inviteCode: String,
|
||||||
password: String
|
password: String
|
||||||
|
|
@ -687,12 +1131,92 @@ class TssRepository @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start keygen as initiator (called when session_started event is received)
|
||||||
|
*/
|
||||||
|
suspend fun startKeygenAsInitiator(
|
||||||
|
sessionId: String,
|
||||||
|
thresholdT: Int,
|
||||||
|
thresholdN: Int,
|
||||||
|
password: String
|
||||||
|
): Result<ShareRecord> = coroutineScope {
|
||||||
|
try {
|
||||||
|
val session = _currentSession.value
|
||||||
|
if (session == null || session.sessionId != sessionId) {
|
||||||
|
return@coroutineScope Result.failure(Exception("No active session"))
|
||||||
|
}
|
||||||
|
|
||||||
|
android.util.Log.d("TssRepository", "Starting keygen as initiator: sessionId=$sessionId, partyIndex=${session.participants.firstOrNull()?.partyIndex}")
|
||||||
|
|
||||||
|
val myPartyIndex = session.participants.firstOrNull()?.partyIndex ?: 0
|
||||||
|
|
||||||
|
// Start TSS keygen
|
||||||
|
val startResult = tssNativeBridge.startKeygen(
|
||||||
|
sessionId = sessionId,
|
||||||
|
partyId = partyId,
|
||||||
|
partyIndex = myPartyIndex,
|
||||||
|
thresholdT = thresholdT,
|
||||||
|
thresholdN = thresholdN,
|
||||||
|
participants = session.participants,
|
||||||
|
password = password
|
||||||
|
)
|
||||||
|
|
||||||
|
if (startResult.isFailure) {
|
||||||
|
return@coroutineScope Result.failure(startResult.exceptionOrNull()!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
_sessionStatus.value = SessionStatus.IN_PROGRESS
|
||||||
|
|
||||||
|
// Mark ready
|
||||||
|
grpcClient.markPartyReady(sessionId, partyId)
|
||||||
|
|
||||||
|
// Wait for keygen result
|
||||||
|
val keygenResult = tssNativeBridge.waitForKeygenResult(password)
|
||||||
|
if (keygenResult.isFailure) {
|
||||||
|
_sessionStatus.value = SessionStatus.FAILED
|
||||||
|
return@coroutineScope Result.failure(keygenResult.exceptionOrNull()!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = keygenResult.getOrThrow()
|
||||||
|
|
||||||
|
// Derive address from public key
|
||||||
|
val publicKeyBytes = Base64.decode(result.publicKey, Base64.NO_WRAP)
|
||||||
|
val address = AddressUtils.deriveKavaAddress(publicKeyBytes)
|
||||||
|
|
||||||
|
// Save share record
|
||||||
|
val shareEntity = ShareRecordEntity(
|
||||||
|
sessionId = sessionId,
|
||||||
|
publicKey = result.publicKey,
|
||||||
|
encryptedShare = result.encryptedShare,
|
||||||
|
thresholdT = thresholdT,
|
||||||
|
thresholdN = thresholdN,
|
||||||
|
partyIndex = myPartyIndex,
|
||||||
|
address = address
|
||||||
|
)
|
||||||
|
val id = shareRecordDao.insertShare(shareEntity)
|
||||||
|
|
||||||
|
// Report completion
|
||||||
|
grpcClient.reportCompletion(sessionId, partyId, publicKeyBytes)
|
||||||
|
|
||||||
|
_sessionStatus.value = SessionStatus.COMPLETED
|
||||||
|
sessionEventJob?.cancel()
|
||||||
|
|
||||||
|
Result.success(shareEntity.copy(id = id).toShareRecord())
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("TssRepository", "Start keygen as initiator failed", e)
|
||||||
|
_sessionStatus.value = SessionStatus.FAILED
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancel current session
|
* Cancel current session
|
||||||
*/
|
*/
|
||||||
fun cancelSession() {
|
fun cancelSession() {
|
||||||
tssNativeBridge.cancelSession()
|
tssNativeBridge.cancelSession()
|
||||||
messageCollectionJob?.cancel()
|
messageCollectionJob?.cancel()
|
||||||
|
sessionEventJob?.cancel()
|
||||||
_currentSession.value = null
|
_currentSession.value = null
|
||||||
_sessionStatus.value = SessionStatus.WAITING
|
_sessionStatus.value = SessionStatus.WAITING
|
||||||
}
|
}
|
||||||
|
|
@ -1135,6 +1659,18 @@ class TssRepository @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of creating a keygen session
|
||||||
|
*/
|
||||||
|
data class CreateKeygenSessionResult(
|
||||||
|
val sessionId: String,
|
||||||
|
val inviteCode: String,
|
||||||
|
val partyIndex: Int,
|
||||||
|
val walletName: String,
|
||||||
|
val thresholdT: Int,
|
||||||
|
val thresholdN: Int
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Result of creating a sign session
|
* Result of creating a sign session
|
||||||
*/
|
*/
|
||||||
|
|
@ -1208,6 +1744,25 @@ data class ApiJoinSignSessionData(
|
||||||
val joinToken: String
|
val joinToken: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of joinKeygenSessionViaGrpc
|
||||||
|
* Matches Electron's grpc:joinSession return value
|
||||||
|
*/
|
||||||
|
data class JoinKeygenViaGrpcResult(
|
||||||
|
val partyIndex: Int,
|
||||||
|
val sessionStatus: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of joining sign session via gRPC
|
||||||
|
* Matches Electron's cosign:joinSession return value
|
||||||
|
*/
|
||||||
|
data class JoinSignViaGrpcResult(
|
||||||
|
val partyIndex: Int,
|
||||||
|
val sessionStatus: String?,
|
||||||
|
val shareId: Long
|
||||||
|
)
|
||||||
|
|
||||||
private fun ShareRecordEntity.toShareRecord() = ShareRecord(
|
private fun ShareRecordEntity.toShareRecord() = ShareRecord(
|
||||||
id = id,
|
id = id,
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package com.durian.tssparty.presentation.screens
|
package com.durian.tssparty.presentation.screens
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
|
@ -9,6 +11,7 @@ import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
|
@ -16,6 +19,8 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.durian.tssparty.domain.model.SessionStatus
|
import com.durian.tssparty.domain.model.SessionStatus
|
||||||
import com.durian.tssparty.domain.model.ShareRecord
|
import com.durian.tssparty.domain.model.ShareRecord
|
||||||
|
import com.journeyapps.barcodescanner.ScanContract
|
||||||
|
import com.journeyapps.barcodescanner.ScanOptions
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sign session info returned from validateSignInviteCode API
|
* Sign session info returned from validateSignInviteCode API
|
||||||
|
|
@ -32,8 +37,8 @@ data class SignSessionInfo(
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CoSign Join screen matching service-party-app/src/renderer/src/pages/CoSignJoin.tsx
|
* CoSign Join screen matching service-party-app/src/renderer/src/pages/Sign.tsx
|
||||||
* 2-step flow: input → select_share → (auto-join) → signing → completed
|
* Flow: input → select_share → joining → signing → completed
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -50,18 +55,53 @@ fun CoSignJoinScreen(
|
||||||
onValidateInviteCode: (inviteCode: String) -> Unit,
|
onValidateInviteCode: (inviteCode: String) -> Unit,
|
||||||
onJoinSign: (inviteCode: String, shareId: Long, password: String) -> Unit,
|
onJoinSign: (inviteCode: String, shareId: Long, password: String) -> Unit,
|
||||||
onCancel: () -> Unit,
|
onCancel: () -> Unit,
|
||||||
|
onResetState: () -> Unit = {}, // Reset ViewModel state without navigating
|
||||||
onBackToHome: () -> Unit = {}
|
onBackToHome: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
var inviteCode by remember { mutableStateOf("") }
|
var inviteCode by remember { mutableStateOf("") }
|
||||||
var selectedShareId by remember { mutableStateOf<Long?>(null) }
|
var selectedShareId by remember { mutableStateOf<Long?>(null) }
|
||||||
var password by remember { mutableStateOf("") }
|
var password by remember { mutableStateOf("") }
|
||||||
var showPassword by remember { mutableStateOf(false) }
|
var showPassword by remember { mutableStateOf(false) }
|
||||||
var validationError by remember { mutableStateOf<String?>(null) }
|
var validationError by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
// 2-step flow: input → select_share → joining → signing → completed
|
// Flow: input → select_share → joining → signing → completed
|
||||||
var step by remember { mutableStateOf("input") }
|
var step by remember { mutableStateOf("input") }
|
||||||
var autoJoinAttempted by remember { mutableStateOf(false) }
|
var autoJoinAttempted by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// QR Code Scanner
|
||||||
|
val scanLauncher = rememberLauncherForActivityResult(ScanContract()) { result ->
|
||||||
|
if (result.contents != null) {
|
||||||
|
// Extract invite code from scanned content
|
||||||
|
val scannedCode = result.contents.let { content ->
|
||||||
|
// Handle both raw invite codes and URLs
|
||||||
|
if (content.contains("/")) {
|
||||||
|
content.substringAfterLast("/")
|
||||||
|
} else {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inviteCode = scannedCode.uppercase()
|
||||||
|
// Auto-validate after scan
|
||||||
|
if (inviteCode.isNotBlank()) {
|
||||||
|
validationError = null
|
||||||
|
onValidateInviteCode(inviteCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset to input state (used by cancel buttons in select_share/joining/signing screens)
|
||||||
|
val resetToInput: () -> Unit = {
|
||||||
|
step = "input"
|
||||||
|
inviteCode = ""
|
||||||
|
selectedShareId = null
|
||||||
|
password = ""
|
||||||
|
validationError = null
|
||||||
|
autoJoinAttempted = false
|
||||||
|
onResetState() // Clear ViewModel state without navigating
|
||||||
|
}
|
||||||
|
|
||||||
// Handle session info received (validation success)
|
// Handle session info received (validation success)
|
||||||
LaunchedEffect(signSessionInfo) {
|
LaunchedEffect(signSessionInfo) {
|
||||||
if (signSessionInfo != null && step == "input") {
|
if (signSessionInfo != null && step == "input") {
|
||||||
|
|
@ -78,20 +118,6 @@ fun CoSignJoinScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-join when we have session info and selected share
|
|
||||||
LaunchedEffect(step, signSessionInfo, selectedShareId, autoJoinAttempted, isLoading) {
|
|
||||||
if (step == "select_share" && signSessionInfo != null &&
|
|
||||||
selectedShareId != null && !autoJoinAttempted && !isLoading && error == null) {
|
|
||||||
// Check if we should auto-join (matching share found)
|
|
||||||
val matchingShare = shares.find { it.sessionId == signSessionInfo.keygenSessionId }
|
|
||||||
if (matchingShare != null && selectedShareId == matchingShare.id) {
|
|
||||||
autoJoinAttempted = true
|
|
||||||
step = "joining"
|
|
||||||
onJoinSign(inviteCode, selectedShareId!!, password)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle session status changes
|
// Handle session status changes
|
||||||
LaunchedEffect(sessionStatus) {
|
LaunchedEffect(sessionStatus) {
|
||||||
when (sessionStatus) {
|
when (sessionStatus) {
|
||||||
|
|
@ -135,7 +161,18 @@ fun CoSignJoinScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onCancel = onCancel
|
onScanQrCode = {
|
||||||
|
val options = ScanOptions().apply {
|
||||||
|
setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
||||||
|
setPrompt("扫描签名会话邀请码")
|
||||||
|
setCameraId(0)
|
||||||
|
setBeepEnabled(true)
|
||||||
|
setBarcodeImageEnabled(false)
|
||||||
|
setOrientationLocked(false)
|
||||||
|
}
|
||||||
|
scanLauncher.launch(options)
|
||||||
|
},
|
||||||
|
onCancel = onCancel // In input state, cancel navigates away
|
||||||
)
|
)
|
||||||
"select_share" -> SelectShareScreen(
|
"select_share" -> SelectShareScreen(
|
||||||
shares = shares,
|
shares = shares,
|
||||||
|
|
@ -164,15 +201,16 @@ fun CoSignJoinScreen(
|
||||||
onJoinSign(inviteCode, selectedShareId!!, password)
|
onJoinSign(inviteCode, selectedShareId!!, password)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
onCancel = resetToInput
|
||||||
)
|
)
|
||||||
"joining" -> JoiningScreen()
|
"joining" -> JoiningScreen(onCancel = resetToInput) // Reset to input state, stay on page
|
||||||
"signing" -> SigningProgressScreen(
|
"signing" -> SigningProgressScreen(
|
||||||
sessionStatus = sessionStatus,
|
sessionStatus = sessionStatus,
|
||||||
participants = participants,
|
participants = participants,
|
||||||
currentRound = currentRound,
|
currentRound = currentRound,
|
||||||
totalRounds = totalRounds,
|
totalRounds = totalRounds,
|
||||||
onCancel = onCancel
|
onCancel = resetToInput // Reset to input state, stay on page
|
||||||
)
|
)
|
||||||
"completed" -> SigningCompletedScreen(
|
"completed" -> SigningCompletedScreen(
|
||||||
signature = signature,
|
signature = signature,
|
||||||
|
|
@ -190,6 +228,7 @@ private fun InputScreen(
|
||||||
validationError: String?,
|
validationError: String?,
|
||||||
onInviteCodeChange: (String) -> Unit,
|
onInviteCodeChange: (String) -> Unit,
|
||||||
onValidateCode: () -> Unit,
|
onValidateCode: () -> Unit,
|
||||||
|
onScanQrCode: () -> Unit,
|
||||||
onCancel: () -> Unit
|
onCancel: () -> Unit
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -208,13 +247,42 @@ private fun InputScreen(
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "输入邀请码加入签名会话",
|
text = "扫描二维码或输入邀请码加入签名会话",
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// QR Code Scan Button
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = onScanQrCode,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
enabled = !isLoading
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.QrCodeScanner, contentDescription = null)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text("扫描签名邀请码")
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Divider with "or" text
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Divider(modifier = Modifier.weight(1f))
|
||||||
|
Text(
|
||||||
|
text = " 或手动输入 ",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Divider(modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// Invite Code Input
|
// Invite Code Input
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = inviteCode,
|
value = inviteCode,
|
||||||
|
|
@ -335,7 +403,8 @@ private fun SelectShareScreen(
|
||||||
onPasswordChange: (String) -> Unit,
|
onPasswordChange: (String) -> Unit,
|
||||||
onTogglePassword: () -> Unit,
|
onTogglePassword: () -> Unit,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onJoinSign: () -> Unit
|
onJoinSign: () -> Unit,
|
||||||
|
onCancel: () -> Unit
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -583,11 +652,19 @@ private fun SelectShareScreen(
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
// Buttons
|
// Buttons - Three buttons: Cancel, Back, Join
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = onCancel,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
enabled = !isLoading
|
||||||
|
) {
|
||||||
|
Text("取消")
|
||||||
|
}
|
||||||
|
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = onBack,
|
onClick = onBack,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
|
|
@ -620,7 +697,9 @@ private fun SelectShareScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun JoiningScreen() {
|
private fun JoiningScreen(
|
||||||
|
onCancel: () -> Unit
|
||||||
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -648,6 +727,14 @@ private fun JoiningScreen() {
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
|
OutlinedButton(onClick = onCancel) {
|
||||||
|
Icon(Icons.Default.Cancel, contentDescription = null)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text("取消")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ fun JoinKeygenScreen(
|
||||||
onValidateInviteCode: (inviteCode: String) -> Unit,
|
onValidateInviteCode: (inviteCode: String) -> Unit,
|
||||||
onJoinKeygen: (inviteCode: String, password: String) -> Unit,
|
onJoinKeygen: (inviteCode: String, password: String) -> Unit,
|
||||||
onCancel: () -> Unit,
|
onCancel: () -> Unit,
|
||||||
|
onResetState: () -> Unit = {}, // Reset ViewModel state without navigating
|
||||||
onBackToHome: () -> Unit = {}
|
onBackToHome: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
var inviteCode by remember { mutableStateOf("") }
|
var inviteCode by remember { mutableStateOf("") }
|
||||||
|
|
@ -101,6 +102,16 @@ fun JoinKeygenScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset to input state (used by cancel buttons in confirm/joining/progress screens)
|
||||||
|
// This resets UI state to input screen WITHOUT navigating away from JoinKeygen page
|
||||||
|
val resetToInput: () -> Unit = {
|
||||||
|
step = "input"
|
||||||
|
inviteCode = ""
|
||||||
|
validationError = null
|
||||||
|
autoJoinAttempted = false
|
||||||
|
onResetState() // Clear ViewModel state (sessionInfo, joinToken, etc.) without navigating
|
||||||
|
}
|
||||||
|
|
||||||
when (step) {
|
when (step) {
|
||||||
"input" -> InputScreen(
|
"input" -> InputScreen(
|
||||||
inviteCode = inviteCode,
|
inviteCode = inviteCode,
|
||||||
|
|
@ -117,7 +128,7 @@ fun JoinKeygenScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onCancel = onCancel
|
onCancel = onCancel // In input state, cancel navigates away
|
||||||
)
|
)
|
||||||
"confirm" -> ConfirmScreen(
|
"confirm" -> ConfirmScreen(
|
||||||
sessionInfo = sessionInfo,
|
sessionInfo = sessionInfo,
|
||||||
|
|
@ -129,15 +140,16 @@ fun JoinKeygenScreen(
|
||||||
},
|
},
|
||||||
onRetry = {
|
onRetry = {
|
||||||
autoJoinAttempted = false
|
autoJoinAttempted = false
|
||||||
}
|
},
|
||||||
|
onCancel = resetToInput // Reset to input state, stay on page
|
||||||
)
|
)
|
||||||
"joining" -> JoiningScreen()
|
"joining" -> JoiningScreen(onCancel = resetToInput) // Reset to input state, stay on page
|
||||||
"progress" -> KeygenProgressScreen(
|
"progress" -> KeygenProgressScreen(
|
||||||
sessionStatus = sessionStatus,
|
sessionStatus = sessionStatus,
|
||||||
participants = participants,
|
participants = participants,
|
||||||
currentRound = currentRound,
|
currentRound = currentRound,
|
||||||
totalRounds = totalRounds,
|
totalRounds = totalRounds,
|
||||||
onCancel = onCancel
|
onCancel = resetToInput // Reset to input state, stay on page
|
||||||
)
|
)
|
||||||
"completed" -> KeygenCompletedScreen(
|
"completed" -> KeygenCompletedScreen(
|
||||||
publicKey = publicKey,
|
publicKey = publicKey,
|
||||||
|
|
@ -396,7 +408,8 @@ private fun ConfirmScreen(
|
||||||
isLoading: Boolean,
|
isLoading: Boolean,
|
||||||
error: String?,
|
error: String?,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onRetry: () -> Unit
|
onRetry: () -> Unit,
|
||||||
|
onCancel: () -> Unit
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -464,10 +477,10 @@ private fun ConfirmScreen(
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = onBack,
|
onClick = onCancel,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Text("返回")
|
Text("取消")
|
||||||
}
|
}
|
||||||
Button(
|
Button(
|
||||||
onClick = onRetry,
|
onClick = onRetry,
|
||||||
|
|
@ -484,12 +497,21 @@ private fun ConfirmScreen(
|
||||||
text = "正在自动加入会话...",
|
text = "正在自动加入会话...",
|
||||||
style = MaterialTheme.typography.bodyLarge
|
style = MaterialTheme.typography.bodyLarge
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// Cancel button during auto-join
|
||||||
|
OutlinedButton(onClick = onCancel) {
|
||||||
|
Icon(Icons.Default.Cancel, contentDescription = null)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text("取消")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun JoiningScreen() {
|
private fun JoiningScreen(onCancel: () -> Unit) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -517,6 +539,15 @@ private fun JoiningScreen() {
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
|
// Cancel button
|
||||||
|
OutlinedButton(onClick = onCancel) {
|
||||||
|
Icon(Icons.Default.Cancel, contentDescription = null)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text("取消")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -234,7 +234,7 @@ private fun ServiceCheckItem(
|
||||||
.background(
|
.background(
|
||||||
when {
|
when {
|
||||||
status.message.isEmpty() -> Color.Gray
|
status.message.isEmpty() -> Color.Gray
|
||||||
status.isOnline -> Color(0xFF4CAF50)
|
status.isOnline -> Color(0xFFD4AF37) // Gold for success
|
||||||
else -> Color(0xFFFF5722)
|
else -> Color(0xFFFF5722)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -614,7 +614,7 @@ private fun SigningScreen(
|
||||||
color = when (sessionStatus) {
|
color = when (sessionStatus) {
|
||||||
SessionStatus.WAITING -> MaterialTheme.colorScheme.tertiaryContainer
|
SessionStatus.WAITING -> MaterialTheme.colorScheme.tertiaryContainer
|
||||||
SessionStatus.IN_PROGRESS -> MaterialTheme.colorScheme.primaryContainer
|
SessionStatus.IN_PROGRESS -> MaterialTheme.colorScheme.primaryContainer
|
||||||
SessionStatus.COMPLETED -> Color(0xFF4CAF50)
|
SessionStatus.COMPLETED -> Color(0xFFD4AF37) // Gold
|
||||||
SessionStatus.FAILED -> MaterialTheme.colorScheme.errorContainer
|
SessionStatus.FAILED -> MaterialTheme.colorScheme.errorContainer
|
||||||
},
|
},
|
||||||
shape = MaterialTheme.shapes.small
|
shape = MaterialTheme.shapes.small
|
||||||
|
|
@ -747,7 +747,7 @@ private fun SigningScreen(
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.CheckCircle,
|
Icons.Default.CheckCircle,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = Color(0xFF4CAF50),
|
tint = Color(0xFFD4AF37), // Gold
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -832,14 +832,14 @@ private fun BroadcastingScreen(
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.size(80.dp),
|
modifier = Modifier.size(80.dp),
|
||||||
shape = MaterialTheme.shapes.extraLarge,
|
shape = MaterialTheme.shapes.extraLarge,
|
||||||
color = Color(0xFF4CAF50)
|
color = Color(0xFFD4AF37) // Gold
|
||||||
) {
|
) {
|
||||||
Box(contentAlignment = Alignment.Center) {
|
Box(contentAlignment = Alignment.Center) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Check,
|
Icons.Default.Check,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(40.dp),
|
modifier = Modifier.size(40.dp),
|
||||||
tint = Color.White
|
tint = Color.Black
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -913,14 +913,14 @@ private fun CompletedScreen(
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.size(100.dp),
|
modifier = Modifier.size(100.dp),
|
||||||
shape = MaterialTheme.shapes.extraLarge,
|
shape = MaterialTheme.shapes.extraLarge,
|
||||||
color = Color(0xFF4CAF50)
|
color = Color(0xFFD4AF37) // Gold
|
||||||
) {
|
) {
|
||||||
Box(contentAlignment = Alignment.Center) {
|
Box(contentAlignment = Alignment.Center) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.CheckCircle,
|
Icons.Default.CheckCircle,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(60.dp),
|
modifier = Modifier.size(60.dp),
|
||||||
tint = Color.White
|
tint = Color.Black
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,14 +83,14 @@ fun WalletsScreen(
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (isConnected) Icons.Default.CheckCircle else Icons.Default.Warning,
|
imageVector = if (isConnected) Icons.Default.CheckCircle else Icons.Default.Warning,
|
||||||
contentDescription = if (isConnected) "已连接" else "未连接",
|
contentDescription = if (isConnected) "已连接" else "未连接",
|
||||||
tint = if (isConnected) Color(0xFF4CAF50) else MaterialTheme.colorScheme.error,
|
tint = if (isConnected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error,
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = if (isConnected) "已连接" else "离线",
|
text = if (isConnected) "已连接" else "离线",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = if (isConnected) Color(0xFF4CAF50) else MaterialTheme.colorScheme.error
|
color = if (isConnected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package com.durian.tssparty.presentation.viewmodel
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.durian.tssparty.data.repository.JoinKeygenViaGrpcResult
|
||||||
import com.durian.tssparty.data.repository.TssRepository
|
import com.durian.tssparty.data.repository.TssRepository
|
||||||
import com.durian.tssparty.domain.model.*
|
import com.durian.tssparty.domain.model.*
|
||||||
import com.durian.tssparty.util.TransactionUtils
|
import com.durian.tssparty.util.TransactionUtils
|
||||||
|
|
@ -42,6 +43,9 @@ class MainViewModel @Inject constructor(
|
||||||
init {
|
init {
|
||||||
// Start initialization on app launch
|
// Start initialization on app launch
|
||||||
checkAllServices()
|
checkAllServices()
|
||||||
|
|
||||||
|
// Setup session event callback (like Electron does after connection)
|
||||||
|
setupSessionEventCallback()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -214,7 +218,7 @@ class MainViewModel @Inject constructor(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new keygen session (initiator)
|
* Create a new keygen session (initiator)
|
||||||
* Note: password is not needed for creating session in service-party-app
|
* Matches Electron behavior: creates session via API, then auto-joins via gRPC
|
||||||
*/
|
*/
|
||||||
fun createKeygenSession(walletName: String, thresholdT: Int, thresholdN: Int, participantName: String) {
|
fun createKeygenSession(walletName: String, thresholdT: Int, thresholdN: Int, participantName: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
|
@ -223,14 +227,16 @@ class MainViewModel @Inject constructor(
|
||||||
val result = repository.createKeygenSession(walletName, thresholdT, thresholdN, participantName)
|
val result = repository.createKeygenSession(walletName, thresholdT, thresholdN, participantName)
|
||||||
|
|
||||||
result.fold(
|
result.fold(
|
||||||
onSuccess = { inviteCode ->
|
onSuccess = { sessionResult ->
|
||||||
_createdInviteCode.value = inviteCode
|
_createdInviteCode.value = sessionResult.inviteCode
|
||||||
// Parse sessionId from invite code (format: sessionId:joinToken)
|
_currentSessionId.value = sessionResult.sessionId
|
||||||
val sessionId = inviteCode.split(":").firstOrNull()
|
|
||||||
_currentSessionId.value = sessionId
|
|
||||||
// Add self as first participant
|
// Add self as first participant
|
||||||
_sessionParticipants.value = listOf(participantName)
|
_sessionParticipants.value = listOf(participantName)
|
||||||
|
// Store party index for later use
|
||||||
|
_currentRound.value = 0
|
||||||
_uiState.update { it.copy(isLoading = false) }
|
_uiState.update { it.copy(isLoading = false) }
|
||||||
|
|
||||||
|
android.util.Log.d("MainViewModel", "Keygen session created: sessionId=${sessionResult.sessionId}, partyIndex=${sessionResult.partyIndex}")
|
||||||
},
|
},
|
||||||
onFailure = { e ->
|
onFailure = { e ->
|
||||||
_uiState.update { it.copy(isLoading = false, error = e.message) }
|
_uiState.update { it.copy(isLoading = false, error = e.message) }
|
||||||
|
|
@ -239,12 +245,133 @@ class MainViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store initiator keygen info
|
||||||
|
private var initiatorKeygenInfo: InitiatorKeygenInfo? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup session event callback (called during initialization)
|
||||||
|
* This mirrors Electron's behavior of subscribing to events after connection
|
||||||
|
* Handles events for:
|
||||||
|
* - Initiator (CreateWallet)
|
||||||
|
* - Joiner (JoinKeygen)
|
||||||
|
* - CoSign joiner (参与签名)
|
||||||
|
*/
|
||||||
|
private fun setupSessionEventCallback() {
|
||||||
|
repository.setSessionEventCallback { event ->
|
||||||
|
android.util.Log.d("MainViewModel", "Session event: ${event.eventType} for session ${event.sessionId}")
|
||||||
|
|
||||||
|
when (event.eventType) {
|
||||||
|
"session_started" -> {
|
||||||
|
// Check if this is for initiator (CreateWallet)
|
||||||
|
val currentSessionId = _currentSessionId.value
|
||||||
|
if (currentSessionId != null && event.sessionId == currentSessionId) {
|
||||||
|
android.util.Log.d("MainViewModel", "Session started event for initiator, triggering keygen")
|
||||||
|
viewModelScope.launch {
|
||||||
|
startKeygenAsInitiator(
|
||||||
|
sessionId = currentSessionId,
|
||||||
|
thresholdT = event.thresholdT,
|
||||||
|
thresholdN = event.thresholdN,
|
||||||
|
selectedParties = event.selectedParties
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is for keygen joiner (JoinKeygen)
|
||||||
|
val joinKeygenInfo = pendingJoinKeygenInfo
|
||||||
|
if (joinKeygenInfo != null && event.sessionId == joinKeygenInfo.sessionId) {
|
||||||
|
android.util.Log.d("MainViewModel", "Session started event for keygen joiner, triggering keygen")
|
||||||
|
startKeygenAsJoiner()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is for sign joiner (CoSign/参与签名)
|
||||||
|
val joinSignInfo = pendingJoinSignInfo
|
||||||
|
if (joinSignInfo != null && event.sessionId == joinSignInfo.sessionId) {
|
||||||
|
android.util.Log.d("MainViewModel", "Session started event for sign joiner, triggering sign")
|
||||||
|
startSignAsJoiner()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"party_joined", "participant_joined" -> {
|
||||||
|
// Update participant count for initiator's CreateWallet screen
|
||||||
|
val currentSessionId = _currentSessionId.value
|
||||||
|
if (currentSessionId != null && event.sessionId == currentSessionId) {
|
||||||
|
_sessionParticipants.update { current ->
|
||||||
|
val newParticipant = "参与方 ${current.size + 1}"
|
||||||
|
current + newParticipant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update participant count for keygen joiner's JoinKeygen screen
|
||||||
|
val joinKeygenInfo = pendingJoinKeygenInfo
|
||||||
|
if (joinKeygenInfo != null && event.sessionId == joinKeygenInfo.sessionId) {
|
||||||
|
_joinKeygenParticipants.update { current ->
|
||||||
|
val newParticipant = "参与方 ${current.size + 1}"
|
||||||
|
current + newParticipant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update participant count for sign joiner's CoSign screen
|
||||||
|
val joinSignInfo = pendingJoinSignInfo
|
||||||
|
if (joinSignInfo != null && event.sessionId == joinSignInfo.sessionId) {
|
||||||
|
_coSignParticipants.update { current ->
|
||||||
|
val newParticipant = "参与方 ${current.size + 1}"
|
||||||
|
current + newParticipant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"all_joined" -> {
|
||||||
|
android.util.Log.d("MainViewModel", "All parties joined, protocol will start soon")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enter session (transition from created to session screen)
|
* Enter session (transition from created to session screen)
|
||||||
|
* The session event subscription is already active from initialization
|
||||||
*/
|
*/
|
||||||
fun enterSession() {
|
fun enterSession() {
|
||||||
// This triggers the session screen to show
|
// Session events are already being listened to via the callback set in init
|
||||||
// The session status will change to IN_PROGRESS once keygen starts
|
// This just transitions the UI to the session view
|
||||||
|
val sessionId = _currentSessionId.value
|
||||||
|
android.util.Log.d("MainViewModel", "Entering session: $sessionId")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start keygen process as initiator
|
||||||
|
*/
|
||||||
|
private suspend fun startKeygenAsInitiator(
|
||||||
|
sessionId: String,
|
||||||
|
thresholdT: Int,
|
||||||
|
thresholdN: Int,
|
||||||
|
selectedParties: List<String>
|
||||||
|
) {
|
||||||
|
android.util.Log.d("MainViewModel", "Starting keygen as initiator: sessionId=$sessionId, t=$thresholdT, n=$thresholdN")
|
||||||
|
|
||||||
|
val result = repository.startKeygenAsInitiator(
|
||||||
|
sessionId = sessionId,
|
||||||
|
thresholdT = thresholdT,
|
||||||
|
thresholdN = thresholdN,
|
||||||
|
password = "" // No password needed, same as Electron
|
||||||
|
)
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
onSuccess = { share ->
|
||||||
|
_publicKey.value = share.publicKey
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
lastCreatedAddress = share.address,
|
||||||
|
successMessage = "钱包创建成功!"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Update wallet count
|
||||||
|
_appState.update { state ->
|
||||||
|
state.copy(walletCount = state.walletCount + 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFailure = { e ->
|
||||||
|
_uiState.update { it.copy(error = e.message) }
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -272,12 +399,14 @@ class MainViewModel @Inject constructor(
|
||||||
private val _joinKeygenPublicKey = MutableStateFlow<String?>(null)
|
private val _joinKeygenPublicKey = MutableStateFlow<String?>(null)
|
||||||
val joinKeygenPublicKey: StateFlow<String?> = _joinKeygenPublicKey.asStateFlow()
|
val joinKeygenPublicKey: StateFlow<String?> = _joinKeygenPublicKey.asStateFlow()
|
||||||
|
|
||||||
// Store invite code and password for auto-join
|
// Store invite code, joinToken and password for auto-join (matching Electron)
|
||||||
private var pendingInviteCode: String = ""
|
private var pendingInviteCode: String = ""
|
||||||
|
private var pendingJoinToken: String = ""
|
||||||
private var pendingPassword: String = ""
|
private var pendingPassword: String = ""
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate invite code and get session info
|
* Validate invite code and get session info
|
||||||
|
* Matches Electron's grpc:validateInviteCode - returns sessionInfo + joinToken
|
||||||
*/
|
*/
|
||||||
fun validateInviteCode(inviteCode: String) {
|
fun validateInviteCode(inviteCode: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
|
@ -289,6 +418,9 @@ class MainViewModel @Inject constructor(
|
||||||
result.fold(
|
result.fold(
|
||||||
onSuccess = { validateResult ->
|
onSuccess = { validateResult ->
|
||||||
val info = validateResult.sessionInfo
|
val info = validateResult.sessionInfo
|
||||||
|
// Store joinToken for later use (matching Electron)
|
||||||
|
pendingJoinToken = validateResult.joinToken
|
||||||
|
|
||||||
_joinSessionInfo.value = JoinKeygenSessionInfo(
|
_joinSessionInfo.value = JoinKeygenSessionInfo(
|
||||||
sessionId = info.sessionId,
|
sessionId = info.sessionId,
|
||||||
walletName = info.walletName,
|
walletName = info.walletName,
|
||||||
|
|
@ -299,6 +431,8 @@ class MainViewModel @Inject constructor(
|
||||||
totalParticipants = info.totalParticipants
|
totalParticipants = info.totalParticipants
|
||||||
)
|
)
|
||||||
_uiState.update { it.copy(isLoading = false) }
|
_uiState.update { it.copy(isLoading = false) }
|
||||||
|
|
||||||
|
android.util.Log.d("MainViewModel", "Validate success: sessionId=${info.sessionId}, joinToken length=${pendingJoinToken.length}")
|
||||||
},
|
},
|
||||||
onFailure = { e ->
|
onFailure = { e ->
|
||||||
_uiState.update { it.copy(isLoading = false, error = e.message) }
|
_uiState.update { it.copy(isLoading = false, error = e.message) }
|
||||||
|
|
@ -308,14 +442,91 @@ class MainViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Join a keygen session (called after validateInviteCode)
|
* Join a keygen session (called after validateInviteCode succeeds)
|
||||||
|
* Matches Electron's grpc:joinSession flow:
|
||||||
|
* 1. Uses stored sessionInfo and joinToken from validateInviteCode
|
||||||
|
* 2. Joins via gRPC
|
||||||
|
* 3. Waits for session_started event to trigger keygen
|
||||||
*/
|
*/
|
||||||
fun joinKeygen(inviteCode: String, password: String) {
|
fun joinKeygen(inviteCode: String, password: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
val sessionInfo = _joinSessionInfo.value
|
||||||
|
if (sessionInfo == null) {
|
||||||
|
_uiState.update { it.copy(error = "会话信息不完整") }
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingJoinToken.isEmpty()) {
|
||||||
|
_uiState.update { it.copy(error = "未获取到加入令牌,请重新验证邀请码") }
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
_uiState.update { it.copy(isLoading = true, error = null) }
|
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||||
pendingPassword = password
|
pendingPassword = password
|
||||||
|
|
||||||
val result = repository.joinKeygenSession(inviteCode, password)
|
android.util.Log.d("MainViewModel", "Joining keygen session: sessionId=${sessionInfo.sessionId}, joinToken length=${pendingJoinToken.length}")
|
||||||
|
|
||||||
|
// Join session via gRPC (matching Electron's grpc:joinSession)
|
||||||
|
val result = repository.joinKeygenSessionViaGrpc(
|
||||||
|
sessionId = sessionInfo.sessionId,
|
||||||
|
joinToken = pendingJoinToken,
|
||||||
|
walletName = sessionInfo.walletName,
|
||||||
|
thresholdT = sessionInfo.thresholdT,
|
||||||
|
thresholdN = sessionInfo.thresholdN
|
||||||
|
)
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
onSuccess = { joinResult ->
|
||||||
|
android.util.Log.d("MainViewModel", "gRPC join success: partyIndex=${joinResult.partyIndex}, sessionStatus=${joinResult.sessionStatus}")
|
||||||
|
|
||||||
|
// Store join info for when session_started event arrives
|
||||||
|
pendingJoinKeygenInfo = PendingJoinKeygenInfo(
|
||||||
|
sessionId = sessionInfo.sessionId,
|
||||||
|
partyIndex = joinResult.partyIndex,
|
||||||
|
thresholdT = sessionInfo.thresholdT,
|
||||||
|
thresholdN = sessionInfo.thresholdN,
|
||||||
|
walletName = sessionInfo.walletName,
|
||||||
|
password = password
|
||||||
|
)
|
||||||
|
|
||||||
|
_uiState.update { it.copy(isLoading = false) }
|
||||||
|
|
||||||
|
// If session is already in_progress, trigger keygen immediately (Solution B from Electron)
|
||||||
|
if (joinResult.sessionStatus == "in_progress") {
|
||||||
|
android.util.Log.d("MainViewModel", "Session already in_progress, triggering keygen immediately")
|
||||||
|
startKeygenAsJoiner()
|
||||||
|
}
|
||||||
|
// Otherwise, wait for session_started event
|
||||||
|
},
|
||||||
|
onFailure = { e ->
|
||||||
|
android.util.Log.e("MainViewModel", "gRPC join failed", e)
|
||||||
|
_uiState.update { it.copy(isLoading = false, error = e.message) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store pending join keygen info for when session_started event arrives
|
||||||
|
private var pendingJoinKeygenInfo: PendingJoinKeygenInfo? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start keygen as joiner (called when session_started event is received)
|
||||||
|
*/
|
||||||
|
private fun startKeygenAsJoiner() {
|
||||||
|
val joinInfo = pendingJoinKeygenInfo ?: return
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||||
|
|
||||||
|
android.util.Log.d("MainViewModel", "Starting keygen as joiner: sessionId=${joinInfo.sessionId}, partyIndex=${joinInfo.partyIndex}")
|
||||||
|
|
||||||
|
val result = repository.executeKeygenAsJoiner(
|
||||||
|
sessionId = joinInfo.sessionId,
|
||||||
|
partyIndex = joinInfo.partyIndex,
|
||||||
|
thresholdT = joinInfo.thresholdT,
|
||||||
|
thresholdN = joinInfo.thresholdN,
|
||||||
|
password = joinInfo.password
|
||||||
|
)
|
||||||
|
|
||||||
result.fold(
|
result.fold(
|
||||||
onSuccess = { share ->
|
onSuccess = { share ->
|
||||||
|
|
@ -331,11 +542,12 @@ class MainViewModel @Inject constructor(
|
||||||
_appState.update { state ->
|
_appState.update { state ->
|
||||||
state.copy(walletCount = state.walletCount + 1)
|
state.copy(walletCount = state.walletCount + 1)
|
||||||
}
|
}
|
||||||
|
// Clear pending info
|
||||||
|
pendingJoinKeygenInfo = null
|
||||||
},
|
},
|
||||||
onFailure = { e ->
|
onFailure = { e ->
|
||||||
_uiState.update {
|
android.util.Log.e("MainViewModel", "Keygen execution failed", e)
|
||||||
it.copy(isLoading = false, error = e.message)
|
_uiState.update { it.copy(isLoading = false, error = e.message) }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -350,7 +562,9 @@ class MainViewModel @Inject constructor(
|
||||||
_joinKeygenRound.value = 0
|
_joinKeygenRound.value = 0
|
||||||
_joinKeygenPublicKey.value = null
|
_joinKeygenPublicKey.value = null
|
||||||
pendingInviteCode = ""
|
pendingInviteCode = ""
|
||||||
|
pendingJoinToken = ""
|
||||||
pendingPassword = ""
|
pendingPassword = ""
|
||||||
|
pendingJoinKeygenInfo = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== CoSign (Join Sign) State ==========
|
// ========== CoSign (Join Sign) State ==========
|
||||||
|
|
@ -367,11 +581,14 @@ class MainViewModel @Inject constructor(
|
||||||
private val _coSignSignature = MutableStateFlow<String?>(null)
|
private val _coSignSignature = MutableStateFlow<String?>(null)
|
||||||
val coSignSignature: StateFlow<String?> = _coSignSignature.asStateFlow()
|
val coSignSignature: StateFlow<String?> = _coSignSignature.asStateFlow()
|
||||||
|
|
||||||
// Store pending CoSign data
|
// Store pending CoSign data (matching Electron's activeCoSignSession)
|
||||||
private var pendingCoSignInviteCode: String = ""
|
private var pendingCoSignInviteCode: String = ""
|
||||||
|
private var pendingCoSignJoinToken: String = ""
|
||||||
|
private var pendingJoinSignInfo: PendingJoinSignInfo? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate sign session invite code
|
* Validate sign session invite code
|
||||||
|
* Matches Electron's cosign:validateInviteCode - returns sessionInfo + joinToken
|
||||||
*/
|
*/
|
||||||
fun validateSignInviteCode(inviteCode: String) {
|
fun validateSignInviteCode(inviteCode: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
|
@ -383,6 +600,9 @@ class MainViewModel @Inject constructor(
|
||||||
result.fold(
|
result.fold(
|
||||||
onSuccess = { validateResult ->
|
onSuccess = { validateResult ->
|
||||||
val info = validateResult.signSessionInfo
|
val info = validateResult.signSessionInfo
|
||||||
|
// Store joinToken for later use (matching Electron)
|
||||||
|
pendingCoSignJoinToken = validateResult.joinToken
|
||||||
|
|
||||||
_coSignSessionInfo.value = CoSignSessionInfo(
|
_coSignSessionInfo.value = CoSignSessionInfo(
|
||||||
sessionId = info.sessionId,
|
sessionId = info.sessionId,
|
||||||
keygenSessionId = info.keygenSessionId,
|
keygenSessionId = info.keygenSessionId,
|
||||||
|
|
@ -393,6 +613,8 @@ class MainViewModel @Inject constructor(
|
||||||
currentParticipants = info.currentParticipants
|
currentParticipants = info.currentParticipants
|
||||||
)
|
)
|
||||||
_uiState.update { it.copy(isLoading = false) }
|
_uiState.update { it.copy(isLoading = false) }
|
||||||
|
|
||||||
|
android.util.Log.d("MainViewModel", "Validate sign success: sessionId=${info.sessionId}, joinToken length=${pendingCoSignJoinToken.length}")
|
||||||
},
|
},
|
||||||
onFailure = { e ->
|
onFailure = { e ->
|
||||||
_uiState.update { it.copy(isLoading = false, error = e.message) }
|
_uiState.update { it.copy(isLoading = false, error = e.message) }
|
||||||
|
|
@ -402,13 +624,95 @@ class MainViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Join a sign session
|
* Join a sign session (called after validateSignInviteCode succeeds)
|
||||||
|
* Matches Electron's cosign:joinSession flow:
|
||||||
|
* 1. Uses stored sessionInfo and joinToken from validateSignInviteCode
|
||||||
|
* 2. Joins via gRPC
|
||||||
|
* 3. If session is already in_progress, trigger sign immediately
|
||||||
|
* 4. Otherwise waits for session_started event to trigger sign
|
||||||
*/
|
*/
|
||||||
fun joinSign(inviteCode: String, shareId: Long, password: String) {
|
fun joinSign(inviteCode: String, shareId: Long, password: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
val sessionInfo = _coSignSessionInfo.value
|
||||||
|
if (sessionInfo == null) {
|
||||||
|
_uiState.update { it.copy(error = "会话信息不完整") }
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingCoSignJoinToken.isEmpty()) {
|
||||||
|
_uiState.update { it.copy(error = "未获取到加入令牌,请重新验证邀请码") }
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
_uiState.update { it.copy(isLoading = true, error = null) }
|
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||||
|
|
||||||
val result = repository.joinSignSession(inviteCode, shareId, password)
|
android.util.Log.d("MainViewModel", "Joining sign session: sessionId=${sessionInfo.sessionId}, joinToken length=${pendingCoSignJoinToken.length}")
|
||||||
|
|
||||||
|
// Join session via gRPC (matching Electron's cosign:joinSession)
|
||||||
|
val result = repository.joinSignSessionViaGrpc(
|
||||||
|
sessionId = sessionInfo.sessionId,
|
||||||
|
joinToken = pendingCoSignJoinToken,
|
||||||
|
shareId = shareId,
|
||||||
|
password = password,
|
||||||
|
walletName = sessionInfo.walletName,
|
||||||
|
messageHash = sessionInfo.messageHash,
|
||||||
|
thresholdT = sessionInfo.thresholdT,
|
||||||
|
thresholdN = sessionInfo.thresholdN,
|
||||||
|
parties = emptyList() // Will use other_parties from gRPC response
|
||||||
|
)
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
onSuccess = { joinResult ->
|
||||||
|
android.util.Log.d("MainViewModel", "gRPC sign join success: partyIndex=${joinResult.partyIndex}, sessionStatus=${joinResult.sessionStatus}")
|
||||||
|
|
||||||
|
// Store join info for when session_started event arrives
|
||||||
|
pendingJoinSignInfo = PendingJoinSignInfo(
|
||||||
|
sessionId = sessionInfo.sessionId,
|
||||||
|
partyIndex = joinResult.partyIndex,
|
||||||
|
thresholdT = sessionInfo.thresholdT,
|
||||||
|
thresholdN = sessionInfo.thresholdN,
|
||||||
|
messageHash = sessionInfo.messageHash,
|
||||||
|
shareId = shareId,
|
||||||
|
password = password
|
||||||
|
)
|
||||||
|
|
||||||
|
_uiState.update { it.copy(isLoading = false) }
|
||||||
|
|
||||||
|
// If session is already in_progress, trigger sign immediately (Solution B from Electron)
|
||||||
|
if (joinResult.sessionStatus == "in_progress") {
|
||||||
|
android.util.Log.d("MainViewModel", "Sign session already in_progress, triggering sign immediately")
|
||||||
|
startSignAsJoiner()
|
||||||
|
}
|
||||||
|
// Otherwise, wait for session_started event
|
||||||
|
},
|
||||||
|
onFailure = { e ->
|
||||||
|
android.util.Log.e("MainViewModel", "gRPC sign join failed", e)
|
||||||
|
_uiState.update { it.copy(isLoading = false, error = e.message) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start sign as joiner (called when session_started event is received or session is already in_progress)
|
||||||
|
*/
|
||||||
|
private fun startSignAsJoiner() {
|
||||||
|
val signInfo = pendingJoinSignInfo ?: return
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||||
|
|
||||||
|
android.util.Log.d("MainViewModel", "Starting sign as joiner: sessionId=${signInfo.sessionId}, partyIndex=${signInfo.partyIndex}")
|
||||||
|
|
||||||
|
val result = repository.executeSignAsJoiner(
|
||||||
|
sessionId = signInfo.sessionId,
|
||||||
|
partyIndex = signInfo.partyIndex,
|
||||||
|
thresholdT = signInfo.thresholdT,
|
||||||
|
thresholdN = signInfo.thresholdN,
|
||||||
|
messageHash = signInfo.messageHash,
|
||||||
|
shareId = signInfo.shareId,
|
||||||
|
password = signInfo.password
|
||||||
|
)
|
||||||
|
|
||||||
result.fold(
|
result.fold(
|
||||||
onSuccess = { signResult ->
|
onSuccess = { signResult ->
|
||||||
|
|
@ -420,11 +724,12 @@ class MainViewModel @Inject constructor(
|
||||||
successMessage = "签名完成!"
|
successMessage = "签名完成!"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// Clear pending info
|
||||||
|
pendingJoinSignInfo = null
|
||||||
},
|
},
|
||||||
onFailure = { e ->
|
onFailure = { e ->
|
||||||
_uiState.update {
|
android.util.Log.e("MainViewModel", "Sign execution failed", e)
|
||||||
it.copy(isLoading = false, error = e.message)
|
_uiState.update { it.copy(isLoading = false, error = e.message) }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -439,6 +744,8 @@ class MainViewModel @Inject constructor(
|
||||||
_coSignRound.value = 0
|
_coSignRound.value = 0
|
||||||
_coSignSignature.value = null
|
_coSignSignature.value = null
|
||||||
pendingCoSignInviteCode = ""
|
pendingCoSignInviteCode = ""
|
||||||
|
pendingCoSignJoinToken = ""
|
||||||
|
pendingJoinSignInfo = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -853,3 +1160,42 @@ data class ConnectionTestResult(
|
||||||
val message: String,
|
val message: String,
|
||||||
val latency: Long? = null
|
val latency: Long? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiator keygen info (stored when creating session)
|
||||||
|
*/
|
||||||
|
data class InitiatorKeygenInfo(
|
||||||
|
val sessionId: String,
|
||||||
|
val partyIndex: Int,
|
||||||
|
val walletName: String,
|
||||||
|
val thresholdT: Int,
|
||||||
|
val thresholdN: Int,
|
||||||
|
val participantName: String
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pending join keygen info (stored when joining session, waiting for session_started)
|
||||||
|
* Matches Electron's flow where join happens first, then keygen starts on session_started event
|
||||||
|
*/
|
||||||
|
data class PendingJoinKeygenInfo(
|
||||||
|
val sessionId: String,
|
||||||
|
val partyIndex: Int,
|
||||||
|
val thresholdT: Int,
|
||||||
|
val thresholdN: Int,
|
||||||
|
val walletName: String,
|
||||||
|
val password: String
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pending join sign info (stored when joining sign session, waiting for session_started)
|
||||||
|
* Matches Electron's activeCoSignSession
|
||||||
|
*/
|
||||||
|
data class PendingJoinSignInfo(
|
||||||
|
val sessionId: String,
|
||||||
|
val partyIndex: Int,
|
||||||
|
val thresholdT: Int,
|
||||||
|
val thresholdN: Int,
|
||||||
|
val messageHash: String,
|
||||||
|
val shareId: Long,
|
||||||
|
val password: String
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -12,45 +12,65 @@ import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
|
|
||||||
// Durian Green Theme Colors
|
// Dark Gray & Gold Theme Colors
|
||||||
private val DurianGreen = Color(0xFF4CAF50)
|
private val Gold = Color(0xFFD4AF37) // Classic gold
|
||||||
private val DurianGreenDark = Color(0xFF388E3C)
|
private val GoldLight = Color(0xFFFFD966) // Light gold
|
||||||
private val DurianGreenLight = Color(0xFF81C784)
|
private val GoldDark = Color(0xFFB8960C) // Dark gold
|
||||||
|
private val DarkGray = Color(0xFF1A1A1A) // Deep dark gray
|
||||||
|
private val MediumGray = Color(0xFF2D2D2D) // Medium dark gray
|
||||||
|
private val LightGray = Color(0xFF3D3D3D) // Lighter gray for surfaces
|
||||||
|
private val TextGray = Color(0xFFB0B0B0) // Gray for secondary text
|
||||||
|
|
||||||
private val DarkColorScheme = darkColorScheme(
|
private val DarkColorScheme = darkColorScheme(
|
||||||
primary = DurianGreenLight,
|
primary = Gold,
|
||||||
onPrimary = Color.Black,
|
onPrimary = Color.Black,
|
||||||
primaryContainer = DurianGreenDark,
|
primaryContainer = GoldDark,
|
||||||
onPrimaryContainer = Color.White,
|
onPrimaryContainer = Color.White,
|
||||||
secondary = Color(0xFFB2DFDB),
|
secondary = GoldLight,
|
||||||
onSecondary = Color.Black,
|
onSecondary = Color.Black,
|
||||||
background = Color(0xFF121212),
|
secondaryContainer = LightGray,
|
||||||
|
onSecondaryContainer = GoldLight,
|
||||||
|
tertiary = GoldLight,
|
||||||
|
onTertiary = Color.Black,
|
||||||
|
background = DarkGray,
|
||||||
onBackground = Color.White,
|
onBackground = Color.White,
|
||||||
surface = Color(0xFF1E1E1E),
|
surface = MediumGray,
|
||||||
onSurface = Color.White,
|
onSurface = Color.White,
|
||||||
|
surfaceVariant = LightGray,
|
||||||
|
onSurfaceVariant = TextGray,
|
||||||
|
outline = Color(0xFF5A5A5A),
|
||||||
|
outlineVariant = Color(0xFF404040),
|
||||||
error = Color(0xFFCF6679),
|
error = Color(0xFFCF6679),
|
||||||
onError = Color.Black
|
onError = Color.Black
|
||||||
)
|
)
|
||||||
|
|
||||||
private val LightColorScheme = lightColorScheme(
|
private val LightColorScheme = lightColorScheme(
|
||||||
primary = DurianGreen,
|
primary = GoldDark,
|
||||||
onPrimary = Color.White,
|
onPrimary = Color.White,
|
||||||
primaryContainer = DurianGreenLight,
|
primaryContainer = GoldLight,
|
||||||
onPrimaryContainer = Color.Black,
|
onPrimaryContainer = Color.Black,
|
||||||
secondary = Color(0xFF00796B),
|
secondary = Gold,
|
||||||
onSecondary = Color.White,
|
onSecondary = Color.Black,
|
||||||
background = Color(0xFFFAFAFA),
|
secondaryContainer = Color(0xFFFFF3CD),
|
||||||
onBackground = Color.Black,
|
onSecondaryContainer = GoldDark,
|
||||||
|
tertiary = GoldDark,
|
||||||
|
onTertiary = Color.White,
|
||||||
|
background = Color(0xFFF5F5F5),
|
||||||
|
onBackground = Color(0xFF2D2D2D),
|
||||||
surface = Color.White,
|
surface = Color.White,
|
||||||
onSurface = Color.Black,
|
onSurface = Color(0xFF2D2D2D),
|
||||||
|
surfaceVariant = Color(0xFFE8E8E8),
|
||||||
|
onSurfaceVariant = Color(0xFF5A5A5A),
|
||||||
|
outline = Color(0xFFB0B0B0),
|
||||||
|
outlineVariant = Color(0xFFD0D0D0),
|
||||||
error = Color(0xFFB00020),
|
error = Color(0xFFB00020),
|
||||||
onError = Color.White
|
onError = Color.White
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TssPartyTheme(
|
fun TssPartyTheme(
|
||||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
darkTheme: Boolean = true, // Default to dark theme for dark gray & gold look
|
||||||
dynamicColor: Boolean = true,
|
dynamicColor: Boolean = false, // Disable dynamic colors to use our custom theme
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
val colorScheme = when {
|
val colorScheme = when {
|
||||||
|
|
@ -66,7 +86,8 @@ fun TssPartyTheme(
|
||||||
if (!view.isInEditMode) {
|
if (!view.isInEditMode) {
|
||||||
SideEffect {
|
SideEffect {
|
||||||
val window = (view.context as Activity).window
|
val window = (view.context as Activity).window
|
||||||
window.statusBarColor = colorScheme.primary.toArgb()
|
// Use dark background color for status bar to match the dark theme
|
||||||
|
window.statusBarColor = colorScheme.background.toArgb()
|
||||||
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
|
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue