429 lines
17 KiB
Kotlin
429 lines
17 KiB
Kotlin
package swiss.qpq.gajumobile
|
|
|
|
import android.os.Bundle
|
|
import android.view.WindowManager
|
|
import android.widget.Toast
|
|
import androidx.activity.compose.BackHandler
|
|
import androidx.activity.compose.setContent
|
|
import androidx.activity.enableEdgeToEdge
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.runtime.collectAsState
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.runtime.mutableStateListOf
|
|
import androidx.compose.runtime.mutableStateOf
|
|
import androidx.compose.runtime.remember
|
|
import androidx.compose.runtime.setValue
|
|
import androidx.compose.runtime.saveable.rememberSaveable
|
|
import androidx.compose.ui.platform.LocalContext
|
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
|
import androidx.fragment.app.FragmentActivity
|
|
import androidx.lifecycle.ProcessLifecycleOwner
|
|
import swiss.qpq.gajumobile.data.AppStateManager
|
|
import swiss.qpq.gajumobile.data.WalletRepository
|
|
import swiss.qpq.gajumobile.data.models.Account
|
|
import swiss.qpq.gajumobile.data.models.Wallet
|
|
import swiss.qpq.gajumobile.security.AppLifecycleObserver
|
|
import swiss.qpq.gajumobile.security.BiometricHelper
|
|
import swiss.qpq.gajumobile.security.SecurityManager
|
|
import swiss.qpq.gajumobile.security.KeyManager
|
|
import swiss.qpq.gajumobile.security.AccountAirlock
|
|
import swiss.qpq.gajumobile.ui.screens.AccountNamingScreen
|
|
import swiss.qpq.gajumobile.ui.screens.CreateWalletScreen
|
|
import swiss.qpq.gajumobile.ui.screens.DashboardScreen
|
|
import swiss.qpq.gajumobile.ui.screens.DeleteConfirmationScreen
|
|
import swiss.qpq.gajumobile.ui.screens.EnvironmentInfoScreen
|
|
import swiss.qpq.gajumobile.ui.screens.ExplorerScreen
|
|
import swiss.qpq.gajumobile.ui.screens.LockScreen
|
|
import swiss.qpq.gajumobile.ui.screens.ManageAccountScreen
|
|
import swiss.qpq.gajumobile.ui.screens.ManageWalletScreen
|
|
import swiss.qpq.gajumobile.ui.screens.MnemonicRecoveryScreen
|
|
import swiss.qpq.gajumobile.ui.screens.MnemonicVerifyScreen
|
|
import swiss.qpq.gajumobile.ui.screens.QRReceiveScreen
|
|
import swiss.qpq.gajumobile.ui.screens.QRScannerScreen
|
|
import swiss.qpq.gajumobile.ui.screens.SettingsScreen
|
|
import swiss.qpq.gajumobile.ui.screens.SetupChoiceScreen
|
|
import swiss.qpq.gajumobile.ui.screens.ViewMnemonicScreen
|
|
import swiss.qpq.gajumobile.ui.theme.MyApplicationTheme
|
|
import swiss.qpq.gajumaru.core.formatting.GajuFormat
|
|
import java.util.UUID
|
|
|
|
class MainActivity : FragmentActivity() {
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
installSplashScreen()
|
|
super.onCreate(savedInstanceState)
|
|
enableEdgeToEdge()
|
|
window.isNavigationBarContrastEnforced = false
|
|
|
|
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
|
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver(this))
|
|
|
|
setContent {
|
|
val context = LocalContext.current
|
|
val isLocked by SecurityManager.isLocked.collectAsState()
|
|
var lockErrorMessage by remember { mutableStateOf<String?>(null) }
|
|
|
|
val walletRepo = remember { WalletRepository(context) }
|
|
val appStateManager = remember { AppStateManager(context) }
|
|
|
|
MyApplicationTheme {
|
|
if (isLocked) {
|
|
LockScreen(errorMessage = lockErrorMessage) {
|
|
BiometricHelper.showBiometricPrompt(
|
|
activity = this,
|
|
onSuccess = {
|
|
lockErrorMessage = null
|
|
SecurityManager.unlock()
|
|
},
|
|
onError = { err ->
|
|
lockErrorMessage = err
|
|
}
|
|
)
|
|
}
|
|
} else {
|
|
val routerError: (String) -> Unit = { message ->
|
|
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
|
}
|
|
GajuRouter(walletRepo, appStateManager, routerError)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
enum class Screen {
|
|
CREATE_WALLET,
|
|
SETUP_CHOICE,
|
|
MNEMONIC_RECOVERY,
|
|
ACCOUNT_NAMING,
|
|
DASHBOARD,
|
|
QR_SCANNER,
|
|
QR_RECEIVE,
|
|
SETTINGS,
|
|
MANAGE_WALLET,
|
|
MANAGE_ACCOUNT,
|
|
DELETE_CONFIRM,
|
|
EXPLORER,
|
|
VIEW_MNEMONIC,
|
|
VERIFY_MNEMONIC,
|
|
ENVIRONMENT_INFO
|
|
}
|
|
|
|
@Composable
|
|
fun GajuRouter(
|
|
walletRepo: WalletRepository,
|
|
appStateManager: AppStateManager,
|
|
onError: (String) -> Unit
|
|
) {
|
|
val context = LocalContext.current
|
|
val appState by appStateManager.state.collectAsState()
|
|
|
|
val initialScreen = remember {
|
|
if (walletRepo.listWallets().isEmpty()) Screen.CREATE_WALLET else Screen.DASHBOARD
|
|
}
|
|
|
|
val screenStack = remember { mutableStateListOf(initialScreen) }
|
|
val currentScreen = screenStack.last()
|
|
|
|
fun navigateTo(screen: Screen) {
|
|
screenStack.add(screen)
|
|
}
|
|
|
|
fun navigateBack() {
|
|
if (screenStack.size > 1) {
|
|
screenStack.removeAt(screenStack.size - 1)
|
|
}
|
|
}
|
|
|
|
BackHandler(enabled = screenStack.size > 1) {
|
|
navigateBack()
|
|
}
|
|
|
|
var pendingWalletName by rememberSaveable { mutableStateOf("") }
|
|
var pendingAccount by remember { mutableStateOf<Account?>(null) }
|
|
var setupRecovered by rememberSaveable { mutableStateOf(false) }
|
|
|
|
var selectedWalletId by rememberSaveable { mutableStateOf("") }
|
|
var selectedAccountId by rememberSaveable { mutableStateOf("") }
|
|
|
|
var deleteType by rememberSaveable { mutableStateOf("") }
|
|
var deleteName by rememberSaveable { mutableStateOf("") }
|
|
|
|
var viewMnemonicAccount by remember { mutableStateOf<Account?>(null) }
|
|
var viewMnemonicPhrase by remember { mutableStateOf<Array<ByteArray>?>(null) }
|
|
|
|
var verifyMnemonicAccount by remember { mutableStateOf<Account?>(null) }
|
|
var verifyMnemonicPhrase by remember { mutableStateOf<Array<ByteArray>?>(null) }
|
|
|
|
when (currentScreen) {
|
|
Screen.CREATE_WALLET -> {
|
|
CreateWalletScreen(
|
|
onWalletCreated = { name ->
|
|
pendingWalletName = name
|
|
selectedWalletId = ""
|
|
navigateTo(Screen.SETUP_CHOICE)
|
|
},
|
|
onBack = if (screenStack.size > 1) { { navigateBack() } } else null
|
|
)
|
|
}
|
|
Screen.SETUP_CHOICE -> {
|
|
SetupChoiceScreen(
|
|
onCreate = {
|
|
try {
|
|
val masterKey = KeyManager.getMasterKey(context)
|
|
val phrase = AccountAirlock.generateMnemonic()
|
|
val account = AccountAirlock.createAccount("New Account", phrase, masterKey)
|
|
AccountAirlock.wipePhrase(phrase)
|
|
|
|
pendingAccount = account
|
|
setupRecovered = false
|
|
navigateTo(Screen.ACCOUNT_NAMING)
|
|
} catch (e: Exception) {
|
|
onError("Failed to generate account: ${e.message}")
|
|
}
|
|
},
|
|
onRecover = {
|
|
setupRecovered = true
|
|
navigateTo(Screen.MNEMONIC_RECOVERY)
|
|
},
|
|
onBack = { navigateBack() }
|
|
)
|
|
}
|
|
Screen.MNEMONIC_RECOVERY -> {
|
|
MnemonicRecoveryScreen(
|
|
onMnemonicConfirmed = { phrase ->
|
|
try {
|
|
val masterKey = KeyManager.getMasterKey(context)
|
|
val account = AccountAirlock.createAccount("Recovered Account", phrase, masterKey)
|
|
AccountAirlock.wipePhrase(phrase)
|
|
|
|
pendingAccount = account
|
|
navigateTo(Screen.ACCOUNT_NAMING)
|
|
} catch (e: Exception) {
|
|
onError("Failed to recover account: ${e.message}")
|
|
}
|
|
},
|
|
onBack = { navigateBack() }
|
|
)
|
|
}
|
|
Screen.ACCOUNT_NAMING -> {
|
|
val account = pendingAccount ?: return
|
|
val akId = account.gajuId
|
|
|
|
AccountNamingScreen(
|
|
publicKey = akId,
|
|
onAccountNamed = { name ->
|
|
val finalAccount = account.copy(label = name)
|
|
|
|
if (selectedWalletId.isNotEmpty()) {
|
|
val wallet = walletRepo.loadWallet(selectedWalletId)
|
|
if (wallet != null) {
|
|
walletRepo.saveWallet(wallet.copy(accounts = wallet.accounts + finalAccount))
|
|
}
|
|
} else {
|
|
val wallet = Wallet(
|
|
id = UUID.randomUUID().toString(),
|
|
name = pendingWalletName.ifBlank { "My Wallet" },
|
|
accounts = listOf(finalAccount)
|
|
)
|
|
walletRepo.saveWallet(wallet)
|
|
}
|
|
|
|
if (!setupRecovered) {
|
|
appStateManager.markUnverified(finalAccount.id)
|
|
}
|
|
|
|
selectedWalletId = ""
|
|
appStateManager.notifyWalletsChanged()
|
|
screenStack.clear()
|
|
screenStack.add(Screen.DASHBOARD)
|
|
},
|
|
onBack = { navigateBack() }
|
|
)
|
|
}
|
|
Screen.VIEW_MNEMONIC -> {
|
|
val acc = viewMnemonicAccount ?: return
|
|
val phrase = viewMnemonicPhrase ?: return
|
|
ViewMnemonicScreen(label = acc.label, phrase = phrase) {
|
|
AccountAirlock.wipePhrase(phrase)
|
|
viewMnemonicAccount = null
|
|
viewMnemonicPhrase = null
|
|
navigateBack()
|
|
}
|
|
}
|
|
Screen.VERIFY_MNEMONIC -> {
|
|
val acc = verifyMnemonicAccount ?: return
|
|
val phrase = verifyMnemonicPhrase ?: return
|
|
MnemonicVerifyScreen(
|
|
label = acc.label,
|
|
expectedPhrase = phrase,
|
|
onVerified = {
|
|
appStateManager.clearVerified(acc.id)
|
|
AccountAirlock.wipePhrase(phrase)
|
|
verifyMnemonicAccount = null
|
|
verifyMnemonicPhrase = null
|
|
navigateBack()
|
|
},
|
|
onBack = {
|
|
AccountAirlock.wipePhrase(phrase)
|
|
verifyMnemonicAccount = null
|
|
verifyMnemonicPhrase = null
|
|
navigateBack()
|
|
}
|
|
)
|
|
}
|
|
Screen.DELETE_CONFIRM -> {
|
|
DeleteConfirmationScreen(
|
|
type = deleteType,
|
|
name = deleteName,
|
|
onConfirm = {
|
|
if (deleteType == "WALLET") {
|
|
walletRepo.deleteWallet(selectedWalletId)
|
|
} else {
|
|
walletRepo.deleteAccount(selectedWalletId, selectedAccountId)
|
|
}
|
|
appStateManager.notifyWalletsChanged()
|
|
screenStack.removeAt(screenStack.size - 1)
|
|
screenStack.removeAt(screenStack.size - 1)
|
|
},
|
|
onCancel = { navigateBack() }
|
|
)
|
|
}
|
|
Screen.DASHBOARD -> {
|
|
val wallets = remember(appState.walletsVersion) {
|
|
walletRepo.listWallets().mapNotNull { walletRepo.loadWallet(it) }
|
|
}
|
|
DashboardScreen(
|
|
wallets = wallets,
|
|
balanceFormat = GajuFormat.Type.valueOf(appState.balanceFormat),
|
|
balanceUnit = GajuFormat.Unit.valueOf(appState.balanceUnit),
|
|
onSend = { navigateTo(Screen.QR_SCANNER) },
|
|
onReceive = { navigateTo(Screen.QR_RECEIVE) },
|
|
onAddWallet = {
|
|
selectedWalletId = ""
|
|
navigateTo(Screen.CREATE_WALLET)
|
|
},
|
|
onAddAccount = { walletId ->
|
|
selectedWalletId = walletId
|
|
navigateTo(Screen.SETUP_CHOICE)
|
|
},
|
|
onAccountSettings = { walletId, accountId ->
|
|
selectedWalletId = walletId
|
|
selectedAccountId = accountId
|
|
navigateTo(Screen.MANAGE_ACCOUNT)
|
|
},
|
|
onNavigate = { dest ->
|
|
val screen = when (dest) {
|
|
"settings" -> Screen.SETTINGS
|
|
"transactions" -> Screen.EXPLORER
|
|
"environment_info" -> Screen.ENVIRONMENT_INFO
|
|
else -> null
|
|
}
|
|
if (screen != null) navigateTo(screen)
|
|
},
|
|
)
|
|
}
|
|
Screen.MANAGE_WALLET -> {
|
|
val wallet = remember(selectedWalletId, appState.walletsVersion) {
|
|
walletRepo.loadWallet(selectedWalletId)
|
|
} ?: return
|
|
ManageWalletScreen(
|
|
walletName = wallet.name,
|
|
onRename = { newName ->
|
|
walletRepo.saveWallet(wallet.copy(name = newName))
|
|
appStateManager.notifyWalletsChanged()
|
|
},
|
|
onDelete = {
|
|
deleteType = "WALLET"
|
|
deleteName = wallet.name
|
|
navigateTo(Screen.DELETE_CONFIRM)
|
|
}
|
|
)
|
|
}
|
|
Screen.MANAGE_ACCOUNT -> {
|
|
val wallet = remember(selectedWalletId, appState.walletsVersion) {
|
|
walletRepo.loadWallet(selectedWalletId)
|
|
} ?: return
|
|
val account = wallet.accounts.find { it.id == selectedAccountId } ?: return
|
|
val isUnverified = appState.unverifiedAccountIds.contains(account.id)
|
|
|
|
ManageAccountScreen(
|
|
account = account,
|
|
walletName = wallet.name,
|
|
balanceFormat = GajuFormat.Type.valueOf(appState.balanceFormat),
|
|
balanceUnit = GajuFormat.Unit.valueOf(appState.balanceUnit),
|
|
isUnverified = isUnverified,
|
|
onViewMnemonic = {
|
|
try {
|
|
val masterKey = KeyManager.getMasterKey(context)
|
|
val phrase = AccountAirlock.deriveMnemonic(account.privateKeyEnvelope, masterKey)
|
|
viewMnemonicAccount = account
|
|
viewMnemonicPhrase = phrase
|
|
navigateTo(Screen.VIEW_MNEMONIC)
|
|
} catch (e: Exception) {
|
|
onError("Authentication failed: ${e.message}")
|
|
}
|
|
},
|
|
onVerifyMnemonic = {
|
|
try {
|
|
val masterKey = KeyManager.getMasterKey(context)
|
|
val phrase = AccountAirlock.deriveMnemonic(account.privateKeyEnvelope, masterKey)
|
|
verifyMnemonicAccount = account
|
|
verifyMnemonicPhrase = phrase
|
|
navigateTo(Screen.VERIFY_MNEMONIC)
|
|
} catch (e: Exception) {
|
|
onError("Authentication failed: ${e.message}")
|
|
}
|
|
},
|
|
onRename = { newName ->
|
|
val updatedAccounts = wallet.accounts.map {
|
|
if (it.id == account.id) it.copy(label = newName) else it
|
|
}
|
|
walletRepo.saveWallet(wallet.copy(accounts = updatedAccounts))
|
|
appStateManager.notifyWalletsChanged()
|
|
},
|
|
onDelete = {
|
|
deleteType = "ACCOUNT"
|
|
deleteName = account.label
|
|
navigateTo(Screen.DELETE_CONFIRM)
|
|
}
|
|
)
|
|
}
|
|
Screen.SETTINGS -> {
|
|
SettingsScreen(
|
|
balanceFormat = GajuFormat.Type.valueOf(appState.balanceFormat),
|
|
onFormatChange = { type ->
|
|
appStateManager.setBalanceFormat(type.name)
|
|
},
|
|
onNavigate = { dest ->
|
|
val screen = when (dest) {
|
|
"dashboard" -> Screen.DASHBOARD
|
|
"transactions" -> Screen.EXPLORER
|
|
"environment_info" -> Screen.ENVIRONMENT_INFO
|
|
else -> null
|
|
}
|
|
if (screen != null) {
|
|
screenStack.clear()
|
|
screenStack.add(screen)
|
|
}
|
|
}
|
|
)
|
|
}
|
|
Screen.ENVIRONMENT_INFO -> {
|
|
EnvironmentInfoScreen(onBack = { navigateBack() })
|
|
}
|
|
Screen.QR_SCANNER -> QRScannerScreen(onGenerate = { navigateTo(Screen.QR_RECEIVE) }, onUpload = {})
|
|
Screen.QR_RECEIVE -> QRReceiveScreen(onDone = { navigateBack() })
|
|
Screen.EXPLORER -> ExplorerScreen(onNavigate = { dest ->
|
|
val screen = when (dest) {
|
|
"dashboard" -> Screen.DASHBOARD
|
|
"settings" -> Screen.SETTINGS
|
|
else -> null
|
|
}
|
|
if (screen != null) {
|
|
screenStack.clear()
|
|
screenStack.add(screen)
|
|
}
|
|
})
|
|
}
|
|
}
|