diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml
index 54db9b23dc..a130bdd366 100644
--- a/app/detekt-baseline.xml
+++ b/app/detekt-baseline.xml
@@ -4,14 +4,12 @@
ComplexCondition:AuthCheckView.kt$(showBio && isBiometrySupported && !requirePin) || requireBiometrics
ComplexCondition:ElectrumConfigViewModel.kt$ElectrumConfigViewModel$currentState.host.isBlank() || port == null || port <= 0 || protocol == null
- ComplexCondition:HomeViewModel.kt$HomeViewModel$thresholdReached && isTimeOutOver && belowMaxWarnings && !_uiState.value.highBalanceSheetVisible
ComplexCondition:MapWebViewClient.kt$MapWebViewClient$it.errorCode == ERROR_HOST_LOOKUP || it.errorCode == ERROR_CONNECT || it.errorCode == ERROR_TIMEOUT || it.errorCode == ERROR_FILE_NOT_FOUND
ComplexCondition:ShopWebViewClient.kt$ShopWebViewClient$it.errorCode == ERROR_HOST_LOOKUP || it.errorCode == ERROR_CONNECT || it.errorCode == ERROR_TIMEOUT || it.errorCode == ERROR_FILE_NOT_FOUND
ComposableParamOrder:ActivityDetailScreen.kt$ActivityDetailScreen
ComposableParamOrder:ActivityExploreScreen.kt$ActivityExploreScreen
ComposableParamOrder:ActivityIcon.kt$ActivityIcon
ComposableParamOrder:ActivityIcon.kt$CircularIcon
- ComposableParamOrder:AmountInput.kt$AmountInput
ComposableParamOrder:AppStatus.kt$AppStatus
ComposableParamOrder:AuthCheckView.kt$AuthCheckView
ComposableParamOrder:AuthCheckView.kt$AuthCheckViewContent
@@ -29,7 +27,6 @@
ComposableParamOrder:InfoScreenContent.kt$InfoScreenContent
ComposableParamOrder:Money.kt$MoneyCaptionB
ComposableParamOrder:OnboardingSlidesScreen.kt$OnboardingSlidesScreen
- ComposableParamOrder:OnboardingSlidesScreen.kt$OnboardingTab
ComposableParamOrder:PriceCard.kt$PriceCard
ComposableParamOrder:ReceiveConfirmScreen.kt$ReceiveConfirmScreen
ComposableParamOrder:ReportIssueScreen.kt$ReportIssueScreen
@@ -71,7 +68,6 @@
CyclomaticComplexMethod:ActivityListGrouped.kt$private fun groupActivityItems(activityItems: List<Activity>): List<Any>
CyclomaticComplexMethod:ActivityRow.kt$@Composable fun ActivityRow( item: Activity, onClick: (String) -> Unit, testTag: String, )
CyclomaticComplexMethod:ActivityRow.kt$@Composable private fun TransactionStatusText( txType: PaymentType, isLightning: Boolean, status: PaymentState?, isTransfer: Boolean, )
- CyclomaticComplexMethod:AmountInput.kt$@Composable fun AmountInput( modifier: Modifier = Modifier, defaultValue: Long = 0, primaryDisplay: PrimaryDisplay, showConversion: Boolean = false, overrideSats: Long? = null, onSatsChange: (Long) -> Unit, )
CyclomaticComplexMethod:AppStatusScreen.kt$@Composable private fun Content( uiState: AppStatusUiState = AppStatusUiState(), onBack: () -> Unit = {}, onClose: () -> Unit = {}, onInternetClick: () -> Unit = {}, onElectrumClick: () -> Unit = {}, onNodeClick: () -> Unit = {}, onChannelsClick: () -> Unit = {}, onBackupClick: () -> Unit = {}, )
CyclomaticComplexMethod:AppViewModel.kt$AppViewModel$private fun observeSendEvents()
CyclomaticComplexMethod:AppViewModel.kt$AppViewModel$private suspend fun handleSanityChecks(amountSats: ULong)
@@ -82,14 +78,12 @@
CyclomaticComplexMethod:CoreService.kt$ActivityService$private suspend fun processOnchainPayment( kind: PaymentKind.Onchain, payment: PaymentDetails, forceUpdate: Boolean, )
CyclomaticComplexMethod:HealthRepo.kt$HealthRepo$private fun collectState()
CyclomaticComplexMethod:HomeScreen.kt$@Composable fun HomeScreen( mainUiState: MainUiState, drawerState: DrawerState, rootNavController: NavController, walletNavController: NavHostController, settingsViewModel: SettingsViewModel, walletViewModel: WalletViewModel, appViewModel: AppViewModel, activityListViewModel: ActivityListViewModel, homeViewModel: HomeViewModel = hiltViewModel(), )
- CyclomaticComplexMethod:HomeScreen.kt$@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class) @Composable private fun Content( mainUiState: MainUiState, homeUiState: HomeUiState, rootNavController: NavController, walletNavController: NavController, drawerState: DrawerState, hazeState: HazeState = rememberHazeState(), latestActivities: List<Activity>?, onClickProfile: () -> Unit = {}, onRefresh: () -> Unit = {}, onRemoveSuggestion: (Suggestion) -> Unit = {}, onClickSuggestion: (Suggestion) -> Unit = {}, onClickAddWidget: () -> Unit = {}, onClickEditWidgetList: () -> Unit = {}, onClickEditWidget: (WidgetType) -> Unit = {}, onClickDeleteWidget: (WidgetType) -> Unit = {}, onMoveWidget: (Int, Int) -> Unit = { _, _ -> }, onDismissEmptyState: () -> Unit = {}, onDismissHighBalanceSheet: () -> Unit = {}, onClickEmptyActivityRow: () -> Unit = {}, balances: BalanceState = LocalBalances.current, )
CyclomaticComplexMethod:LightningService.kt$LightningService$private fun logEvent(event: Event)
CyclomaticComplexMethod:ReceiveQrScreen.kt$@Composable fun ReceiveQrScreen( cjitInvoice: MutableState<String?>, cjitActive: MutableState<Boolean>, walletState: MainUiState, onCjitToggle: (Boolean) -> Unit, onClickEditInvoice: () -> Unit, onClickReceiveOnSpending: () -> Unit, modifier: Modifier = Modifier, )
CyclomaticComplexMethod:RestoreWalletScreen.kt$@Composable fun RestoreWalletView( onBackClick: () -> Unit, onRestoreClick: (mnemonic: String, passphrase: String?) -> Unit, )
CyclomaticComplexMethod:SendSheet.kt$@Composable fun SendSheet( appViewModel: AppViewModel, walletViewModel: WalletViewModel, startDestination: SendRoute = SendRoute.Recipient, )
CyclomaticComplexMethod:SettingsButtonRow.kt$@Composable fun SettingsButtonRow( title: String, modifier: Modifier = Modifier, subtitle: String? = null, value: SettingsButtonValue = SettingsButtonValue.None, description: String? = null, iconRes: Int? = null, iconTint: Color = Color.Unspecified, iconSize: Dp = 32.dp, maxLinesSubtitle: Int = Int.MAX_VALUE, enabled: Boolean = true, loading: Boolean = false, onClick: () -> Unit, )
CyclomaticComplexMethod:Slider.kt$@Composable fun StepSlider( value: Int, steps: List<Int>, onValueChange: (Int) -> Unit, modifier: Modifier = Modifier, )
- CyclomaticComplexMethod:WakeNodeWorker.kt$WakeNodeWorker$private suspend fun handleLdkEvent(event: Event)
DestructuringDeclarationWithTooManyEntries:ActivityRow.kt$val (_, _, _, _, _, displayUnit, primaryDisplay) = LocalCurrencies.current
DestructuringDeclarationWithTooManyEntries:BalanceHeaderView.kt$val (_, _, _, _, _, displayUnit, primaryDisplay) = LocalCurrencies.current
DestructuringDeclarationWithTooManyEntries:DefaultUnitSettingsScreen.kt$val (_, _, _, selectedCurrency, _, displayUnit, primaryDisplay) = LocalCurrencies.current
@@ -108,7 +102,6 @@
ForbiddenComment:ActivityDetailScreen.kt$/* TODO: Implement assign functionality */
ForbiddenComment:ActivityListViewModel.kt$ActivityListViewModel$// TODO: sync only on specific events for better performance
ForbiddenComment:ActivityRow.kt$// TODO: calculate confirmsIn text
- ForbiddenComment:AppViewModel.kt$AppViewModel$// TODO: handle ONLY cjit as payment received. This makes it look like any channel confirmed is a received payment.
ForbiddenComment:BackupNavSheetViewModel.kt$BackupNavSheetViewModel$// TODO: get from actual repository state
ForbiddenComment:BackupRepo.kt$BackupRepo$// TODO: Add other backup categories as they get implemented:
ForbiddenComment:BoostTransactionViewModel.kt$BoostTransactionUiState$// TODO: Implement dynamic time estimation
@@ -120,19 +113,16 @@
ForbiddenComment:LightningNodeService.kt$LightningNodeService$// TODO: Get from resources
ForbiddenComment:Notifications.kt$// TODO: review if needed:
ForbiddenComment:SuccessScreen.kt$// TODO: verify backup
- ForbiddenComment:TransferViewModel.kt$TransferViewModel$// TODO: showBottomSheet: forceTransfer
FunctionOnlyReturningConstant:ShopWebViewInterface.kt$ShopWebViewInterface$@JavascriptInterface fun isReady(): Boolean
ImplicitDefaultLocale:BlocksService.kt$BlocksService$String.format("%.2f", blockInfo.difficulty / 1_000_000_000_000.0)
ImplicitDefaultLocale:PriceService.kt$PriceService$String.format("%.2f", price)
InstanceOfCheckForException:LightningService.kt$LightningService$e is NodeException
- LambdaParameterEventTrailing:AmountInput.kt$onSatsChange
LambdaParameterEventTrailing:CalculatorCard.kt$onFiatChange
LambdaParameterEventTrailing:QrScanningScreen.kt$onSubmitDebug
LambdaParameterEventTrailing:ReceiveQrScreen.kt$onClickEditInvoice
LambdaParameterEventTrailing:SettingsButtonRow.kt$onClick
LambdaParameterEventTrailing:SuggestionCard.kt$onClick
LambdaParameterEventTrailing:ToastView.kt$onDismiss
- LambdaParameterInRestartableEffect:AmountInput.kt$onSatsChange
LambdaParameterInRestartableEffect:AuthCheckView.kt$validatePin
LambdaParameterInRestartableEffect:BiometricPrompt.kt$onSuccess
LambdaParameterInRestartableEffect:BoostTransactionSheet.kt$onFailure
@@ -151,7 +141,6 @@
LambdaParameterInRestartableEffect:QrScanningScreen.kt$onScanSuccess
LambdaParameterInRestartableEffect:ReportIssueScreen.kt$navigateResultScreen
LambdaParameterInRestartableEffect:SendCoinSelectionScreen.kt$onRender
- LambdaParameterInRestartableEffect:SendConfirmScreen.kt$onEvent
LambdaParameterInRestartableEffect:SendConfirmScreen.kt$onNavigateToPin
LambdaParameterInRestartableEffect:SendPinCheckScreen.kt$onSuccess
LambdaParameterInRestartableEffect:SendQuickPayScreen.kt$onPaymentComplete
@@ -169,50 +158,29 @@
LongMethod:CoreService.kt$ActivityService$suspend fun generateRandomTestData(count: Int = 100)
LongMethod:LightningService.kt$LightningService$private fun logEvent(event: Event)
LongMethod:MainActivity.kt$MainActivity$override fun onCreate(savedInstanceState: Bundle?)
- LongMethod:WakeNodeWorker.kt$WakeNodeWorker$private suspend fun handleLdkEvent(event: Event)
- LongParameterList:ActivityRepo.kt$ActivityRepo$( filter: ActivityFilter? = null, txType: PaymentType? = null, tags: List<String>? = null, search: String? = null, minDate: ULong? = null, maxDate: ULong? = null, limit: UInt? = null, sortDirection: SortDirection? = null, )
- LongParameterList:ActivityRepo.kt$ActivityRepo$( id: String, paymentHash: String? = null, txId: String? = null, address: String, isReceive: Boolean, tags: List<String>, )
LongParameterList:BiometricPrompt.kt$( activity: Context, title: String, cancelButtonText: String, onAuthSucceed: () -> Unit, onAuthFailed: (() -> Unit), onAuthError: ((errorCode: Int, errString: CharSequence) -> Unit), )
LongParameterList:BiometricPrompt.kt$( activity: Context, title: String, cancelButtonText: String, onAuthSucceeded: () -> Unit, onAuthFailed: (() -> Unit), onAuthError: ((errorCode: Int, errString: CharSequence) -> Unit), onUnsupported: () -> Unit, )
LongParameterList:CoreService.kt$ActivityService$( filter: ActivityFilter? = null, txType: PaymentType? = null, tags: List<String>? = null, search: String? = null, minDate: ULong? = null, maxDate: ULong? = null, limit: UInt? = null, sortDirection: SortDirection? = null, )
LongParameterList:CoreService.kt$BlocktankService$( channelSizeSat: ULong, invoiceSat: ULong, invoiceDescription: String, nodeId: String, channelExpiryWeeks: UInt, options: CreateCjitOptions, )
LongParameterList:CoreService.kt$OnchainService$( mnemonicPhrase: String, derivationPathStr: String?, network: Network?, bip39Passphrase: String?, isChange: Boolean?, startIndex: UInt?, count: UInt?, )
- LongParameterList:DevSettingsViewModel.kt$DevSettingsViewModel$( @ApplicationContext private val context: Context, @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val firebaseMessaging: FirebaseMessaging, private val lightningRepo: LightningRepo, private val walletRepo: WalletRepo, private val widgetsStore: WidgetsStore, private val currencyRepo: CurrencyRepo, private val logsRepo: LogsRepo, private val cacheStore: CacheStore, private val blocktankRepo: BlocktankRepo, )
LongParameterList:LightningConnectionsViewModel.kt$LightningConnectionsViewModel$( @ApplicationContext private val context: Context, @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val lightningRepo: LightningRepo, internal val blocktankRepo: BlocktankRepo, private val logsRepo: LogsRepo, private val addressChecker: AddressChecker, private val ldkNodeEventBus: LdkNodeEventBus, private val walletRepo: WalletRepo, )
LongParameterList:LightningRepo.kt$LightningRepo$( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val lightningService: LightningService, private val ldkNodeEventBus: LdkNodeEventBus, private val settingsStore: SettingsStore, private val coreService: CoreService, private val lspNotificationsService: LspNotificationsService, private val firebaseMessaging: FirebaseMessaging, private val keychain: Keychain, private val lnurlService: LnurlService, private val cacheStore: CacheStore, )
- LongParameterList:LightningRepo.kt$LightningRepo$( address: Address, sats: ULong, speed: TransactionSpeed? = null, utxosToSpend: List<SpendableUtxo>? = null, feeRates: FeeRates? = null, isTransfer: Boolean = false, channelId: String? = null, )
LongParameterList:Notifications.kt$( title: String?, text: String?, extras: Bundle? = null, bigText: String? = null, id: Int = Random.nextInt(), context: Context, )
- LongParameterList:TransferViewModel.kt$TransferViewModel$( @ApplicationContext private val context: Context, private val lightningRepo: LightningRepo, private val blocktankRepo: BlocktankRepo, private val walletRepo: WalletRepo, private val currencyRepo: CurrencyRepo, private val settingsStore: SettingsStore, private val cacheStore: CacheStore, )
- LongParameterList:WalletRepo.kt$WalletRepo$( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val db: AppDb, private val keychain: Keychain, private val coreService: CoreService, private val settingsStore: SettingsStore, private val addressChecker: AddressChecker, private val lightningRepo: LightningRepo, private val cacheStore: CacheStore, )
LongParameterList:WidgetsRepo.kt$WidgetsRepo$( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val newsService: NewsService, private val factsService: FactsService, private val blocksService: BlocksService, private val weatherService: WeatherService, private val priceService: PriceService, private val widgetsStore: WidgetsStore, private val settingsStore: SettingsStore, )
LoopWithTooManyJumpStatements:MonetaryVisualTransformation.kt$MonetaryVisualTransformation.<no name provided>$for
LoopWithTooManyJumpStatements:TransferViewModel.kt$TransferViewModel$while
MagicNumber:ActivityDetailScreen.kt$40
MagicNumber:ActivityExploreScreen.kt$40
- MagicNumber:ActivityIcon.kt$0.5f
- MagicNumber:ActivityListViewModel.kt$ActivityListViewModel$1000
MagicNumber:ActivityListViewModel.kt$ActivityListViewModel$300
- MagicNumber:ActivityRepo.kt$ActivityRepo$1000
MagicNumber:AddressViewerScreen.kt$1500000L
MagicNumber:AddressViewerScreen.kt$250000L
MagicNumber:AddressViewerScreen.kt$50000L
MagicNumber:AddressViewerViewModel.kt$AddressViewerViewModel$300
- MagicNumber:AllActivityScreen.kt$0.5f
MagicNumber:AllActivityScreen.kt$0xFF161616
MagicNumber:AllActivityScreen.kt$0xFF1e1e1e
- MagicNumber:AllActivityScreen.kt$1500f
- MagicNumber:AmountInput.kt$8
- MagicNumber:AndroidKeyStore.kt$AndroidKeyStore$128
- MagicNumber:AndroidKeyStore.kt$AndroidKeyStore$256
- MagicNumber:AppStatus.kt$0.2f
MagicNumber:AppStatus.kt$0.4f
- MagicNumber:AppStatus.kt$0.5f
- MagicNumber:AppStatus.kt$0.6f
- MagicNumber:AppStatus.kt$2000
- MagicNumber:AppStatus.kt$600
MagicNumber:AppViewModel.kt$AppViewModel$1000
MagicNumber:AppViewModel.kt$AppViewModel$250
- MagicNumber:AppViewModel.kt$AppViewModel$300
MagicNumber:AppViewModel.kt$AppViewModel$500
MagicNumber:ArticleModel.kt$24
MagicNumber:ArticleModel.kt$30
@@ -220,103 +188,51 @@
MagicNumber:AutoReadClipboardHandler.kt$1000
MagicNumber:BackupNavSheetViewModel.kt$BackupNavSheetViewModel$200
MagicNumber:BackupRepo.kt$BackupRepo$60000
- MagicNumber:BackupSettingsScreen.kt$1000
- MagicNumber:BackupSettingsScreen.kt$60
MagicNumber:BackupsViewModel.kt$BackupsViewModel$500
- MagicNumber:BiometricCrypto.kt$BiometricCrypto$256
MagicNumber:BiometricsView.kt$5
- MagicNumber:Bip21Utils.kt$Bip21Utils$8
- MagicNumber:Bip39Utils.kt$0xFF
MagicNumber:Bip39Utils.kt$12
- MagicNumber:Bip39Utils.kt$128
MagicNumber:Bip39Utils.kt$24
- MagicNumber:Bip39Utils.kt$256
- MagicNumber:Bip39Utils.kt$32
MagicNumber:Bip39Utils.kt$8
- MagicNumber:BlocksService.kt$BlocksService$1000L
- MagicNumber:BlocksService.kt$BlocksService$1024.0
- MagicNumber:BlocksService.kt$BlocksService$1_000_000_000_000.0
- MagicNumber:BlocktankRepo.kt$BlocktankRepo$1.1
- MagicNumber:BlocktankRepo.kt$BlocktankRepo$1000
- MagicNumber:BlocktankRepo.kt$BlocktankRepo$225
- MagicNumber:BlocktankRepo.kt$BlocktankRepo$450
- MagicNumber:BlocktankRepo.kt$BlocktankRepo$495
- MagicNumber:Button.kt$0.5f
MagicNumber:ChangePinConfirmScreen.kt$500
MagicNumber:ChannelDetailScreen.kt$1.5f
MagicNumber:ChannelOrdersScreen.kt$10
MagicNumber:ChannelOrdersScreen.kt$100
- MagicNumber:ChannelOrdersScreen.kt$30
- MagicNumber:ChannelOrdersScreen.kt$40
- MagicNumber:ConfirmMnemonicScreen.kt$12
- MagicNumber:ConfirmMnemonicScreen.kt$24
MagicNumber:ConfirmMnemonicScreen.kt$300
MagicNumber:ContentView.kt$100
MagicNumber:ContentView.kt$500
- MagicNumber:Context.kt$1024
- MagicNumber:CoreService.kt$ActivityService$24
- MagicNumber:CoreService.kt$ActivityService$30L
- MagicNumber:CoreService.kt$ActivityService$60
MagicNumber:CoreService.kt$ActivityService$64
- MagicNumber:CoreService.kt$ActivityService$8
- MagicNumber:Crypto.kt$Crypto$128
MagicNumber:Crypto.kt$Crypto$16
MagicNumber:Crypto.kt$Crypto$32
- MagicNumber:CurrencyService.kt$CurrencyService$1000L
MagicNumber:ElectrumConfigViewModel.kt$ElectrumConfigViewModel$65535
MagicNumber:ElectrumServer.kt$50001
MagicNumber:ElectrumServer.kt$50002
MagicNumber:ElectrumServer.kt$60001
MagicNumber:ElectrumServer.kt$60002
- MagicNumber:ExternalConnectionScreen.kt$66
- MagicNumber:HomeScreen.kt$0.5f
MagicNumber:HomeScreen.kt$0.8f
MagicNumber:HomeScreen.kt$3
MagicNumber:HttpModule.kt$HttpModule$30_000
MagicNumber:HttpModule.kt$HttpModule$60_000
MagicNumber:InitializingWalletView.kt$500
MagicNumber:InitializingWalletView.kt$99.9
- MagicNumber:LightningChannel.kt$0.5f
- MagicNumber:LightningConnectionsViewModel.kt$LightningConnectionsViewModel$10
MagicNumber:LightningConnectionsViewModel.kt$LightningConnectionsViewModel$500
- MagicNumber:LiquidityScreen.kt$200_000L
- MagicNumber:LiquidityScreen.kt$50_000L
- MagicNumber:Logger.kt$Logger$1024L
MagicNumber:Logger.kt$Logger$4
- MagicNumber:LogsRepo.kt$LogsRepo$3
- MagicNumber:MigrationService.kt$MigrationService$1000
- MagicNumber:MigrationService.kt$MigrationService$1000L
MagicNumber:NewsService.kt$NewsService$10
MagicNumber:OnboardingSlidesScreen.kt$3
MagicNumber:OnboardingSlidesScreen.kt$4
MagicNumber:OnboardingSlidesScreen.kt$5
- MagicNumber:Perf.kt$1000.0
MagicNumber:PinConfirmScreen.kt$500
MagicNumber:PinPromptScreen.kt$0.8f
- MagicNumber:PreviewItems.kt$10
- MagicNumber:PreviewItems.kt$3
MagicNumber:PriceCard.kt$1000
MagicNumber:PriceCard.kt$3.0
MagicNumber:PriceCard.kt$4.0
MagicNumber:PricePreviewScreen.kt$3.0
MagicNumber:PricePreviewScreen.kt$4.0
- MagicNumber:PriceService.kt$PriceService$100
- MagicNumber:PriceService.kt$PriceService$1000
- MagicNumber:PriceService.kt$PriceService$6
MagicNumber:ProgressSteps.kt$12f
MagicNumber:QrScanningScreen.kt$100
- MagicNumber:QuickPaySettingsScreen.kt$10
- MagicNumber:QuickPaySettingsScreen.kt$20
- MagicNumber:QuickPaySettingsScreen.kt$5
- MagicNumber:QuickPaySettingsScreen.kt$50
- MagicNumber:ReceiveLiquidityScreen.kt$100.0
MagicNumber:ReceiveQrScreen.kt$17.33f
MagicNumber:ReceiveQrScreen.kt$32
- MagicNumber:RectangleButton.kt$0.5f
MagicNumber:RestoreWalletScreen.kt$12
MagicNumber:RestoreWalletScreen.kt$24
- MagicNumber:RestoreWalletScreen.kt$3
- MagicNumber:RestoreWalletScreen.kt$6
MagicNumber:SavingsConfirmScreen.kt$300
MagicNumber:SavingsProgressScreen.kt$2500
MagicNumber:SavingsProgressScreen.kt$5000
@@ -324,13 +240,9 @@
MagicNumber:SendConfirmScreen.kt$300
MagicNumber:SendConfirmScreen.kt$43
MagicNumber:SendConfirmScreen.kt$654_321
- MagicNumber:SettingUpScreen.kt$3
MagicNumber:SettingUpScreen.kt$5000
- MagicNumber:SettingsButtonRow.kt$0.5f
- MagicNumber:SettingsTextButtonRow.kt$0.5f
MagicNumber:SettingsViewModel.kt$SettingsViewModel$5000
MagicNumber:ShareSheet.kt$100
- MagicNumber:ShowMnemonicScreen.kt$0.075f
MagicNumber:ShowMnemonicScreen.kt$12
MagicNumber:ShowMnemonicScreen.kt$24
MagicNumber:ShowMnemonicScreen.kt$300
@@ -340,23 +252,8 @@
MagicNumber:Slider.kt$50
MagicNumber:SpendingConfirmScreen.kt$300
MagicNumber:String.kt$3
- MagicNumber:SwipeToConfirm.kt$0.8f
MagicNumber:SwipeToConfirm.kt$1500
MagicNumber:SwipeToConfirm.kt$500
- MagicNumber:TabBar.kt$0.5f
- MagicNumber:TermsOfUseScreen.kt$0x52FF6600
- MagicNumber:Thread.kt$4
- MagicNumber:ToastView.kt$0XFF032E56
- MagicNumber:ToastView.kt$0XFF1D2F1C
- MagicNumber:ToastView.kt$0XFF2B1637
- MagicNumber:ToastView.kt$0XFF3C1001
- MagicNumber:ToastView.kt$0XFF491F25
- MagicNumber:TransferViewModel.kt$TransferViewModel$0.025
- MagicNumber:TransferViewModel.kt$TransferViewModel$0.98
- MagicNumber:TransferViewModel.kt$TransferViewModel$225
- MagicNumber:TransferViewModel.kt$TransferViewModel$450
- MagicNumber:TransferViewModel.kt$TransferViewModel$495
- MagicNumber:WalletRepo.kt$WalletRepo$0.9
MatchingDeclarationName:AddressType.kt$AddressTypeInfo
MatchingDeclarationName:Button.kt$ButtonSize
MatchingDeclarationName:CoinSelectPreferenceScreen.kt$CoinSelectPreferenceTestTags
@@ -368,7 +265,6 @@
MatchingDeclarationName:SavingsProgressScreen.kt$SavingsProgressState
MatchingDeclarationName:SettingsButtonRow.kt$SettingsButtonValue
MaxLineLength:ActivityDetailScreen.kt$description = "Unable to increase the fee any further. Otherwise, it will exceed half the current input balance"
- MaxLineLength:AppViewModel.kt$AppViewModel$// TODO: handle ONLY cjit as payment received. This makes it look like any channel confirmed is a received payment.
MaxLineLength:Bip39Test.kt$Bip39Test$"AbAnDoN abandon ABANDON abandon abandon abandon abandon abandon abandon abandon abandon about".toWordList().validBip39Checksum()
MaxLineLength:Bip39Test.kt$Bip39Test$"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art" to true
MaxLineLength:Bip39Test.kt$Bip39Test$"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon"
@@ -455,7 +351,6 @@
ModifierMissing:FundingScreen.kt$FundingScreen
ModifierMissing:HeadlinesEditScreen.kt$HeadlinesEditContent
ModifierMissing:HeadlinesPreviewScreen.kt$HeadlinesPreviewContent
- ModifierMissing:HighBalanceWarningSheet.kt$HighBalanceWarningContent
ModifierMissing:HomeNav.kt$HomeNav
ModifierMissing:InfoScreenContent.kt$InfoScreenContent
ModifierMissing:InitializingWalletView.kt$InitializingWalletView
@@ -464,7 +359,6 @@
ModifierMissing:LogsScreen.kt$LogDetailScreen
ModifierMissing:LogsScreen.kt$LogsScreen
ModifierMissing:Money.kt$MoneyDisplay
- ModifierMissing:NewTransactionSheet.kt$NewTransactionSheetView
ModifierMissing:OnboardingSlidesScreen.kt$OnboardingSlidesScreen
ModifierMissing:PinChooseScreen.kt$PinChooseScreen
ModifierMissing:PinSheet.kt$PinSheet
@@ -472,7 +366,6 @@
ModifierMissing:PricePreviewScreen.kt$PricePreviewContent
ModifierMissing:ProfileIntroScreen.kt$ProfileIntroScreen
ModifierMissing:QrScanningScreen.kt$QrScanningScreen
- ModifierMissing:QuickPayIntroScreen.kt$QuickPayIntroScreen
ModifierMissing:QuickPaySettingsScreen.kt$QuickPaySettingsScreenContent
ModifierMissing:ReceiveSheet.kt$ReceiveSheet
ModifierMissing:ReportIssueResultScreen.kt$ReportIssueResultScreen
@@ -493,7 +386,6 @@
ModifierMissing:Spacers.kt$FillWidth
ModifierMissing:Spacers.kt$HorizontalSpacer
ModifierMissing:Spacers.kt$VerticalSpacer
- ModifierMissing:SpendingAdvancedScreen.kt$SpendingAdvancedScreen
ModifierMissing:SpendingIntroScreen.kt$SpendingIntroScreen
ModifierMissing:SpendingWalletScreen.kt$SpendingWalletScreen
ModifierMissing:SplashScreen.kt$SplashScreen
@@ -504,7 +396,6 @@
ModifierMissing:WeatherEditScreen.kt$WeatherEditContent
ModifierMissing:WeatherPreviewScreen.kt$WeatherPreviewContent
ModifierMissing:WidgetsIntroScreen.kt$WidgetsIntroScreen
- ModifierNotUsedAtRoot:AmountInput.kt$modifier = modifier.clickableAlpha { currency.switchUnit() }
ModifierNotUsedAtRoot:SettingsTextButtonRow.kt$modifier = modifier.then(if (!enabled) Modifier.alpha(0.5f) else Modifier)
ModifierWithoutDefault:ReceiveQrScreen.kt$modifier
ModifierWithoutDefault:SyncNodeView.kt$modifier
@@ -592,7 +483,6 @@
TooGenericExceptionCaught:ServiceQueue.kt$ServiceQueue$e: Exception
TooGenericExceptionCaught:SettingUpScreen.kt$e: Throwable
TooGenericExceptionCaught:ShopWebViewInterface.kt$ShopWebViewInterface$e: Exception
- TooGenericExceptionCaught:SpendingAdvancedScreen.kt$e: Throwable
TooGenericExceptionCaught:TransferViewModel.kt$TransferViewModel$e: Throwable
TooGenericExceptionCaught:VssBackupClient.kt$VssBackupClient$e: Throwable
TooGenericExceptionCaught:WakeNodeWorker.kt$WakeNodeWorker$e: Exception
@@ -611,23 +501,17 @@
TooManyFunctions:BlocktankRepo.kt$BlocktankRepo
TooManyFunctions:BoostTransactionViewModel.kt$BoostTransactionViewModel : ViewModel
TooManyFunctions:CacheStore.kt$CacheStore
- TooManyFunctions:ChannelDetailScreen.kt$to.bitkit.ui.settings.lightning.ChannelDetailScreen.kt
TooManyFunctions:ChannelOrdersScreen.kt$to.bitkit.ui.settings.ChannelOrdersScreen.kt
- TooManyFunctions:ChannelStatusView.kt$to.bitkit.ui.settings.lightning.components.ChannelStatusView.kt
TooManyFunctions:ContentView.kt$to.bitkit.ui.ContentView.kt
TooManyFunctions:CoreService.kt$ActivityService
TooManyFunctions:CoreService.kt$BlocktankService
TooManyFunctions:DevSettingsViewModel.kt$DevSettingsViewModel : ViewModel
TooManyFunctions:ElectrumConfigViewModel.kt$ElectrumConfigViewModel : ViewModel
- TooManyFunctions:ExternalNodeViewModel.kt$ExternalNodeViewModel : ViewModel
TooManyFunctions:HomeViewModel.kt$HomeViewModel : ViewModel
TooManyFunctions:LightningConnectionsViewModel.kt$LightningConnectionsViewModel : ViewModel
TooManyFunctions:LightningRepo.kt$LightningRepo
TooManyFunctions:LightningService.kt$LightningService : BaseCoroutineScope
TooManyFunctions:Logger.kt$Logger
- TooManyFunctions:NodeInfoScreen.kt$to.bitkit.ui.NodeInfoScreen.kt
- TooManyFunctions:SendAmountScreen.kt$to.bitkit.ui.screens.wallets.send.SendAmountScreen.kt
- TooManyFunctions:SendConfirmScreen.kt$to.bitkit.ui.screens.wallets.send.SendConfirmScreen.kt
TooManyFunctions:SettingsViewModel.kt$SettingsViewModel : ViewModel
TooManyFunctions:TOS.kt$to.bitkit.ui.onboarding.TOS.kt
TooManyFunctions:TagMetadataDao.kt$TagMetadataDao
@@ -639,22 +523,7 @@
TooManyFunctions:WidgetsStore.kt$WidgetsStore
TopLevelPropertyNaming:DrawerMenu.kt$private const val zIndexMenu = 11f
TopLevelPropertyNaming:DrawerMenu.kt$private const val zIndexScrim = 10f
- UnusedPrivateProperty:ActivityRepoTest.kt$ActivityRepoTest$private val testOnChainActivity = mock<Activity.Onchain> { on { v1 } doReturn testOnChainActivityV1 }
UnusedPrivateProperty:CurrencyRepoTest.kt$CurrencyRepoTest$private val toastEventBus: ToastEventBus = mock()
- ViewModelForwarding:ActivityDetailScreen.kt$ActivityAddTagSheet( listViewModel = listViewModel, activityViewModel = detailViewModel, onDismiss = { showAddTagSheet = false }, )
- ViewModelForwarding:ContentView.kt$BackupSheet(sheet, appViewModel)
- ViewModelForwarding:ContentView.kt$LnurlAuthSheet(sheet, appViewModel)
- ViewModelForwarding:ContentView.kt$PinSheet(sheet, appViewModel)
- ViewModelForwarding:ContentView.kt$RootNavHost( navController = navController, walletViewModel = walletViewModel, appViewModel = appViewModel, activityListViewModel = activityListViewModel, settingsViewModel = settingsViewModel, currencyViewModel = currencyViewModel, transferViewModel = transferViewModel, )
- ViewModelForwarding:ContentView.kt$SendSheet( appViewModel = appViewModel, walletViewModel = walletViewModel, startDestination = sheet.route, )
- ViewModelForwarding:ContentView.kt$SettingUpScreen( viewModel = transferViewModel, onCloseClick = { navController.popBackStack<Routes.TransferRoot>(inclusive = true) }, onContinueClick = { navController.popBackStack<Routes.TransferRoot>(inclusive = true) }, )
- ViewModelForwarding:ContentView.kt$SpendingAdvancedScreen( viewModel = transferViewModel, onBackClick = { navController.popBackStack() }, onCloseClick = { navController.navigateToHome() }, onOrderCreated = { navController.popBackStack<Routes.SpendingConfirm>(inclusive = false) }, )
- ViewModelForwarding:ContentView.kt$SpendingAmountScreen( viewModel = transferViewModel, onBackClick = { navController.popBackStack() }, onCloseClick = { navController.navigateToHome() }, onOrderCreated = { navController.navigate(Routes.SpendingConfirm) }, toastException = { appViewModel.toast(it) }, toast = { title, description -> appViewModel.toast( type = Toast.ToastType.ERROR, title = title, description = description ) }, )
- ViewModelForwarding:ContentView.kt$SpendingConfirmScreen( viewModel = transferViewModel, onBackClick = { navController.popBackStack() }, onCloseClick = { navController.navigateToHome() }, onLearnMoreClick = { navController.navigate(Routes.TransferLiquidity) }, onAdvancedClick = { navController.navigate(Routes.SpendingAdvanced) }, onConfirm = { navController.navigate(Routes.SettingUp) }, )
- ViewModelForwarding:HomeNav.kt$AllActivityScreen( viewModel = activityListViewModel, onBack = { activityListViewModel.clearFilters() walletNavController.popBackStack() }, onActivityItemClick = { rootNavController.navigateToActivityItem(it) }, )
- ViewModelForwarding:HomeNav.kt$HomeScreen( mainUiState = mainUiState, drawerState = drawerState, rootNavController = rootNavController, walletNavController = walletNavController, settingsViewModel = settingsViewModel, walletViewModel = walletViewModel, appViewModel = appViewModel, activityListViewModel = activityListViewModel, )
- ViewModelForwarding:HomeNav.kt$NavContent( walletNavController = walletNavController, rootNavController = rootNavController, mainUiState = uiState, drawerState = drawerState, settingsViewModel = settingsViewModel, appViewModel = appViewModel, walletViewModel = walletViewModel, activityListViewModel = activityListViewModel, )
- ViewModelForwarding:HomeScreen.kt$DeleteWidgetAlert(type, homeViewModel)
WildcardImport:LightningChannel.kt$import androidx.compose.foundation.layout.*
diff --git a/app/src/main/java/to/bitkit/env/Env.kt b/app/src/main/java/to/bitkit/env/Env.kt
index dd8d6ff8c8..b9ed0ac979 100644
--- a/app/src/main/java/to/bitkit/env/Env.kt
+++ b/app/src/main/java/to/bitkit/env/Env.kt
@@ -3,10 +3,11 @@ package to.bitkit.env
import android.os.Build
import org.lightningdevkit.ldknode.LogLevel
import org.lightningdevkit.ldknode.Network
+import org.lightningdevkit.ldknode.PeerDetails
import to.bitkit.BuildConfig
import to.bitkit.ext.ensureDir
+import to.bitkit.ext.parse
import to.bitkit.models.BlocktankNotificationType
-import to.bitkit.models.LnPeer
import to.bitkit.utils.Logger
import java.io.File
import kotlin.io.path.Path
@@ -24,8 +25,8 @@ internal object Env {
// TODO: remove this to load from BT API instead
val trustedLnPeers
get() = when (network) {
- Network.REGTEST -> listOf(Peers.btStaging)
- Network.TESTNET -> listOf(Peers.btStaging)
+ Network.REGTEST -> listOf(Peers.staging)
+ Network.TESTNET -> listOf(Peers.staging)
else -> TODO("Not yet implemented")
}
@@ -143,10 +144,8 @@ internal object Env {
}
object Peers {
- val btStaging = LnPeer(
- nodeId = "028a8910b0048630d4eb17af25668cdd7ea6f2d8ae20956e7a06e2ae46ebcb69fc",
- address = "34.65.86.104:9400",
- )
+ val staging =
+ PeerDetails.parse("028a8910b0048630d4eb17af25668cdd7ea6f2d8ae20956e7a06e2ae46ebcb69fc@34.65.86.104:9400")
}
object ElectrumServers {
diff --git a/app/src/main/java/to/bitkit/ext/LightningBalance.kt b/app/src/main/java/to/bitkit/ext/LightningBalance.kt
index a05737cfc3..a1b2d2a22d 100644
--- a/app/src/main/java/to/bitkit/ext/LightningBalance.kt
+++ b/app/src/main/java/to/bitkit/ext/LightningBalance.kt
@@ -1,6 +1,6 @@
package to.bitkit.ext
-import to.bitkit.models.LightningBalance
+import org.lightningdevkit.ldknode.LightningBalance
fun LightningBalance.amountSats(): ULong {
return when (this) {
diff --git a/app/src/main/java/to/bitkit/ext/PeerDetails.kt b/app/src/main/java/to/bitkit/ext/PeerDetails.kt
new file mode 100644
index 0000000000..e30a6d9970
--- /dev/null
+++ b/app/src/main/java/to/bitkit/ext/PeerDetails.kt
@@ -0,0 +1,37 @@
+package to.bitkit.ext
+
+import org.lightningdevkit.ldknode.PeerDetails
+
+val PeerDetails.host get() = address.substringBefore(":")
+
+val PeerDetails.port get() = address.substringAfter(":")
+
+val PeerDetails.uri get() = "$nodeId@$address"
+
+fun PeerDetails.Companion.parse(uri: String): PeerDetails {
+ val parts = uri.split("@")
+ require(parts.size == 2) { "Invalid uri format, expected: '@:', got: '$uri'" }
+
+ val nodeId = parts[0]
+
+ val addressParts = parts[1].split(":")
+ require(addressParts.size == 2) { "Invalid uri format, expected: '@:', got: '$uri'" }
+
+ val host = addressParts[0]
+ val port = addressParts[1]
+ val address = "$host:$port"
+
+ return PeerDetails(
+ nodeId = nodeId,
+ address = address,
+ isConnected = false,
+ isPersisted = false,
+ )
+}
+
+fun PeerDetails.Companion.from(nodeId: String, host: String, port: String) = PeerDetails(
+ nodeId = nodeId,
+ address = "$host:$port",
+ isConnected = false,
+ isPersisted = false,
+)
diff --git a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt
index a47905aa5c..fd30c92f04 100644
--- a/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt
+++ b/app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt
@@ -37,6 +37,7 @@ import to.bitkit.utils.Logger
import to.bitkit.utils.withPerformanceLogging
import kotlin.time.Duration.Companion.minutes
+@Suppress("LongParameterList")
@HiltWorker
class WakeNodeWorker @AssistedInject constructor(
@Assisted private val appContext: Context,
@@ -120,25 +121,7 @@ class WakeNodeWorker @AssistedInject constructor(
val showDetails = settingsStore.data.first().showNotificationDetails
val openBitkitMessage = "Open Bitkit to see details"
when (event) {
- is Event.PaymentReceived -> {
- bestAttemptContent?.title = "Payment Received"
- val sats = event.amountMsat / 1000u
- // Save for UI to pick up
- NewTransactionSheetDetails.save(
- appContext,
- NewTransactionSheetDetails(
- type = NewTransactionSheetType.LIGHTNING,
- direction = NewTransactionSheetDirection.RECEIVED,
- paymentHashOrTxId = event.paymentHash,
- sats = sats.toLong(),
- )
- )
- val content = if (showDetails) "$BITCOIN_SYMBOL $sats" else openBitkitMessage
- bestAttemptContent?.body = content
- if (self.notificationType == incomingHtlc) {
- self.deliver()
- }
- }
+ is Event.PaymentReceived -> onPaymentReceived(event, showDetails, openBitkitMessage)
is Event.ChannelPending -> {
self.bestAttemptContent?.title = "Channel Opened"
@@ -146,49 +129,8 @@ class WakeNodeWorker @AssistedInject constructor(
// Don't deliver, give a chance for channelReady event to update the content if it's a turbo channel
}
- is Event.ChannelReady -> {
- if (self.notificationType == cjitPaymentArrived) {
- self.bestAttemptContent?.title = "Payment received"
- self.bestAttemptContent?.body = "Via new channel"
-
- lightningRepo.getChannels()?.find { it.channelId == event.channelId }?.let { channel ->
- val sats = channel.amountOnClose
- val content = if (showDetails) "$BITCOIN_SYMBOL $sats" else openBitkitMessage
- self.bestAttemptContent?.title = content
- val cjitEntry = channel.let { blocktankRepo.getCjitEntry(it) }
- if (cjitEntry != null) {
- // Save for UI to pick up
- NewTransactionSheetDetails.save(
- appContext,
- NewTransactionSheetDetails(
- type = NewTransactionSheetType.LIGHTNING,
- direction = NewTransactionSheetDirection.RECEIVED,
- sats = sats.toLong(),
- )
- )
- activityRepo.insertActivityFromCjit(cjitEntry = cjitEntry, channel = channel)
- }
- }
- } else if (self.notificationType == orderPaymentConfirmed) {
- self.bestAttemptContent?.title = "Channel opened"
- self.bestAttemptContent?.body = "Ready to send"
- }
- self.deliver()
- }
-
- is Event.ChannelClosed -> {
- self.bestAttemptContent?.title = "Channel closed"
- self.bestAttemptContent?.body = "Reason: ${event.reason}"
-
- if (self.notificationType == mutualClose) {
- self.bestAttemptContent?.body = "Balance moved from spending to savings"
- } else if (self.notificationType == orderPaymentConfirmed) {
- self.bestAttemptContent?.title = "Channel failed to open in the background"
- self.bestAttemptContent?.body = "Please try again"
- }
-
- self.deliver()
- }
+ is Event.ChannelReady -> onChannelReady(event, showDetails, openBitkitMessage)
+ is Event.ChannelClosed -> onChannelClosed(event)
is Event.PaymentSuccessful -> Unit
is Event.PaymentClaimable -> Unit
@@ -205,6 +147,78 @@ class WakeNodeWorker @AssistedInject constructor(
}
}
+ private suspend fun onChannelClosed(event: Event.ChannelClosed) {
+ self.bestAttemptContent?.title = "Channel closed"
+ self.bestAttemptContent?.body = "Reason: ${event.reason}"
+
+ if (self.notificationType == mutualClose) {
+ self.bestAttemptContent?.body = "Balance moved from spending to savings"
+ } else if (self.notificationType == orderPaymentConfirmed) {
+ self.bestAttemptContent?.title = "Channel failed to open in the background"
+ self.bestAttemptContent?.body = "Please try again"
+ }
+
+ self.deliver()
+ }
+
+ private suspend fun onPaymentReceived(
+ event: Event.PaymentReceived,
+ showDetails: Boolean,
+ openBitkitMessage: String,
+ ) {
+ bestAttemptContent?.title = "Payment Received"
+ val sats = event.amountMsat / 1000u
+ // Save for UI to pick up
+ NewTransactionSheetDetails.save(
+ appContext,
+ NewTransactionSheetDetails(
+ type = NewTransactionSheetType.LIGHTNING,
+ direction = NewTransactionSheetDirection.RECEIVED,
+ paymentHashOrTxId = event.paymentHash,
+ sats = sats.toLong(),
+ )
+ )
+ val content = if (showDetails) "$BITCOIN_SYMBOL $sats" else openBitkitMessage
+ bestAttemptContent?.body = content
+ if (self.notificationType == incomingHtlc) {
+ self.deliver()
+ }
+ }
+
+ private suspend fun onChannelReady(
+ event: Event.ChannelReady,
+ showDetails: Boolean,
+ openBitkitMessage: String,
+ ) {
+ if (self.notificationType == cjitPaymentArrived) {
+ self.bestAttemptContent?.title = "Payment received"
+ self.bestAttemptContent?.body = "Via new channel"
+
+ lightningRepo.getChannels()?.find { it.channelId == event.channelId }?.let { channel ->
+ val sats = channel.amountOnClose
+ val content = if (showDetails) "$BITCOIN_SYMBOL $sats" else openBitkitMessage
+ self.bestAttemptContent?.title = content
+ val cjitEntry = channel.let { blocktankRepo.getCjitEntry(it) }
+ if (cjitEntry != null) {
+ // Save for UI to pick up
+ NewTransactionSheetDetails.save(
+ appContext,
+ NewTransactionSheetDetails(
+ type = NewTransactionSheetType.LIGHTNING,
+ direction = NewTransactionSheetDirection.RECEIVED,
+ sats = sats.toLong(),
+ )
+ )
+ activityRepo.insertActivityFromCjit(cjitEntry = cjitEntry, channel = channel)
+ }
+ }
+ } else if (self.notificationType == orderPaymentConfirmed) {
+ self.bestAttemptContent?.title = "Channel opened"
+ self.bestAttemptContent?.body = "Ready to send"
+ }
+ self.deliver()
+ }
+
private suspend fun deliver() {
lightningRepo.stop()
diff --git a/app/src/main/java/to/bitkit/models/BalanceState.kt b/app/src/main/java/to/bitkit/models/BalanceState.kt
index a28b0ce28a..140d81c552 100644
--- a/app/src/main/java/to/bitkit/models/BalanceState.kt
+++ b/app/src/main/java/to/bitkit/models/BalanceState.kt
@@ -1,213 +1,15 @@
package to.bitkit.models
import kotlinx.serialization.Serializable
-import org.lightningdevkit.ldknode.BalanceSource
-import org.lightningdevkit.ldknode.BlockHash
-import org.lightningdevkit.ldknode.ChannelId
-import org.lightningdevkit.ldknode.PaymentHash
-import org.lightningdevkit.ldknode.PaymentPreimage
-import org.lightningdevkit.ldknode.PublicKey
-import org.lightningdevkit.ldknode.Txid
-import org.lightningdevkit.ldknode.BalanceDetails as LdkBalanceDetails
-import org.lightningdevkit.ldknode.LightningBalance as LdkLightningBalance
-import org.lightningdevkit.ldknode.PendingSweepBalance as LdkPendingSweepBalance
@Serializable
data class BalanceState(
val totalOnchainSats: ULong = 0uL,
val totalLightningSats: ULong = 0uL,
- val maxSendLightningSats: ULong = 0uL, // TODO use where applicable
+ val maxSendLightningSats: ULong = 0uL,
val maxSendOnchainSats: ULong = 0uL,
val balanceInTransferToSavings: ULong = 0uL,
val balanceInTransferToSpending: ULong = 0uL,
) {
val totalSats get() = totalOnchainSats + totalLightningSats
}
-
-// region BalanceDetails mapping
-
-// TODO replace when ldk-node exports uniffi kotlin bindings with serializable records
-@Serializable
-data class BalanceDetails(
- var totalOnchainBalanceSats: ULong,
- var spendableOnchainBalanceSats: ULong,
- var totalAnchorChannelsReserveSats: ULong,
- var totalLightningBalanceSats: ULong,
- var lightningBalances: List,
- var pendingBalancesFromChannelClosures: List,
-)
-
-@Serializable
-sealed class LightningBalance {
-
- @Serializable
- data class ClaimableOnChannelClose(
- val channelId: ChannelId,
- val counterpartyNodeId: PublicKey,
- val amountSatoshis: ULong,
- val transactionFeeSatoshis: ULong,
- val outboundPaymentHtlcRoundedMsat: ULong,
- val outboundForwardedHtlcRoundedMsat: ULong,
- val inboundClaimingHtlcRoundedMsat: ULong,
- val inboundHtlcRoundedMsat: ULong,
- ) : LightningBalance()
-
- @Serializable
- data class ClaimableAwaitingConfirmations(
- val channelId: ChannelId,
- val counterpartyNodeId: PublicKey,
- val amountSatoshis: ULong,
- val confirmationHeight: UInt,
- val source: BalanceSource,
- ) : LightningBalance()
-
- @Serializable
- data class ContentiousClaimable(
- val channelId: ChannelId,
- val counterpartyNodeId: PublicKey,
- val amountSatoshis: ULong,
- val timeoutHeight: UInt,
- val paymentHash: PaymentHash,
- val paymentPreimage: PaymentPreimage,
- ) : LightningBalance()
-
- @Serializable
- data class MaybeTimeoutClaimableHtlc(
- val channelId: ChannelId,
- val counterpartyNodeId: PublicKey,
- val amountSatoshis: ULong,
- val claimableHeight: UInt,
- val paymentHash: PaymentHash,
- val outboundPayment: Boolean,
- ) : LightningBalance()
-
- @Serializable
- data class MaybePreimageClaimableHtlc(
- val channelId: ChannelId,
- val counterpartyNodeId: PublicKey,
- val amountSatoshis: ULong,
- val expiryHeight: UInt,
- val paymentHash: PaymentHash,
- ) : LightningBalance()
-
- @Serializable
- data class CounterpartyRevokedOutputClaimable(
- val channelId: ChannelId,
- val counterpartyNodeId: PublicKey,
- val amountSatoshis: ULong,
- ) : LightningBalance()
-}
-
-@Serializable
-sealed class PendingSweepBalance {
-
- @Serializable
- data class PendingBroadcast(
- val channelId: ChannelId?,
- val amountSatoshis: ULong,
- ) : PendingSweepBalance()
-
- @Serializable
- data class BroadcastAwaitingConfirmation(
- val channelId: ChannelId?,
- val latestBroadcastHeight: UInt,
- val latestSpendingTxid: Txid,
- val amountSatoshis: ULong,
- ) : PendingSweepBalance()
-
- @Serializable
- data class AwaitingThresholdConfirmations(
- val channelId: ChannelId?,
- val latestSpendingTxid: Txid,
- val confirmationHash: BlockHash,
- val confirmationHeight: UInt,
- val amountSatoshis: ULong,
- ) : PendingSweepBalance()
-}
-
-fun LdkBalanceDetails.toDomainModel() = BalanceDetails(
- totalOnchainBalanceSats = totalOnchainBalanceSats,
- spendableOnchainBalanceSats = spendableOnchainBalanceSats,
- totalAnchorChannelsReserveSats = totalAnchorChannelsReserveSats,
- totalLightningBalanceSats = totalLightningBalanceSats,
- lightningBalances = lightningBalances.map { it.mapToLightningBalance() },
- pendingBalancesFromChannelClosures = pendingBalancesFromChannelClosures.map { it.mapToPendingSweepBalance() },
-)
-
-fun LdkLightningBalance.mapToLightningBalance() = when (this) {
- is LdkLightningBalance.ClaimableOnChannelClose ->
- LightningBalance.ClaimableOnChannelClose(
- channelId = channelId,
- counterpartyNodeId = counterpartyNodeId,
- amountSatoshis = amountSatoshis,
- transactionFeeSatoshis = transactionFeeSatoshis,
- outboundPaymentHtlcRoundedMsat = outboundPaymentHtlcRoundedMsat,
- outboundForwardedHtlcRoundedMsat = outboundForwardedHtlcRoundedMsat,
- inboundClaimingHtlcRoundedMsat = inboundClaimingHtlcRoundedMsat,
- inboundHtlcRoundedMsat = inboundHtlcRoundedMsat
- )
-
- is LdkLightningBalance.ClaimableAwaitingConfirmations -> LightningBalance.ClaimableAwaitingConfirmations(
- channelId = channelId,
- counterpartyNodeId = counterpartyNodeId,
- amountSatoshis = amountSatoshis,
- confirmationHeight = confirmationHeight,
- source = source
- )
-
- is LdkLightningBalance.ContentiousClaimable -> LightningBalance.ContentiousClaimable(
- channelId = channelId,
- counterpartyNodeId = counterpartyNodeId,
- amountSatoshis = amountSatoshis,
- timeoutHeight = timeoutHeight,
- paymentHash = paymentHash,
- paymentPreimage = paymentPreimage
- )
-
- is LdkLightningBalance.CounterpartyRevokedOutputClaimable -> LightningBalance.CounterpartyRevokedOutputClaimable(
- channelId = channelId,
- counterpartyNodeId = counterpartyNodeId,
- amountSatoshis = amountSatoshis
- )
-
- is LdkLightningBalance.MaybePreimageClaimableHtlc -> LightningBalance.MaybePreimageClaimableHtlc(
- channelId = channelId,
- counterpartyNodeId = counterpartyNodeId,
- amountSatoshis = amountSatoshis,
- expiryHeight = expiryHeight,
- paymentHash = paymentHash
- )
-
- is LdkLightningBalance.MaybeTimeoutClaimableHtlc -> LightningBalance.MaybeTimeoutClaimableHtlc(
- channelId = channelId,
- counterpartyNodeId = counterpartyNodeId,
- amountSatoshis = amountSatoshis,
- claimableHeight = claimableHeight,
- paymentHash = paymentHash,
- outboundPayment = outboundPayment
- )
-}
-
-fun LdkPendingSweepBalance.mapToPendingSweepBalance() = when (this) {
- is LdkPendingSweepBalance.PendingBroadcast -> PendingSweepBalance.PendingBroadcast(
- channelId = channelId,
- amountSatoshis = amountSatoshis
- )
-
- is LdkPendingSweepBalance.BroadcastAwaitingConfirmation -> PendingSweepBalance.BroadcastAwaitingConfirmation(
- channelId = channelId,
- latestBroadcastHeight = latestBroadcastHeight,
- latestSpendingTxid = latestSpendingTxid,
- amountSatoshis = amountSatoshis
- )
-
- is LdkPendingSweepBalance.AwaitingThresholdConfirmations -> PendingSweepBalance.AwaitingThresholdConfirmations(
- channelId = channelId,
- latestSpendingTxid = latestSpendingTxid,
- confirmationHash = confirmationHash,
- confirmationHeight = confirmationHeight,
- amountSatoshis = amountSatoshis
- )
-}
-
-// endregion
diff --git a/app/src/main/java/to/bitkit/models/LnPeer.kt b/app/src/main/java/to/bitkit/models/LnPeer.kt
deleted file mode 100644
index 6bf1b75a47..0000000000
--- a/app/src/main/java/to/bitkit/models/LnPeer.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-package to.bitkit.models
-
-import kotlinx.serialization.Serializable
-import org.lightningdevkit.ldknode.PeerDetails
-
-@Serializable
-data class LnPeer(
- val nodeId: String,
- val host: String,
- val port: String,
- val isConnected: Boolean = false,
- val isPersisted: Boolean = false,
-) {
- constructor(
- nodeId: String,
- address: String,
- ) : this(
- nodeId,
- address.substringBefore(":"),
- address.substringAfter(":"),
- )
-
- constructor(peerDetails: PeerDetails) : this(
- nodeId = peerDetails.nodeId,
- host = peerDetails.address.substringBefore(":"),
- port = peerDetails.address.substringAfter(":"),
- isConnected = peerDetails.isConnected,
- isPersisted = peerDetails.isPersisted,
- )
-
- val address get() = "$host:$port"
-
- override fun toString() = "$nodeId@$address"
-
- companion object {
- fun parseUri(uriString: String): Result {
- val uriComponents = uriString.split("@")
- val nodeId = uriComponents[0]
-
- if (uriComponents.size != 2) {
- return Result.failure(Exception("Invalid peer uri"))
- }
-
- val address = uriComponents[1].split(":")
-
- if (address.size < 2) {
- return Result.failure(Exception("Invalid peer uri"))
- }
-
- val ip = address[0]
- val port = address[1]
-
- return Result.success(
- LnPeer(
- nodeId = nodeId,
- host = ip,
- port = port,
- )
- )
- }
- }
-}
diff --git a/app/src/main/java/to/bitkit/models/OpenChannelResult.kt b/app/src/main/java/to/bitkit/models/OpenChannelResult.kt
index 959c52a04d..a3dbacbec0 100644
--- a/app/src/main/java/to/bitkit/models/OpenChannelResult.kt
+++ b/app/src/main/java/to/bitkit/models/OpenChannelResult.kt
@@ -1,11 +1,12 @@
package to.bitkit.models
import org.lightningdevkit.ldknode.ChannelConfig
+import org.lightningdevkit.ldknode.PeerDetails
import org.lightningdevkit.ldknode.UserChannelId
data class OpenChannelResult(
val userChannelId: UserChannelId,
- val peer: LnPeer,
+ val peer: PeerDetails,
val channelAmountSats: ULong,
val pushToCounterpartySats: ULong? = null,
val channelConfig: ChannelConfig? = null,
diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt
index df6ad4b3ea..eddc82c267 100644
--- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt
+++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt
@@ -23,12 +23,14 @@ import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.withTimeoutOrNull
import org.lightningdevkit.ldknode.Address
+import org.lightningdevkit.ldknode.BalanceDetails
import org.lightningdevkit.ldknode.BestBlock
import org.lightningdevkit.ldknode.ChannelConfig
import org.lightningdevkit.ldknode.ChannelDetails
import org.lightningdevkit.ldknode.NodeStatus
import org.lightningdevkit.ldknode.PaymentDetails
import org.lightningdevkit.ldknode.PaymentId
+import org.lightningdevkit.ldknode.PeerDetails
import org.lightningdevkit.ldknode.SpendableUtxo
import org.lightningdevkit.ldknode.Txid
import to.bitkit.data.CacheStore
@@ -37,9 +39,7 @@ import to.bitkit.data.keychain.Keychain
import to.bitkit.di.BgDispatcher
import to.bitkit.env.Env
import to.bitkit.ext.getSatsPerVByteFor
-import to.bitkit.models.BalanceDetails
import to.bitkit.models.CoinSelectionPreference
-import to.bitkit.models.LnPeer
import to.bitkit.models.NodeLifecycleState
import to.bitkit.models.OpenChannelResult
import to.bitkit.models.TransactionMetadata
@@ -63,8 +63,6 @@ import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
-private const val SYNC_TIMEOUT_MS = 20_000L
-
@Singleton
class LightningRepo @Inject constructor(
@BgDispatcher private val bgDispatcher: CoroutineDispatcher,
@@ -421,7 +419,7 @@ class LightningRepo @Inject constructor(
Result.success(Unit)
}
- suspend fun connectPeer(peer: LnPeer): Result = executeWhenNodeRunning("connectPeer") {
+ suspend fun connectPeer(peer: PeerDetails): Result = executeWhenNodeRunning("connectPeer") {
lightningService.connectPeer(peer).onFailure { e ->
return@executeWhenNodeRunning Result.failure(e)
}
@@ -429,7 +427,7 @@ class LightningRepo @Inject constructor(
Result.success(Unit)
}
- suspend fun disconnectPeer(peer: LnPeer): Result = executeWhenNodeRunning("Disconnect peer") {
+ suspend fun disconnectPeer(peer: PeerDetails): Result = executeWhenNodeRunning("Disconnect peer") {
lightningService.disconnectPeer(peer)
syncState()
Result.success(Unit)
@@ -538,7 +536,9 @@ class LightningRepo @Inject constructor(
channelId: String? = null,
isMaxAmount: Boolean = false,
): Result =
- executeWhenNodeRunning("Send on-chain") {
+ executeWhenNodeRunning("sendOnChain") {
+ require(address.isNotEmpty()) { "Send address cannot be empty" }
+
val transactionSpeed = speed ?: settingsStore.data.first().defaultTransactionSpeed
val satsPerVByte = getFeeRateForSpeed(transactionSpeed, feeRates).getOrThrow().toUInt()
@@ -662,7 +662,7 @@ class LightningRepo @Inject constructor(
}
suspend fun openChannel(
- peer: LnPeer,
+ peer: PeerDetails,
channelAmountSats: ULong,
pushToCounterpartySats: ULong? = null,
channelConfig: ChannelConfig? = null,
@@ -721,7 +721,7 @@ class LightningRepo @Inject constructor(
fun getStatus(): NodeStatus? =
if (_lightningState.value.nodeLifecycleState.isRunning()) lightningService.status else null
- fun getPeers(): List? =
+ fun getPeers(): List? =
if (_lightningState.value.nodeLifecycleState.isRunning()) lightningService.peers else null
fun getChannels(): List? =
@@ -864,8 +864,9 @@ class LightningRepo @Inject constructor(
}
}
- private companion object {
- const val TAG = "LightningRepo"
+ companion object {
+ private const val TAG = "LightningRepo"
+ private const val SYNC_TIMEOUT_MS = 20_000L
}
}
@@ -875,7 +876,7 @@ data class LightningState(
val nodeId: String = "",
val nodeStatus: NodeStatus? = null,
val nodeLifecycleState: NodeLifecycleState = NodeLifecycleState.Stopped,
- val peers: List = emptyList(),
+ val peers: List = emptyList(),
val channels: List = emptyList(),
val balances: BalanceDetails? = null,
val isSyncingWallet: Boolean = false,
diff --git a/app/src/main/java/to/bitkit/services/LightningService.kt b/app/src/main/java/to/bitkit/services/LightningService.kt
index aa2746e284..9b27c25b34 100644
--- a/app/src/main/java/to/bitkit/services/LightningService.kt
+++ b/app/src/main/java/to/bitkit/services/LightningService.kt
@@ -13,6 +13,7 @@ import kotlinx.coroutines.withTimeout
import org.lightningdevkit.ldknode.Address
import org.lightningdevkit.ldknode.AnchorChannelsConfig
import org.lightningdevkit.ldknode.BackgroundSyncConfig
+import org.lightningdevkit.ldknode.BalanceDetails
import org.lightningdevkit.ldknode.Bolt11Invoice
import org.lightningdevkit.ldknode.Bolt11InvoiceDescription
import org.lightningdevkit.ldknode.BuildException
@@ -29,6 +30,7 @@ import org.lightningdevkit.ldknode.NodeException
import org.lightningdevkit.ldknode.NodeStatus
import org.lightningdevkit.ldknode.PaymentDetails
import org.lightningdevkit.ldknode.PaymentId
+import org.lightningdevkit.ldknode.PeerDetails
import org.lightningdevkit.ldknode.SpendableUtxo
import org.lightningdevkit.ldknode.Txid
import org.lightningdevkit.ldknode.defaultConfig
@@ -42,10 +44,8 @@ import to.bitkit.env.Env
import to.bitkit.ext.DatePattern
import to.bitkit.ext.totalNextOutboundHtlcLimitSats
import to.bitkit.ext.uByteList
-import to.bitkit.models.BalanceDetails
-import to.bitkit.models.LnPeer
+import to.bitkit.ext.uri
import to.bitkit.models.OpenChannelResult
-import to.bitkit.models.toDomainModel
import to.bitkit.utils.LdkError
import to.bitkit.utils.Logger
import to.bitkit.utils.ServiceError
@@ -59,7 +59,6 @@ import javax.inject.Singleton
import kotlin.io.path.Path
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
-import kotlin.toString
typealias NodeEventHandler = suspend (Event) -> Unit
@@ -74,7 +73,7 @@ class LightningService @Inject constructor(
@Volatile
var node: Node? = null
- private lateinit var trustedLnPeers: List
+ private lateinit var trustedPeers: List
suspend fun setup(
walletIndex: Int,
@@ -85,19 +84,20 @@ class LightningService @Inject constructor(
val passphrase = keychain.loadString(Keychain.Key.BIP39_PASSPHRASE.name)
// TODO get trustedLnPeers from blocktank info
- this.trustedLnPeers = Env.trustedLnPeers
+ this.trustedPeers = Env.trustedLnPeers
val dirPath = Env.ldkStoragePath(walletIndex)
- val config = defaultConfig().apply {
- storageDirPath = dirPath
- network = Env.network
+ val trustedPeerNodeIds = trustedPeers.map { it.nodeId }
- trustedPeers0conf = trustedLnPeers.map { it.nodeId }
+ val config = defaultConfig().copy(
+ storageDirPath = dirPath,
+ network = Env.network,
+ trustedPeers0conf = trustedPeerNodeIds,
anchorChannelsConfig = AnchorChannelsConfig(
- trustedPeersNoReserve = trustedPeers0conf,
+ trustedPeersNoReserve = trustedPeerNodeIds,
perChannelReserveSats = 1u,
)
- }
+ )
val builder = Builder.fromConfig(config).apply {
setFilesystemLogger(generateLogFilePath(), Env.ldkLogLevel)
@@ -274,7 +274,7 @@ class LightningService @Inject constructor(
val node = this.node ?: throw ServiceError.NodeNotSetup
ServiceQueue.LDK.background {
- for (peer in trustedLnPeers) {
+ for (peer in trustedPeers) {
try {
node.connect(peer.nodeId, peer.address, persist = true)
Logger.info("Connected to trusted peer: $peer")
@@ -285,54 +285,59 @@ class LightningService @Inject constructor(
}
}
- suspend fun connectPeer(peer: LnPeer): Result {
+ suspend fun connectPeer(peer: PeerDetails): Result {
val node = this.node ?: throw ServiceError.NodeNotSetup
-
+ val uri = peer.uri
return ServiceQueue.LDK.background {
try {
- Logger.debug("Connecting peer: $peer")
+ Logger.debug("Connecting peer: $uri")
node.connect(peer.nodeId, peer.address, persist = true)
- Logger.info("Peer connected: $peer")
+ Logger.info("Peer connected: $uri")
Result.success(Unit)
} catch (e: NodeException) {
val error = LdkError(e)
- Logger.error("Peer connect error: $peer", error)
+ Logger.error("Peer connect error: $uri", error)
Result.failure(error)
}
}
}
- suspend fun disconnectPeer(peer: LnPeer) {
+ suspend fun disconnectPeer(peer: PeerDetails) {
val node = this.node ?: throw ServiceError.NodeNotSetup
- Logger.debug("Disconnecting peer: $peer")
+ val uri = peer.uri
+ Logger.debug("Disconnecting peer: $uri")
try {
ServiceQueue.LDK.background {
node.disconnect(peer.nodeId)
}
- Logger.info("Peer disconnected: $peer")
+ Logger.info("Peer disconnected: $uri")
} catch (e: NodeException) {
- Logger.warn("Peer disconnect error: $peer", LdkError(e))
+ Logger.warn("Peer disconnect error: $uri", LdkError(e))
}
}
- private fun getLspPeers(): List {
+ private fun getLspPeers(): List {
val lspPeers = Env.trustedLnPeers
// TODO get from blocktank info.nodes[] when setup uses it to set trustedPeers0conf
// pseudocode idea:
- // val lspPeers = getInfo(refresh = true)?.nodes?.map { LnPeer(nodeId = it.pubkey, address = "TO_DO") }
+ // val lspPeers = getInfo(true)?.nodes?.map { PeerDetails.from(nodeId = it.pubkey, address = "TO DO") }
return lspPeers
}
- fun hasExternalPeers() = peers?.any { p -> p.toString() !in getLspPeers().map { it.toString() } } == true
+ fun hasExternalPeers(): Boolean {
+ val ourPeers = this.peers.orEmpty().map { it.uri }
+ val lspPeers = getLspPeers().map { it.uri }.toSet()
+ return ourPeers.any { p -> p !in lspPeers }
+ }
// endregion
// region channels
suspend fun openChannel(
- peer: LnPeer,
+ peer: PeerDetails,
channelAmountSats: ULong,
pushToCounterpartySats: ULong? = null,
channelConfig: ChannelConfig? = null,
@@ -341,7 +346,7 @@ class LightningService @Inject constructor(
return ServiceQueue.LDK.background {
try {
- Logger.debug("Initiating channel open (sats: $channelAmountSats) with peer: $peer")
+ Logger.debug("Initiating channel open (sats: $channelAmountSats) with peer: ${peer.uri}")
val userChannelId = node.openChannel(
nodeId = peer.nodeId,
@@ -764,10 +769,10 @@ class LightningService @Inject constructor(
// region state
val nodeId: String? get() = node?.nodeId()
- val balances: BalanceDetails? get() = node?.listBalances()?.toDomainModel()
+ val balances: BalanceDetails? get() = node?.listBalances()
val status: NodeStatus? get() = node?.status()
val config: Config? get() = node?.config()
- val peers: List? get() = node?.listPeers()?.map(::LnPeer)
+ val peers: List? get() = node?.listPeers()
val channels: List? get() = node?.listChannels()
val payments: List? get() = node?.listPayments()
diff --git a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt
index 4075548956..50d4be7dc3 100644
--- a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt
+++ b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt
@@ -31,19 +31,21 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import kotlinx.datetime.Clock
+import org.lightningdevkit.ldknode.BalanceDetails
import org.lightningdevkit.ldknode.BalanceSource
import org.lightningdevkit.ldknode.BestBlock
import org.lightningdevkit.ldknode.ChannelDetails
+import org.lightningdevkit.ldknode.LightningBalance
import org.lightningdevkit.ldknode.NodeStatus
+import org.lightningdevkit.ldknode.PeerDetails
import to.bitkit.R
+import to.bitkit.env.Env
import to.bitkit.ext.amountSats
import to.bitkit.ext.balanceUiText
import to.bitkit.ext.channelId
import to.bitkit.ext.createChannelDetails
import to.bitkit.ext.formatted
-import to.bitkit.models.BalanceDetails
-import to.bitkit.models.LightningBalance
-import to.bitkit.models.LnPeer
+import to.bitkit.ext.uri
import to.bitkit.models.NodeLifecycleState
import to.bitkit.models.Toast
import to.bitkit.models.formatToModernDisplay
@@ -108,7 +110,7 @@ private fun Content(
onBack: () -> Unit = {},
onClose: () -> Unit = {},
onRefresh: () -> Unit = {},
- onDisconnectPeer: (LnPeer) -> Unit = {},
+ onDisconnectPeer: (PeerDetails) -> Unit = {},
onCopy: (String) -> Unit = {},
) {
ScreenColumn {
@@ -178,7 +180,7 @@ private fun NodeIdSection(
modifier = Modifier
.clickableAlpha(
onClick = copyToClipboard(nodeId) {
- onCopy(nodeId)
+ onCopy(it)
}
)
.testTag("LDKNodeID")
@@ -307,7 +309,7 @@ private fun ChannelsSection(
overflow = TextOverflow.MiddleEllipsis,
modifier = Modifier.clickableAlpha(
onClick = copyToClipboard(channel.channelId) {
- onCopy(channel.channelId)
+ onCopy(it)
}
)
)
@@ -366,8 +368,8 @@ private fun ChannelsSection(
@Composable
private fun PeersSection(
- peers: List,
- onDisconnectPeer: (LnPeer) -> Unit,
+ peers: List,
+ onDisconnectPeer: (PeerDetails) -> Unit,
onCopy: (String) -> Unit = {},
) {
Column(modifier = Modifier.fillMaxWidth()) {
@@ -379,14 +381,14 @@ private fun PeersSection(
modifier = Modifier.height(52.dp)
) {
BodyM(
- text = peer.toString(),
+ text = peer.uri,
maxLines = 1,
overflow = TextOverflow.MiddleEllipsis,
modifier = Modifier
.weight(1f)
.clickableAlpha(
- onClick = copyToClipboard(peer.toString()) {
- onCopy(peer.toString())
+ onClick = copyToClipboard(peer.uri) {
+ onCopy(it)
}
)
)
@@ -465,12 +467,7 @@ private fun PreviewDevMode() {
latestChannelMonitorArchivalHeight = null,
),
nodeId = "0348a2b7c2d3f4e5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9",
- peers = listOf(
- LnPeer(
- nodeId = "0248a2b7c2d3f4e5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9",
- address = "192.168.1.1:9735",
- ),
- ),
+ peers = listOf(Env.Peers.staging),
channels = listOf(
createChannelDetails().copy(
channelId = "abc123def456789012345678901234567890123456789012345678901234567890",
diff --git a/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt b/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt
index f18b44ae6d..e3e8af358e 100644
--- a/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt
+++ b/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt
@@ -1,6 +1,8 @@
package to.bitkit.ui.components
import android.graphics.Bitmap
+import androidx.compose.animation.Crossfade
+import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -21,6 +23,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
@@ -37,7 +40,6 @@ import androidx.compose.ui.unit.dp
import androidx.core.graphics.createBitmap
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
-import com.google.zxing.WriterException
import com.google.zxing.qrcode.QRCodeWriter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -47,6 +49,10 @@ import to.bitkit.ui.theme.AppShapes
import to.bitkit.ui.theme.AppThemeSurface
import to.bitkit.ui.theme.Colors
+private const val QUIET_ZONE_MIN = 2
+private const val QUIET_ZONE_MAX = 4
+private const val QUIET_ZONE_RATIO = 150
+
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun QrCodeImage(
@@ -63,11 +69,11 @@ fun QrCodeImage(
val coroutineScope = rememberCoroutineScope()
Box(
- contentAlignment = Alignment.TopCenter,
+ contentAlignment = Alignment.Center,
modifier = modifier
- .background(Color.White, AppShapes.small)
.aspectRatio(1f)
- .padding(8.dp)
+ .clip(AppShapes.small)
+ .background(Color.White)
) {
val bitmap = rememberQrBitmap(content, size)
@@ -75,53 +81,61 @@ fun QrCodeImage(
onBitmapGenerated(bitmap)
}
- if (bitmap != null) {
- val imageComposable = @Composable {
- Image(
- painter = remember(bitmap) { BitmapPainter(bitmap.asImageBitmap()) },
- contentDescription = content,
- contentScale = ContentScale.Inside,
- modifier = Modifier
- .clickable(enabled = tipMessage.isNotBlank()) {
- coroutineScope.launch {
- context.setClipboardText(content)
- tooltipState.show()
+ Crossfade(
+ targetState = bitmap,
+ animationSpec = tween(durationMillis = 200),
+ label = "QR Code Crossfade"
+ ) { currentBitmap ->
+ if (currentBitmap != null) {
+ val imageComposable = @Composable {
+ Image(
+ painter = remember(currentBitmap) { BitmapPainter(currentBitmap.asImageBitmap()) },
+ contentDescription = content,
+ contentScale = ContentScale.Inside,
+ modifier = Modifier
+ .clickable(enabled = tipMessage.isNotBlank()) {
+ coroutineScope.launch {
+ context.setClipboardText(content)
+ tooltipState.show()
+ }
}
- }
- .then(testTag?.let { Modifier.testTag(it) } ?: Modifier)
- )
+ .then(testTag?.let { Modifier.testTag(it) } ?: Modifier)
+ )
+ }
+
+ if (tipMessage.isNotBlank()) {
+ Tooltip(
+ text = tipMessage,
+ tooltipState = tooltipState,
+ content = imageComposable,
+ )
+ } else {
+ imageComposable()
+ }
}
+ }
- if (tipMessage.isNotBlank()) {
- Tooltip(
- text = tipMessage,
- tooltipState = tooltipState,
- content = imageComposable,
+ logoPainter?.let {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .size(68.dp)
+ .background(Color.White, shape = CircleShape)
+ .align(Alignment.Center)
+ ) {
+ Image(
+ painter = it,
+ contentDescription = null,
+ modifier = Modifier.size(50.dp)
)
- } else {
- imageComposable()
}
+ }
- logoPainter?.let {
- Box(
- contentAlignment = Alignment.Center,
- modifier = Modifier
- .size(68.dp)
- .background(Color.White, shape = CircleShape)
- .align(Alignment.Center)
- ) {
- Image(
- painter = it,
- contentDescription = null,
- modifier = Modifier.size(50.dp)
- )
- }
- }
- } else {
+ if (bitmap == null) {
CircularProgressIndicator(
color = Colors.Black,
- strokeWidth = 2.dp,
- modifier = Modifier.align(Alignment.Center)
+ strokeWidth = 4.dp,
+ modifier = Modifier.size(68.dp)
)
}
}
@@ -135,16 +149,16 @@ private fun rememberQrBitmap(content: String, size: Dp): Bitmap? {
val sizePx = with(LocalDensity.current) { size.roundToPx() }
LaunchedEffect(content, size) {
- if (bitmap != null) return@LaunchedEffect
+ bitmap = null // Always reset to show loading indicator
launch(Dispatchers.Default) {
val qrCodeWriter = QRCodeWriter()
- val encodeHints = mutableMapOf().apply {
- this[EncodeHintType.MARGIN] = 0
- }
+ val quietZoneModules = (content.length / QUIET_ZONE_RATIO + 1).coerceIn(QUIET_ZONE_MIN, QUIET_ZONE_MAX)
+
+ val encodeHints = mapOf(EncodeHintType.MARGIN to quietZoneModules)
- val bitmapMatrix = try {
+ val bitmapMatrix = runCatching {
qrCodeWriter.encode(
content,
BarcodeFormat.QR_CODE,
@@ -152,29 +166,22 @@ private fun rememberQrBitmap(content: String, size: Dp): Bitmap? {
sizePx,
encodeHints,
)
- } catch (_: WriterException) {
- null
- }
+ }.getOrElse { return@launch }
- val matrixWidth = bitmapMatrix?.width ?: sizePx
- val matrixHeight = bitmapMatrix?.height ?: sizePx
-
- val newBitmap = createBitmap(
- width = bitmapMatrix?.width ?: sizePx,
- height = bitmapMatrix?.height ?: sizePx
- )
+ val matrixWidth = bitmapMatrix.width
+ val matrixHeight = bitmapMatrix.height
+ val newBitmap = createBitmap(width = matrixWidth, height = matrixHeight)
val pixels = IntArray(matrixWidth * matrixHeight)
for (x in 0 until matrixWidth) {
for (y in 0 until matrixHeight) {
- val shouldColorPixel = bitmapMatrix?.get(x, y) ?: false
- val pixelColor =
- if (shouldColorPixel) {
- android.graphics.Color.BLACK
- } else {
- android.graphics.Color.WHITE
- }
+ val shouldColorPixel = bitmapMatrix[x, y]
+ val pixelColor = if (shouldColorPixel) {
+ android.graphics.Color.BLACK
+ } else {
+ android.graphics.Color.WHITE
+ }
pixels[y * matrixWidth + x] = pixelColor
}
diff --git a/app/src/main/java/to/bitkit/ui/components/Tooltip.kt b/app/src/main/java/to/bitkit/ui/components/Tooltip.kt
index 8457e030f2..dfd0425d84 100644
--- a/app/src/main/java/to/bitkit/ui/components/Tooltip.kt
+++ b/app/src/main/java/to/bitkit/ui/components/Tooltip.kt
@@ -20,7 +20,7 @@ fun Tooltip(
text: String,
tooltipState: TooltipState,
modifier: Modifier = Modifier,
- content: @Composable (() -> Unit)
+ content: @Composable () -> Unit
) {
TooltipBox(
modifier = modifier,
@@ -48,6 +48,7 @@ fun Tooltip(
}
},
state = tooltipState,
+ focusable = false,
content = content
)
}
diff --git a/app/src/main/java/to/bitkit/ui/components/settings/SectionHeader.kt b/app/src/main/java/to/bitkit/ui/components/settings/SectionHeader.kt
index 3042f682f3..f654fbdb20 100644
--- a/app/src/main/java/to/bitkit/ui/components/settings/SectionHeader.kt
+++ b/app/src/main/java/to/bitkit/ui/components/settings/SectionHeader.kt
@@ -2,15 +2,21 @@ package to.bitkit.ui.components.settings
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeContent
+import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import to.bitkit.ui.components.Caption13Up
+import to.bitkit.ui.shared.util.screen
import to.bitkit.ui.theme.AppThemeSurface
import to.bitkit.ui.theme.Colors
@@ -19,22 +25,51 @@ fun SectionHeader(
title: String,
modifier: Modifier = Modifier,
color: Color = Colors.White64,
+ padding: PaddingValues = PaddingValues(top = 16.dp),
+ height: Dp = 50.dp,
) {
Column(
verticalArrangement = Arrangement.Center,
modifier = modifier
.fillMaxWidth()
- .padding(top = 16.dp)
- .height(50.dp)
+ .padding(padding)
+ .height(height)
) {
Caption13Up(text = title, color = color)
}
}
-@Preview
+@Preview(showSystemUi = true)
@Composable
private fun Preview() {
AppThemeSurface {
- SectionHeader("General")
+ Column(
+ modifier = Modifier
+ .screen(insets = WindowInsets.safeContent)
+ ) {
+ SectionHeader("Default")
+ HorizontalDivider()
+ SectionHeader(
+ title = "Colors.Brand",
+ color = Colors.Brand,
+ )
+ HorizontalDivider()
+ SectionHeader(
+ title = "Dp.Unspecified",
+ height = Dp.Unspecified,
+ )
+ HorizontalDivider()
+ SectionHeader(
+ title = "PaddingValues.Zero",
+ padding = PaddingValues.Zero,
+ )
+ HorizontalDivider()
+ SectionHeader(
+ title = "PaddingValues.Zero + Dp.Unspecified",
+ padding = PaddingValues.Zero,
+ height = Dp.Unspecified,
+ )
+ HorizontalDivider()
+ }
}
}
diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConnectionScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConnectionScreen.kt
index c1e5128c74..f34f1ad6dc 100644
--- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConnectionScreen.kt
+++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConnectionScreen.kt
@@ -33,9 +33,12 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.SavedStateHandle
import kotlinx.coroutines.flow.filterNotNull
+import org.lightningdevkit.ldknode.PeerDetails
import to.bitkit.R
+import to.bitkit.ext.from
import to.bitkit.ext.getClipboardText
-import to.bitkit.models.LnPeer
+import to.bitkit.ext.host
+import to.bitkit.ext.port
import to.bitkit.ui.Routes
import to.bitkit.ui.components.BodyM
import to.bitkit.ui.components.ButtonSize
@@ -105,7 +108,7 @@ fun ExternalConnectionScreen(
@Composable
private fun ExternalConnectionContent(
uiState: ExternalNodeContract.UiState,
- onContinueClick: (LnPeer) -> Unit = {},
+ onContinueClick: (PeerDetails) -> Unit = {},
onScanClick: () -> Unit = {},
onPasteClick: () -> Unit = {},
onBackClick: () -> Unit = {},
@@ -220,10 +223,12 @@ private fun ExternalConnectionContent(
)
PrimaryButton(
text = stringResource(R.string.common__continue),
- onClick = { onContinueClick(LnPeer(nodeId = nodeId, host = host, port = port)) },
+ onClick = { onContinueClick(PeerDetails.from(nodeId = nodeId, host = host, port = port)) },
enabled = isValid,
isLoading = uiState.isLoading,
- modifier = Modifier.weight(1f).testTag("ExternalContinue")
+ modifier = Modifier
+ .weight(1f)
+ .testTag("ExternalContinue")
)
}
Spacer(modifier = Modifier.height(16.dp))
diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt
index e3060b0880..80f2aad4ed 100644
--- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt
+++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt
@@ -13,13 +13,14 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.lightningdevkit.ldknode.Event
+import org.lightningdevkit.ldknode.PeerDetails
import org.lightningdevkit.ldknode.UserChannelId
import to.bitkit.R
import to.bitkit.data.CacheStore
import to.bitkit.data.SettingsStore
import to.bitkit.ext.WatchResult
+import to.bitkit.ext.parse
import to.bitkit.ext.watchUntil
-import to.bitkit.models.LnPeer
import to.bitkit.models.Toast
import to.bitkit.models.TransactionMetadata
import to.bitkit.models.TransactionSpeed
@@ -67,7 +68,7 @@ class ExternalNodeViewModel @Inject constructor(
}
}
- fun onConnectionContinue(peer: LnPeer) {
+ fun onConnectionContinue(peer: PeerDetails) {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
@@ -90,7 +91,7 @@ class ExternalNodeViewModel @Inject constructor(
fun parseNodeUri(uriString: String) {
viewModelScope.launch {
- val result = LnPeer.parseUri(uriString.trim())
+ val result = runCatching { PeerDetails.parse(uriString) }
if (result.isSuccess) {
_uiState.update { it.copy(peer = result.getOrNull()) }
@@ -229,7 +230,7 @@ class ExternalNodeViewModel @Inject constructor(
interface ExternalNodeContract {
data class UiState(
val isLoading: Boolean = false,
- val peer: LnPeer? = null,
+ val peer: PeerDetails? = null,
val amount: Amount = Amount(),
val networkFee: Long = 0,
val customFeeRate: UInt? = null,
diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelScreen.kt
index f8abe21ef2..9e1236f983 100644
--- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelScreen.kt
+++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelScreen.kt
@@ -23,7 +23,9 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import to.bitkit.R
-import to.bitkit.models.LnPeer
+import to.bitkit.env.Env
+import to.bitkit.ext.host
+import to.bitkit.ext.port
import to.bitkit.ui.Routes
import to.bitkit.ui.components.BodyM
import to.bitkit.ui.components.Caption13Up
@@ -173,13 +175,7 @@ private fun InfoRow(
private fun Preview() {
AppThemeSurface {
Content(
- uiState = LnurlChannelUiState(
- peer = LnPeer(
- nodeId = "12345678901234567890123456789012345678901234567890",
- host = "127.0.0.1",
- port = "9735",
- )
- ),
+ uiState = LnurlChannelUiState(peer = Env.Peers.staging),
)
}
}
diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt
index 49b1cb3eac..21891070df 100644
--- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt
+++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt
@@ -9,8 +9,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
+import org.lightningdevkit.ldknode.PeerDetails
import to.bitkit.R
-import to.bitkit.models.LnPeer
+import to.bitkit.ext.parse
import to.bitkit.models.Toast
import to.bitkit.repositories.LightningRepo
import to.bitkit.ui.Routes
@@ -37,7 +38,7 @@ class LnurlChannelViewModel @Inject constructor(
viewModelScope.launch {
lightningRepo.fetchLnurlChannelInfo(params.uri)
.onSuccess { channelInfo ->
- val peer = LnPeer.parseUri(channelInfo.uri).getOrElse {
+ val peer = runCatching { PeerDetails.parse(channelInfo.uri) }.getOrElse {
errorToast(it)
return@onSuccess
}
@@ -91,7 +92,7 @@ class LnurlChannelViewModel @Inject constructor(
}
data class LnurlChannelUiState(
- val peer: LnPeer? = null,
+ val peer: PeerDetails? = null,
val isConnecting: Boolean = false,
val isConnected: Boolean = false,
)
diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt
index 7006f85739..8ab356d07c 100644
--- a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt
+++ b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt
@@ -460,99 +460,7 @@ private fun Content(
)
}
} else {
- Column(
- modifier = Modifier.fillMaxWidth(),
- verticalArrangement = Arrangement.spacedBy(16.dp)
- ) {
- homeUiState.widgetsWithPosition.forEach { widgetsWithPosition ->
- when (widgetsWithPosition.type) {
- WidgetType.BLOCK -> {
- homeUiState.currentBlock?.run {
- BlockCard(
- showWidgetTitle = homeUiState.showWidgetTitles,
- showBlock = homeUiState.blocksPreferences.showBlock,
- showTime = homeUiState.blocksPreferences.showTime,
- showDate = homeUiState.blocksPreferences.showDate,
- showTransactions = homeUiState.blocksPreferences.showTransactions,
- showSize = homeUiState.blocksPreferences.showSize,
- showSource = homeUiState.blocksPreferences.showSource,
- time = time,
- date = date,
- transactions = transactionCount,
- size = size,
- source = source,
- block = height,
- modifier = Modifier
- .fillMaxWidth()
- .testTag("BlocksWidget")
- )
- }
- }
-
- WidgetType.CALCULATOR -> {
- currencyViewModel?.let {
- CalculatorCard(
- currencyViewModel = it,
- showWidgetTitle = homeUiState.showWidgetTitles,
- modifier = Modifier.fillMaxWidth()
- )
- }
- }
-
- WidgetType.FACTS -> {
- homeUiState.currentFact?.run {
- FactsCard(
- showWidgetTitle = homeUiState.showWidgetTitles,
- showSource = homeUiState.factsPreferences.showSource,
- headline = homeUiState.currentFact,
- modifier = Modifier.fillMaxWidth()
- )
- }
- }
-
- WidgetType.NEWS -> {
- homeUiState.currentArticle?.run {
- HeadlineCard(
- showWidgetTitle = homeUiState.showWidgetTitles,
- showTime = homeUiState.headlinePreferences.showTime,
- showSource = homeUiState.headlinePreferences.showSource,
- headline = title,
- time = timeAgo,
- source = publisher,
- link = link,
- modifier = Modifier
- .fillMaxWidth()
- .testTag("NewsWidget")
- )
- }
- }
-
- WidgetType.PRICE -> {
- homeUiState.currentPrice?.run {
- PriceCard(
- showWidgetTitle = homeUiState.showWidgetTitles,
- pricePreferences = homeUiState.pricePreferences,
- priceDTO = homeUiState.currentPrice,
- modifier = Modifier
- .fillMaxWidth()
- .testTag("PriceWidget")
- )
- }
- }
-
- WidgetType.WEATHER -> {
- homeUiState.currentWeather?.run {
- WeatherCard(
- showWidgetTitle = homeUiState.showWidgetTitles,
- weatherModel = this,
- preferences = homeUiState.weatherPreferences,
- modifier = Modifier.fillMaxWidth()
- )
- }
- }
- }
- }
- }
+ Widgets(homeUiState)
}
Spacer(modifier = Modifier.height(32.dp))
@@ -594,6 +502,103 @@ private fun Content(
}
}
+@Composable
+private fun Widgets(homeUiState: HomeUiState) {
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ homeUiState.widgetsWithPosition.forEach { widgetsWithPosition ->
+ when (widgetsWithPosition.type) {
+ WidgetType.BLOCK -> {
+ homeUiState.currentBlock?.run {
+ BlockCard(
+ showWidgetTitle = homeUiState.showWidgetTitles,
+ showBlock = homeUiState.blocksPreferences.showBlock,
+ showTime = homeUiState.blocksPreferences.showTime,
+ showDate = homeUiState.blocksPreferences.showDate,
+ showTransactions = homeUiState.blocksPreferences.showTransactions,
+ showSize = homeUiState.blocksPreferences.showSize,
+ showSource = homeUiState.blocksPreferences.showSource,
+ time = time,
+ date = date,
+ transactions = transactionCount,
+ size = size,
+ source = source,
+ block = height,
+ modifier = Modifier
+ .fillMaxWidth()
+ .testTag("BlocksWidget")
+ )
+ }
+ }
+
+ WidgetType.CALCULATOR -> {
+ currencyViewModel?.let {
+ CalculatorCard(
+ currencyViewModel = it,
+ showWidgetTitle = homeUiState.showWidgetTitles,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ }
+
+ WidgetType.FACTS -> {
+ homeUiState.currentFact?.run {
+ FactsCard(
+ showWidgetTitle = homeUiState.showWidgetTitles,
+ showSource = homeUiState.factsPreferences.showSource,
+ headline = homeUiState.currentFact,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ }
+
+ WidgetType.NEWS -> {
+ homeUiState.currentArticle?.run {
+ HeadlineCard(
+ showWidgetTitle = homeUiState.showWidgetTitles,
+ showTime = homeUiState.headlinePreferences.showTime,
+ showSource = homeUiState.headlinePreferences.showSource,
+ headline = title,
+ time = timeAgo,
+ source = publisher,
+ link = link,
+ modifier = Modifier
+ .fillMaxWidth()
+ .testTag("NewsWidget")
+ )
+ }
+ }
+
+ WidgetType.PRICE -> {
+ homeUiState.currentPrice?.run {
+ PriceCard(
+ showWidgetTitle = homeUiState.showWidgetTitles,
+ pricePreferences = homeUiState.pricePreferences,
+ priceDTO = homeUiState.currentPrice,
+ modifier = Modifier
+ .fillMaxWidth()
+ .testTag("PriceWidget")
+ )
+ }
+ }
+
+ WidgetType.WEATHER -> {
+ homeUiState.currentWeather?.run {
+ WeatherCard(
+ showWidgetTitle = homeUiState.showWidgetTitles,
+ weatherModel = this,
+ preferences = homeUiState.weatherPreferences,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun TopBar(
diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt
index 21411ed73b..ed9eb60b68 100644
--- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt
+++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt
@@ -386,7 +386,7 @@ private fun ActivityDetailContent(
.fillMaxWidth()
.clickableAlpha(
onClick = copyToClipboard(message) {
- onCopy(message)
+ onCopy(it)
}
)
) {
diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt
index 8eefa7c23f..3410218928 100644
--- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt
+++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt
@@ -196,7 +196,7 @@ private fun LightningDetails(
value = preimage,
modifier = Modifier.clickableAlpha(
onClick = copyToClipboard(preimage) {
- onCopy(preimage)
+ onCopy(it)
}
),
)
@@ -206,7 +206,7 @@ private fun LightningDetails(
value = paymentHash,
modifier = Modifier.clickableAlpha(
onClick = copyToClipboard(paymentHash) {
- onCopy(paymentHash)
+ onCopy(it)
}
),
)
@@ -215,7 +215,7 @@ private fun LightningDetails(
value = invoice,
modifier = Modifier.clickableAlpha(
onClick = copyToClipboard(invoice) {
- onCopy(invoice)
+ onCopy(it)
}
),
)
@@ -235,7 +235,7 @@ private fun ColumnScope.OnchainDetails(
modifier = Modifier
.clickableAlpha(
onClick = copyToClipboard(txId) {
- onCopy(txId)
+ onCopy(it)
}
)
.testTag("TXID")
diff --git a/app/src/main/java/to/bitkit/ui/settings/ChannelOrdersScreen.kt b/app/src/main/java/to/bitkit/ui/settings/ChannelOrdersScreen.kt
index 6808f4b5a9..3c9fb99fb3 100644
--- a/app/src/main/java/to/bitkit/ui/settings/ChannelOrdersScreen.kt
+++ b/app/src/main/java/to/bitkit/ui/settings/ChannelOrdersScreen.kt
@@ -1,15 +1,11 @@
package to.bitkit.ui.settings
-import androidx.compose.animation.core.Spring
-import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.animation.core.spring
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@@ -19,26 +15,16 @@ import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.scale
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalClipboardManager
-import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.synonym.bitkitcore.BtBolt11InvoiceState
import com.synonym.bitkitcore.BtOrderState
@@ -54,17 +40,26 @@ import com.synonym.bitkitcore.IBtPayment
import com.synonym.bitkitcore.IDiscount
import com.synonym.bitkitcore.ILspNode
import com.synonym.bitkitcore.IcJitEntry
-import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import to.bitkit.models.formatToModernDisplay
import to.bitkit.ui.Routes
import to.bitkit.ui.blocktankViewModel
import to.bitkit.ui.components.BodyS
+import to.bitkit.ui.components.BodySSB
+import to.bitkit.ui.components.Caption
+import to.bitkit.ui.components.Caption13Up
+import to.bitkit.ui.components.CaptionB
+import to.bitkit.ui.components.Footnote
+import to.bitkit.ui.components.HorizontalSpacer
import to.bitkit.ui.components.PrimaryButton
+import to.bitkit.ui.components.VerticalSpacer
+import to.bitkit.ui.components.settings.SectionHeader
import to.bitkit.ui.scaffold.AppTopBar
import to.bitkit.ui.shared.util.clickableAlpha
+import to.bitkit.ui.theme.AppShapes
import to.bitkit.ui.theme.AppThemeSurface
import to.bitkit.ui.theme.Colors
+import to.bitkit.ui.utils.copyToClipboard
import to.bitkit.utils.Logger
@Composable
@@ -77,98 +72,60 @@ fun ChannelOrdersScreen(
val orders by blocktank.orders.collectAsStateWithLifecycle()
val cJitEntries by blocktank.cJitEntries.collectAsStateWithLifecycle()
- LaunchedEffect(Unit) {
- blocktank.refreshOrders()
- }
+ LaunchedEffect(Unit) { blocktank.refreshOrders() }
- ChannelOrdersView(
+ Content(
orders = orders,
cJitEntries = cJitEntries,
- onBackClick = onBackClick,
- onOrderItemClick = onOrderItemClick,
- onCjitItemClick = onCjitItemClick,
+ onBack = onBackClick,
+ onClickOrder = onOrderItemClick,
+ onClickCjit = onCjitItemClick,
)
}
@Composable
-private fun ChannelOrdersView(
+private fun Content(
orders: List,
cJitEntries: List,
- onBackClick: () -> Unit,
- onOrderItemClick: (String) -> Unit,
- onCjitItemClick: (String) -> Unit,
+ modifier: Modifier = Modifier,
+ onBack: () -> Unit = {},
+ onClickOrder: (String) -> Unit = {},
+ onClickCjit: (String) -> Unit = {},
) {
Scaffold(
- topBar = {
- AppTopBar(
- titleText = "Channel Orders",
- onBackClick = onBackClick,
- )
- }
+ topBar = { AppTopBar(titleText = "Channel Orders", onBackClick = onBack) },
+ modifier = modifier,
) { padding ->
LazyColumn(
- modifier = Modifier.padding(padding)
+ contentPadding = PaddingValues(horizontal = 16.dp),
+ modifier = Modifier
+ .padding(padding)
) {
- item {
- Text(
- text = "Orders",
- style = MaterialTheme.typography.titleMedium,
- modifier = Modifier.padding(16.dp)
- )
+ stickyHeader {
+ SectionHeader(title = "Orders", padding = PaddingValues.Zero)
}
-
orders.let { orders ->
if (orders.isEmpty()) {
item {
- Text(
- text = "No orders found…",
- color = Color.Gray,
- modifier = Modifier.padding(16.dp)
- )
+ BodyS(text = "No CJIT entries found…")
}
} else {
items(orders) { order ->
- Card(
- colors = cardColors,
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp)
- .clickable { onOrderItemClick(order.id) }
- ) {
- OrderRow(order = order)
- }
+ OrderCard(order, onClickOrder)
}
}
}
-
- item {
- Text(
- text = "CJIT Entries",
- style = MaterialTheme.typography.titleMedium,
- modifier = Modifier.padding(16.dp)
- )
+ stickyHeader {
+ SectionHeader(title = "CJIT Entries", padding = PaddingValues.Zero)
}
-
cJitEntries.let { entries ->
if (entries.isEmpty()) {
item {
- Text(
- text = "No CJIT entries found…",
- color = Color.Gray,
- modifier = Modifier.padding(16.dp)
- )
+ BodyS(text = "No CJIT entries found…")
}
} else {
items(entries) { entry ->
- Card(
- colors = cardColors,
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp)
- .clickable { onCjitItemClick(entry.id) }
- ) {
- CJitRow(entry = entry)
- }
+ CJitCard(entry, onClickCjit)
}
}
}
@@ -179,160 +136,99 @@ private fun ChannelOrdersView(
@Composable
fun OrderDetailScreen(
orderItem: Routes.OrderDetail,
- onBackClick: () -> Unit,
+ onBackClick: () -> Unit = {},
) {
val blocktank = blocktankViewModel ?: return
val orders by blocktank.orders.collectAsStateWithLifecycle()
val order = orders.find { it.id == orderItem.id } ?: return
- OrderDetailView(
+ val coroutineScope = rememberCoroutineScope()
+
+ OrderDetailContent(
order = order,
- onBackClick = onBackClick,
+ onBack = onBackClick,
+ onClickOpen = {
+ coroutineScope.launch {
+ Logger.info("Opening channel for order ${order.id}")
+ try {
+ blocktank.openChannel(orderId = order.id)
+ Logger.info("Channel opened for order ${order.id}")
+ } catch (e: Throwable) {
+ Logger.error("Error opening channel for order ${order.id}", e)
+ }
+ }
+ },
)
}
@Composable
-private fun OrderDetailView(
+private fun OrderDetailContent(
order: IBtOrder,
- onBackClick: () -> Unit,
+ onBack: () -> Unit = {},
+ onClickOpen: () -> Unit = {},
) {
- val coroutineScope = rememberCoroutineScope()
Scaffold(
- topBar = {
- AppTopBar(
- titleText = "Order Details",
- onBackClick = onBackClick,
- )
- }
+ topBar = { AppTopBar(titleText = "Order Details", onBackClick = onBack) }
) { padding ->
LazyColumn(
+ contentPadding = PaddingValues(16.dp),
modifier = Modifier.padding(padding)
) {
- // Order Details
item {
- Card(
- colors = cardColors,
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp)
- ) {
- Column(modifier = Modifier.padding(16.dp)) {
- Text(text = "Order Details", style = MaterialTheme.typography.titleMedium)
- Spacer(modifier = Modifier.height(8.dp))
- DetailRow("ID", order.id)
- DetailRow("Onchain txs", order.payment.onchain.transactions.size.toString())
- DetailRow("State", order.state.toString())
- DetailRow("State 2", order.state2.toString())
- DetailRow("LSP Balance", order.lspBalanceSat.formatToModernDisplay())
- DetailRow("Client Balance", order.clientBalanceSat.formatToModernDisplay())
- DetailRow("Total Fee", order.feeSat.formatToModernDisplay())
- DetailRow("Network Fee", order.networkFeeSat.formatToModernDisplay())
- DetailRow("Service Fee", order.serviceFeeSat.formatToModernDisplay())
- }
+ InfoCard(header = "Order Details") {
+ DetailRow("ID", order.id)
+ DetailRow("Onchain txs", order.payment?.onchain?.transactions?.size?.toString() ?: "0")
+ DetailRow("State", order.state.toString())
+ DetailRow("State 2", order.state2.toString())
+ DetailRow("LSP Balance", order.lspBalanceSat.formatToModernDisplay())
+ DetailRow("Client Balance", order.clientBalanceSat.formatToModernDisplay())
+ DetailRow("Total Fee", order.feeSat.formatToModernDisplay())
+ DetailRow("Network Fee", order.networkFeeSat.formatToModernDisplay())
+ DetailRow("Service Fee", order.serviceFeeSat.formatToModernDisplay())
}
}
-
- // Channel Settings
item {
- Card(
- colors = cardColors,
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp)
- ) {
- Column(modifier = Modifier.padding(16.dp)) {
- Text(text = "Channel Settings", style = MaterialTheme.typography.titleMedium)
- Spacer(modifier = Modifier.height(8.dp))
- DetailRow("Zero Conf", if (order.zeroConf) "Yes" else "No")
- DetailRow("Zero Reserve", if (order.zeroReserve) "Yes" else "No")
- order.clientNodeId?.let {
- DetailRow("Client Node ID", it)
- }
- DetailRow("Expiry Weeks", order.channelExpiryWeeks.toString())
- DetailRow("Channel Expires", order.channelExpiresAt)
- DetailRow("Order Expires", order.orderExpiresAt)
+ InfoCard(header = "Channel Settings") {
+ DetailRow("Zero Conf", if (order.zeroConf) "Yes" else "No")
+ DetailRow("Zero Reserve", if (order.zeroReserve) "Yes" else "No")
+ order.clientNodeId?.let {
+ DetailRow("Client Node ID", it)
}
+ DetailRow("Expiry Weeks", order.channelExpiryWeeks.toString())
+ DetailRow("Channel Expires", order.channelExpiresAt)
+ DetailRow("Order Expires", order.orderExpiresAt)
}
}
-
- // LSP Information
item {
- Card(
- colors = cardColors,
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp)
- ) {
- Column(modifier = Modifier.padding(16.dp)) {
- Text(text = "LSP Information", style = MaterialTheme.typography.titleMedium)
- Spacer(modifier = Modifier.height(8.dp))
- DetailRow("Alias", order.lspNode.alias)
- DetailRow("Node ID", order.lspNode.pubkey)
- order.lnurl?.let {
- DetailRow("LNURL", it)
- }
+ InfoCard(header = "LSP Info") {
+ DetailRow("Alias", order.lspNode?.alias.orEmpty())
+ DetailRow("Node ID", order.lspNode?.pubkey.orEmpty())
+ order.lnurl?.let {
+ DetailRow("LNURL", it)
}
}
}
-
- // Discount Section
order.couponCode?.let { couponCode ->
item {
- Card(
- colors = cardColors,
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp)
- ) {
- Column(modifier = Modifier.padding(16.dp)) {
- Text(
- text = "Discount",
- style = MaterialTheme.typography.titleMedium
- )
- Spacer(modifier = Modifier.height(8.dp))
- DetailRow("Coupon Code", couponCode)
- order.discount?.let { discount ->
- DetailRow("Discount Type", discount.code)
- DetailRow("Value", discount.absoluteSat.formatToModernDisplay())
- }
+ InfoCard(header = "Discount") {
+ DetailRow("Coupon Code", couponCode)
+ order.discount?.let { discount ->
+ DetailRow("Discount Type", discount.code)
+ DetailRow("Value", discount.absoluteSat.formatToModernDisplay())
}
}
}
}
-
- // Timestamps Section
item {
- Card(
- colors = cardColors,
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp)
- ) {
- Column(modifier = Modifier.padding(16.dp)) {
- Text(text = "Timestamps", style = MaterialTheme.typography.titleMedium)
- Spacer(modifier = Modifier.height(8.dp))
- DetailRow("Created", order.createdAt)
- DetailRow("Updated", order.updatedAt)
- }
+ InfoCard(header = "Timestamps") {
+ DetailRow("Created", order.createdAt)
+ DetailRow("Updated", order.updatedAt)
}
}
-
- // Open Channel Button
if (order.state2 == BtOrderState2.PAID) {
item {
- val blocktank = blocktankViewModel ?: return@item
PrimaryButton(
text = "Open Channel",
- onClick = {
- coroutineScope.launch {
- Logger.info("Opening channel for order ${order.id}")
- try {
- blocktank.openChannel(orderId = order.id)
- Logger.info("Channel opened for order ${order.id}")
- } catch (e: Throwable) {
- Logger.error("Error opening channel for order ${order.id}", e)
- }
- }
- },
+ onClick = onClickOpen,
modifier = Modifier.padding(horizontal = 16.dp)
)
}
@@ -344,364 +240,262 @@ private fun OrderDetailView(
@Composable
fun CJitDetailScreen(
cjitItem: Routes.CjitDetail,
- onBackClick: () -> Unit,
+ onBackClick: () -> Unit = {},
) {
val blocktank = blocktankViewModel ?: return
val cJitEntries by blocktank.cJitEntries.collectAsStateWithLifecycle()
val entry = cJitEntries.find { it.id == cjitItem.id } ?: return
- CJitDetailView(
+ CJitDetailContent(
entry = entry,
- onBackClick = onBackClick,
+ onBack = onBackClick,
)
}
@Composable
-private fun CJitDetailView(
+private fun CJitDetailContent(
entry: IcJitEntry,
- onBackClick: () -> Unit,
+ onBack: () -> Unit = {},
) {
Scaffold(
- topBar = {
- AppTopBar(
- titleText = "CJIT Entry Details",
- onBackClick = onBackClick,
- )
- }
+ topBar = { AppTopBar(titleText = "CJIT Details", onBackClick = onBack) }
) { padding ->
LazyColumn(
+ contentPadding = PaddingValues(horizontal = 16.dp),
modifier = Modifier.padding(padding)
) {
- // Entry Details Section
item {
- Card(
- colors = cardColors,
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp)
- ) {
- Column(modifier = Modifier.padding(16.dp)) {
- Text(text = "Entry Details", style = MaterialTheme.typography.titleMedium)
- Spacer(modifier = Modifier.height(8.dp))
- DetailRow(label = "ID", value = entry.id)
- DetailRow(label = "State", value = entry.state.toString())
- DetailRow(label = "Channel Size", value = entry.channelSizeSat.formatToModernDisplay())
- entry.channelOpenError?.let { error ->
- DetailRow(label = "Error", value = error, isError = true)
- }
+ InfoCard(header = "CJIT Details") {
+ DetailRow(label = "ID", value = entry.id)
+ DetailRow(label = "State", value = entry.state.toString())
+ DetailRow(label = "Channel Size", value = entry.channelSizeSat.formatToModernDisplay())
+ entry.channelOpenError?.let { error ->
+ DetailRow(label = "Error", value = error, isError = true)
}
}
}
-
- // Fees Section
item {
- Card(
- colors = cardColors,
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp)
- ) {
- Column(modifier = Modifier.padding(16.dp)) {
- Text(text = "Fees", style = MaterialTheme.typography.titleMedium)
- Spacer(modifier = Modifier.height(8.dp))
- DetailRow(label = "Total Fee", value = entry.feeSat.formatToModernDisplay())
- DetailRow(label = "Network Fee", value = entry.networkFeeSat.formatToModernDisplay())
- DetailRow(label = "Service Fee", value = entry.serviceFeeSat.formatToModernDisplay())
- }
+ InfoCard(header = "Fees") {
+ DetailRow(label = "Total Fee", value = entry.feeSat.formatToModernDisplay())
+ DetailRow(label = "Network Fee", value = entry.networkFeeSat.formatToModernDisplay())
+ DetailRow(label = "Service Fee", value = entry.serviceFeeSat.formatToModernDisplay())
}
}
-
- // Channel Settings Section
item {
- Card(
- colors = cardColors,
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp)
- ) {
- Column(modifier = Modifier.padding(16.dp)) {
- Text(text = "Channel Settings", style = MaterialTheme.typography.titleMedium)
- Spacer(modifier = Modifier.height(8.dp))
- DetailRow(label = "Node ID", value = entry.nodeId)
- DetailRow(label = "Expiry Weeks", value = "${entry.channelExpiryWeeks}")
- }
+ InfoCard(header = "Channel Settings") {
+ DetailRow(label = "Node ID", value = entry.nodeId)
+ DetailRow(label = "Expiry Weeks", value = "${entry.channelExpiryWeeks}")
}
}
-
- // LSP Information Section
item {
- Card(
- colors = cardColors,
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp)
- ) {
- Column(modifier = Modifier.padding(16.dp)) {
- Text(text = "LSP Information", style = MaterialTheme.typography.titleMedium)
- Spacer(modifier = Modifier.height(8.dp))
- DetailRow(label = "Alias", value = entry.lspNode.alias)
- DetailRow(label = "Node ID", value = entry.lspNode.pubkey)
- }
+ InfoCard(header = "LSP Information") {
+ DetailRow(label = "Alias", value = entry.lspNode.alias)
+ DetailRow(label = "Node ID", value = entry.lspNode.pubkey)
}
}
-
- // Discount Section
if (entry.couponCode.isNotEmpty()) {
item {
- Card(
- colors = cardColors,
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp)
- ) {
- Column(modifier = Modifier.padding(16.dp)) {
- Text(text = "Discount", style = MaterialTheme.typography.titleMedium)
- Spacer(modifier = Modifier.height(8.dp))
- DetailRow(label = "Coupon Code", value = entry.couponCode)
- entry.discount?.let { discount ->
- DetailRow(label = "Discount Type", value = discount.code)
- DetailRow(label = "Value", value = "${discount.absoluteSat}")
- }
+ InfoCard(header = "Discount") {
+ DetailRow(label = "Coupon Code", value = entry.couponCode)
+ entry.discount?.let { discount ->
+ DetailRow(label = "Discount Type", value = discount.code)
+ DetailRow(label = "Value", value = "${discount.absoluteSat}")
}
}
}
}
-
- // Timestamps Section
item {
- Card(
- colors = cardColors,
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp)
- ) {
- Column(modifier = Modifier.padding(16.dp)) {
- Text(text = "Timestamps", style = MaterialTheme.typography.titleMedium)
- Spacer(modifier = Modifier.height(8.dp))
- DetailRow(label = "Created", value = entry.createdAt)
- DetailRow(label = "Updated", value = entry.updatedAt)
- DetailRow(label = "Expires", value = entry.expiresAt)
- }
+ InfoCard(header = "Timestamps") {
+ DetailRow(label = "Created", value = entry.createdAt)
+ DetailRow(label = "Updated", value = entry.updatedAt)
+ DetailRow(label = "Expires", value = entry.expiresAt)
}
}
}
}
}
-// region Helpers
-
-private val cardColors: CardColors
- @Composable get() = CardDefaults.cardColors(containerColor = Colors.White10)
+private val cardColors: CardColors @Composable get() = CardDefaults.cardColors(containerColor = Colors.White10)
@Composable
-private fun CopyableText(text: String) {
- val clipboardManager = LocalClipboardManager.current
- var isPressed by remember { mutableStateOf(false) }
- val scale by animateFloatAsState(
- targetValue = if (isPressed) 0.95f else 1f,
- animationSpec = spring(
- dampingRatio = Spring.DampingRatioMediumBouncy,
- stiffness = Spring.StiffnessLow
- ),
- label = "scale"
- )
- val coroutineScope = rememberCoroutineScope()
-
- Text(
- text = text,
- fontSize = if (text.length > 20) 10.sp else 12.sp,
- fontFamily = FontFamily.Monospace,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- modifier = Modifier
- .scale(scale)
- .clickableAlpha {
- clipboardManager.setText(AnnotatedString(text))
- coroutineScope.launch {
- isPressed = true
- delay(100)
- isPressed = false
- }
+private fun InfoCard(
+ header: String,
+ modifier: Modifier = Modifier,
+ content: @Composable () -> Unit,
+) {
+ Column(modifier = modifier) {
+ SectionHeader(header, padding = PaddingValues.Zero)
+ Card(
+ colors = cardColors,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ content()
}
- )
+ }
+ }
}
@Composable
-private fun OrderRow(order: IBtOrder) {
- Column(
- verticalArrangement = Arrangement.spacedBy(8.dp),
+private fun OrderCard(model: IBtOrder, onClick: (String) -> Unit) {
+ Card(
+ colors = cardColors,
modifier = Modifier
.fillMaxWidth()
- .padding(8.dp)
+ .clickable { onClick(model.id) }
) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
+ Column(
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
) {
- CopyableText(text = order.id)
- Surface(
- color = Colors.White16,
- shape = MaterialTheme.shapes.small
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
) {
- Text(
- text = order.state2.toString(),
- style = MaterialTheme.typography.bodySmall,
- modifier = Modifier.padding(4.dp)
+ CaptionB(
+ text = model.id,
+ maxLines = 1,
+ overflow = TextOverflow.MiddleEllipsis,
+ modifier = Modifier.clickableAlpha(onClick = copyToClipboard(model.id))
)
+ Surface(color = Colors.White16, shape = AppShapes.small) {
+ Footnote(
+ text = model.state2.toString(),
+ color = Colors.White64,
+ maxLines = 1,
+ modifier = Modifier.padding(4.dp)
+ )
+ }
}
- }
- Row(
- horizontalArrangement = Arrangement.SpaceBetween,
- modifier = Modifier.fillMaxWidth(),
- ) {
- InfoCell(label = "LSP Balance", value = order.lspBalanceSat.formatToModernDisplay())
- InfoCell(
- label = "Client Balance",
- value = order.clientBalanceSat.formatToModernDisplay(),
- alignment = Alignment.End
- )
- }
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween,
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ InfoCell(label = "LSP Balance", value = model.lspBalanceSat.formatToModernDisplay())
+ InfoCell(
+ label = "Client Balance",
+ value = model.clientBalanceSat.formatToModernDisplay(),
+ alignment = Alignment.End
+ )
+ }
- Row(
- horizontalArrangement = Arrangement.SpaceBetween,
- modifier = Modifier.fillMaxWidth(),
- ) {
- InfoCell(label = "Fees", value = order.feeSat.formatToModernDisplay())
- InfoCell(
- label = "Expires",
- value = order.channelExpiresAt.take(10),
- alignment = Alignment.End
- )
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween,
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ InfoCell(label = "Fees", value = model.feeSat.formatToModernDisplay())
+ InfoCell(
+ label = "Expires",
+ value = model.channelExpiresAt.take(10),
+ alignment = Alignment.End
+ )
+ }
}
}
}
@Composable
-private fun CJitRow(entry: IcJitEntry) {
- Column(
+private fun CJitCard(model: IcJitEntry, onClick: (String) -> Unit) {
+ Card(
+ colors = cardColors,
modifier = Modifier
.fillMaxWidth()
- .padding(8.dp),
- verticalArrangement = Arrangement.spacedBy(8.dp)
+ .clickable { onClick(model.id) }
) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
+ Column(
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
) {
- CopyableText(text = entry.id)
- Surface(
- color = Colors.White16,
- shape = MaterialTheme.shapes.small
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
) {
- Text(
- text = entry.state.toString(),
- style = MaterialTheme.typography.bodySmall,
- modifier = Modifier.padding(4.dp)
+ CaptionB(
+ text = model.id,
+ maxLines = 1,
+ overflow = TextOverflow.MiddleEllipsis,
+ modifier = Modifier.clickableAlpha(onClick = copyToClipboard(model.id))
)
+ Surface(color = Colors.White16, shape = MaterialTheme.shapes.small) {
+ Footnote(
+ text = model.state.toString(),
+ color = Colors.White64,
+ maxLines = 1,
+ modifier = Modifier.padding(4.dp)
+ )
+ }
}
- }
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- InfoCell(label = "Channel Size", value = "${entry.channelSizeSat.formatToModernDisplay()} sats")
- InfoCell(
- label = "Fees",
- value = "${entry.feeSat.formatToModernDisplay()} sats",
- alignment = Alignment.End
- )
- }
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ InfoCell(label = "Channel Size", value = "${model.channelSizeSat.formatToModernDisplay()} sats")
+ InfoCell(
+ label = "Fees",
+ value = "${model.feeSat.formatToModernDisplay()} sats",
+ alignment = Alignment.End
+ )
+ }
- entry.channelOpenError?.let { error ->
- Text(
- text = error,
- fontSize = if (error.length > 50) 10.sp else 12.sp,
- color = MaterialTheme.colorScheme.error,
- maxLines = 2,
- overflow = TextOverflow.Ellipsis
- )
+ Column {
+ model.channelOpenError?.let { error ->
+ Caption(
+ text = error,
+ color = MaterialTheme.colorScheme.error,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+ VerticalSpacer(4.dp)
+ Footnote(text = "Expires: ${model.expiresAt.take(10)}", color = Colors.White32)
+ }
}
-
- Text(
- text = "Expires: ${entry.expiresAt.take(10)}",
- style = MaterialTheme.typography.bodySmall,
- color = Color.Gray
- )
}
}
@Composable
private fun InfoCell(label: String, value: String, alignment: Alignment.Horizontal = Alignment.Start) {
Column(horizontalAlignment = alignment) {
- Text(
- text = label,
- style = MaterialTheme.typography.bodySmall,
- color = Color.Gray
- )
- Text(
- text = value,
- style = MaterialTheme.typography.bodyMedium
- )
+ Caption13Up(text = label, color = Colors.White64)
+ VerticalSpacer(4.dp)
+ BodySSB(text = value)
}
}
@Composable
private fun DetailRow(label: String, value: String, isError: Boolean = false) {
- val clipboardManager = LocalClipboardManager.current
- var isPressed by remember { mutableStateOf(false) }
- val scale by animateFloatAsState(
- targetValue = if (isPressed) 0.95f else 1f,
- animationSpec = spring(
- dampingRatio = Spring.DampingRatioMediumBouncy,
- stiffness = Spring.StiffnessLow
- ),
- label = "scale"
- )
- val coroutineScope = rememberCoroutineScope()
-
- val fontSize = when {
- value.length > 40 -> 11.sp
- value.length > 30 -> 12.sp
- else -> 13.sp
- }
-
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
- .padding(vertical = 6.dp),
+ .padding(vertical = 6.dp)
) {
- BodyS(
+ Caption(
text = label,
color = Colors.White64,
+ overflow = TextOverflow.MiddleEllipsis,
+ maxLines = 1,
)
- Text(
+ HorizontalSpacer(16.dp)
+ Caption(
text = value,
- fontSize = fontSize,
color = if (isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.End,
- modifier = Modifier
- .scale(scale)
- .clickableAlpha {
- clipboardManager.setText(AnnotatedString(value))
- coroutineScope.launch {
- isPressed = true
- delay(100)
- isPressed = false
- }
- }
+ overflow = TextOverflow.MiddleEllipsis,
+ maxLines = 1,
+ modifier = Modifier.clickableAlpha(onClick = copyToClipboard(value))
)
}
}
-// endregion
-
-// region Preview
-
@Suppress("SpellCheckingInspection")
private val order = IBtOrder(
id = "order-3c564573-ec4b-b502-5e6fe930435f",
@@ -806,38 +600,42 @@ private val cjitEntry = IcJitEntry(
@Preview(showSystemUi = true)
@Composable
-private fun ChannelOrdersViewPreview() {
+private fun Preview() {
AppThemeSurface {
- ChannelOrdersView(
- orders = mutableListOf(order),
- cJitEntries = mutableListOf(cjitEntry),
- onBackClick = { },
- onOrderItemClick = { },
- onCjitItemClick = { },
+ Content(
+ orders = listOf(order),
+ cJitEntries = listOf(cjitEntry),
)
}
}
@Preview(showSystemUi = true)
@Composable
-private fun OrderDetailViewPreview() {
+private fun PreviewEmpty() {
AppThemeSurface {
- OrderDetailView(
+ Content(
+ orders = emptyList(),
+ cJitEntries = emptyList(),
+ )
+ }
+}
+
+@Preview(showSystemUi = true)
+@Composable
+private fun PreviewOrderDetail() {
+ AppThemeSurface {
+ OrderDetailContent(
order = order,
- onBackClick = { },
)
}
}
@Preview(showSystemUi = true)
@Composable
-private fun CJitDetailViewPreview() {
+private fun PreviewCJitDetail() {
AppThemeSurface {
- CJitDetailView(
+ CJitDetailContent(
entry = cjitEntry,
- onBackClick = { },
)
}
}
-
-// endregion
diff --git a/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsSettings.kt b/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsSettings.kt
index fa5b82d101..2ebd7aa59d 100644
--- a/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsSettings.kt
+++ b/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsSettings.kt
@@ -94,6 +94,7 @@ private fun Content(
)
if (hasPermission) {
+ @Suppress("MaxLineLength") // TODO transifex
BodyM(
text = "Background payments are enabled. You can receive funds even when the app is closed (if your device is connected to the internet).",
color = Colors.White64,
diff --git a/app/src/main/java/to/bitkit/ui/settings/general/GeneralSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/general/GeneralSettingsScreen.kt
index dda09b0731..f4a002cd49 100644
--- a/app/src/main/java/to/bitkit/ui/settings/general/GeneralSettingsScreen.kt
+++ b/app/src/main/java/to/bitkit/ui/settings/general/GeneralSettingsScreen.kt
@@ -87,8 +87,8 @@ private fun GeneralSettingsContent(
primaryDisplay: PrimaryDisplay,
defaultTransactionSpeed: TransactionSpeed,
selectedLanguage: String,
- showTagsButton: Boolean = false,
notificationsGranted: Boolean,
+ showTagsButton: Boolean = false,
onBackClick: () -> Unit = {},
onCloseClick: () -> Unit = {},
onLocalCurrencyClick: () -> Unit = {},
@@ -175,9 +175,8 @@ private fun Preview() {
selectedCurrency = "USD",
primaryDisplay = PrimaryDisplay.BITCOIN,
defaultTransactionSpeed = TransactionSpeed.Medium,
- showTagsButton = true,
selectedLanguage = Language.SYSTEM_DEFAULT.displayName,
- notificationsGranted = true
+ notificationsGranted = true,
)
}
}
diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt
index cde26d5a21..47b01bd4cd 100644
--- a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt
+++ b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt
@@ -530,8 +530,8 @@ private fun getChannelStatus(
blocktankOrder?.let { order ->
when {
order.state2 == BtOrderState2.EXPIRED ||
- order.payment.state2 == BtPaymentState2.CANCELED ||
- order.payment.state2 == BtPaymentState2.REFUNDED -> {
+ order.payment?.state2 == BtPaymentState2.CANCELED ||
+ order.payment?.state2 == BtPaymentState2.REFUNDED -> {
return ChannelStatusUi.CLOSED
}
diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt
index dde193e759..97b2caaaa1 100644
--- a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt
+++ b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt
@@ -229,7 +229,7 @@ class LightningConnectionsViewModel @Inject constructor(
createChannelDetails().copy(
channelId = order.id,
- counterpartyNodeId = order.lspNode.pubkey,
+ counterpartyNodeId = order.lspNode?.pubkey.orEmpty(),
fundingTxo = order.channel?.fundingTx?.let { OutPoint(txid = it.id, vout = it.vout.toUInt()) },
channelValueSats = order.clientBalanceSat + order.lspBalanceSat,
outboundCapacityMsat = order.clientBalanceSat * 1000u,
@@ -246,7 +246,7 @@ class LightningConnectionsViewModel @Inject constructor(
createChannelDetails().copy(
channelId = order.id,
- counterpartyNodeId = order.lspNode.pubkey,
+ counterpartyNodeId = order.lspNode?.pubkey.orEmpty(),
fundingTxo = order.channel?.fundingTx?.let { OutPoint(txid = it.id, vout = it.vout.toUInt()) },
channelValueSats = order.clientBalanceSat + order.lspBalanceSat,
outboundCapacityMsat = order.clientBalanceSat * 1000u,
diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/components/ChannelStatusView.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/components/ChannelStatusView.kt
index 6b15420b47..900bac9266 100644
--- a/app/src/main/java/to/bitkit/ui/settings/lightning/components/ChannelStatusView.kt
+++ b/app/src/main/java/to/bitkit/ui/settings/lightning/components/ChannelStatusView.kt
@@ -59,6 +59,7 @@ fun ChannelStatusView(
}
}
+@Suppress("CyclomaticComplexMethod")
@Composable
private fun getStatusInfo(
channel: ChannelUi,
@@ -122,7 +123,7 @@ private fun getStatusInfo(
)
}
- when (order.payment.state2) {
+ when (order.payment?.state2) {
BtPaymentState2.CANCELED -> {
return StatusInfo(
iconRes = R.drawable.ic_x,
@@ -172,6 +173,8 @@ private fun getStatusInfo(
statusColor = Colors.Purple
)
}
+
+ null -> Unit
}
}
@@ -279,7 +282,7 @@ private fun PreviewPaymentCanceled() {
details = createChannelDetails(),
),
blocktankOrder = mockOrder().copy(
- payment = mockOrder().payment.copy(
+ payment = mockOrder().payment?.copy(
state2 = BtPaymentState2.CANCELED,
),
),
@@ -297,7 +300,7 @@ private fun PreviewRefundAvailable() {
details = createChannelDetails(),
),
blocktankOrder = mockOrder().copy(
- payment = mockOrder().payment.copy(
+ payment = mockOrder().payment?.copy(
state2 = BtPaymentState2.REFUND_AVAILABLE,
),
),
@@ -315,7 +318,7 @@ private fun PreviewRefunded() {
details = createChannelDetails(),
),
blocktankOrder = mockOrder().copy(
- payment = mockOrder().payment.copy(
+ payment = mockOrder().payment?.copy(
state2 = BtPaymentState2.REFUNDED,
),
),
@@ -347,7 +350,7 @@ private fun PreviewPaymentPaid() {
details = createChannelDetails(),
),
blocktankOrder = mockOrder().copy(
- payment = mockOrder().payment.copy(
+ payment = mockOrder().payment?.copy(
state2 = BtPaymentState2.PAID,
),
),
diff --git a/app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt
index 8373d4cd61..bc9bd5cee2 100644
--- a/app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt
+++ b/app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt
@@ -39,7 +39,7 @@ fun BackupSheet(
) {
val navController = rememberNavController()
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
- val onDismissSheet by rememberUpdatedState { onDismiss }
+ val currentOnDismiss by rememberUpdatedState(onDismiss)
LaunchedEffect(Unit) {
viewModel.loadMnemonicData()
@@ -70,7 +70,7 @@ fun BackupSheet(
)
BackupContract.SideEffect.NavigateToMetadata -> navController.navigate(BackupRoute.Metadata)
- BackupContract.SideEffect.DismissSheet -> onDismissSheet()
+ BackupContract.SideEffect.DismissSheet -> currentOnDismiss()
}
}
}
@@ -88,7 +88,7 @@ fun BackupSheet(
composableWithDefaultTransitions {
BackupIntroScreen(
hasFunds = LocalBalances.current.totalSats > 0u,
- onClose = onDismissSheet(),
+ onClose = currentOnDismiss,
onConfirm = { navController.navigate(BackupRoute.ShowMnemonic) },
)
}
diff --git a/app/src/main/java/to/bitkit/ui/utils/Clipboard.kt b/app/src/main/java/to/bitkit/ui/utils/Clipboard.kt
index fea2a23968..440f552240 100644
--- a/app/src/main/java/to/bitkit/ui/utils/Clipboard.kt
+++ b/app/src/main/java/to/bitkit/ui/utils/Clipboard.kt
@@ -15,7 +15,7 @@ import to.bitkit.R
fun copyToClipboard(
text: String,
label: String = stringResource(R.string.app_name),
- block: (() -> Unit)? = null,
+ callback: ((String) -> Unit)? = null,
): () -> Unit {
val clipboard = LocalClipboard.current
val haptic = LocalHapticFeedback.current
@@ -26,7 +26,7 @@ fun copyToClipboard(
val clipData = ClipData.newPlainText(label, text)
clipboard.setClipEntry(ClipEntry(clipData))
haptic.performHapticFeedback(HapticFeedbackType.Confirm)
- block?.invoke()
+ callback?.invoke(text)
}
}
}
diff --git a/app/src/main/java/to/bitkit/ui/utils/RequestNotificationPermissions.kt b/app/src/main/java/to/bitkit/ui/utils/RequestNotificationPermissions.kt
index 21c815ec4c..490ff9d03d 100644
--- a/app/src/main/java/to/bitkit/ui/utils/RequestNotificationPermissions.kt
+++ b/app/src/main/java/to/bitkit/ui/utils/RequestNotificationPermissions.kt
@@ -10,10 +10,12 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.compose.LocalLifecycleOwner
@Composable
fun RequestNotificationPermissions(
@@ -21,7 +23,8 @@ fun RequestNotificationPermissions(
showPermissionDialog: Boolean = true,
) {
val context = LocalContext.current
- val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
+ val lifecycleOwner = LocalLifecycleOwner.current
+ val currentOnPermissionChange by rememberUpdatedState(onPermissionChange)
// Check if permission is required (Android 13+)
val requiresPermission = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
@@ -38,6 +41,17 @@ fun RequestNotificationPermissions(
onPermissionChange(granted)
}
+ // Request permission on first composition if needed
+ LaunchedEffect(Unit) {
+ val currentPermissionState = NotificationUtils.areNotificationsEnabled(context)
+ isGranted = currentPermissionState
+ currentOnPermissionChange(currentPermissionState)
+
+ if (!currentPermissionState && requiresPermission && showPermissionDialog) {
+ launcher.launch(Manifest.permission.POST_NOTIFICATIONS)
+ }
+ }
+
// Monitor lifecycle to check permission when returning from settings
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
@@ -45,7 +59,7 @@ fun RequestNotificationPermissions(
val currentPermissionState = NotificationUtils.areNotificationsEnabled(context)
if (currentPermissionState != isGranted) {
isGranted = currentPermissionState
- onPermissionChange(currentPermissionState)
+ currentOnPermissionChange(currentPermissionState)
}
}
}
@@ -56,15 +70,4 @@ fun RequestNotificationPermissions(
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
-
- // Request permission on first composition if needed
- LaunchedEffect(Unit) {
- val currentPermissionState = NotificationUtils.areNotificationsEnabled(context)
- isGranted = currentPermissionState
- onPermissionChange(currentPermissionState)
-
- if (!currentPermissionState && requiresPermission && showPermissionDialog) {
- launcher.launch(Manifest.permission.POST_NOTIFICATIONS)
- }
- }
}
diff --git a/app/src/main/java/to/bitkit/usecases/DeriveBalanceStateUseCase.kt b/app/src/main/java/to/bitkit/usecases/DeriveBalanceStateUseCase.kt
index 5d4a7270d5..a239c3ca70 100644
--- a/app/src/main/java/to/bitkit/usecases/DeriveBalanceStateUseCase.kt
+++ b/app/src/main/java/to/bitkit/usecases/DeriveBalanceStateUseCase.kt
@@ -1,6 +1,7 @@
package to.bitkit.usecases
import kotlinx.coroutines.flow.first
+import org.lightningdevkit.ldknode.BalanceDetails
import org.lightningdevkit.ldknode.ChannelDetails
import to.bitkit.data.SettingsStore
import to.bitkit.data.entities.TransferEntity
@@ -8,7 +9,6 @@ import to.bitkit.ext.amountSats
import to.bitkit.ext.channelId
import to.bitkit.ext.minusOrZero
import to.bitkit.ext.totalNextOutboundHtlcLimitSats
-import to.bitkit.models.BalanceDetails
import to.bitkit.models.BalanceState
import to.bitkit.repositories.LightningRepo
import to.bitkit.repositories.TransferRepo
diff --git a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt
index cef8ab5dd0..6d88e87c4a 100644
--- a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt
+++ b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt
@@ -206,7 +206,7 @@ class TransferViewModel @Inject constructor(
viewModelScope.launch {
lightningRepo
.sendOnChain(
- address = order.payment.onchain.address,
+ address = order.payment?.onchain?.address.orEmpty(),
sats = order.feeSat,
speed = speed,
isTransfer = true,
diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt
index 0a6cff1ca5..d72543a46e 100644
--- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt
+++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt
@@ -19,9 +19,9 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.lightningdevkit.ldknode.ChannelDetails
import org.lightningdevkit.ldknode.NodeStatus
+import org.lightningdevkit.ldknode.PeerDetails
import to.bitkit.data.SettingsStore
import to.bitkit.di.BgDispatcher
-import to.bitkit.models.LnPeer
import to.bitkit.models.NodeLifecycleState
import to.bitkit.models.Toast
import to.bitkit.repositories.BackupRepo
@@ -181,7 +181,7 @@ class WalletViewModel @Inject constructor(
}
}
- fun disconnectPeer(peer: LnPeer) {
+ fun disconnectPeer(peer: PeerDetails) {
viewModelScope.launch {
lightningRepo.disconnectPeer(peer)
.onSuccess {
@@ -309,7 +309,7 @@ data class MainUiState(
val bip21: String = "",
val nodeStatus: NodeStatus? = null,
val nodeLifecycleState: NodeLifecycleState = NodeLifecycleState.Stopped,
- val peers: List = emptyList(),
+ val peers: List = emptyList(),
val channels: List = emptyList(),
val isRefreshing: Boolean = false,
val receiveOnSpendingBalance: Boolean = true,
diff --git a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt
index 092882baea..4d31d2e002 100644
--- a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt
+++ b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt
@@ -9,6 +9,7 @@ import org.junit.Test
import org.lightningdevkit.ldknode.ChannelDetails
import org.lightningdevkit.ldknode.NodeStatus
import org.lightningdevkit.ldknode.PaymentDetails
+import org.lightningdevkit.ldknode.PeerDetails
import org.lightningdevkit.ldknode.SpendableUtxo
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
@@ -28,9 +29,9 @@ import to.bitkit.data.SettingsData
import to.bitkit.data.SettingsStore
import to.bitkit.data.keychain.Keychain
import to.bitkit.ext.createChannelDetails
+import to.bitkit.ext.from
import to.bitkit.models.BalanceState
import to.bitkit.models.CoinSelectionPreference
-import to.bitkit.models.LnPeer
import to.bitkit.models.NodeLifecycleState
import to.bitkit.models.OpenChannelResult
import to.bitkit.models.TransactionMetadata
@@ -200,7 +201,7 @@ class LightningRepoTest : BaseUnitTest() {
@Test
fun `openChannel should fail when node is not running`() = test {
- val testPeer = LnPeer("nodeId", "host", "9735")
+ val testPeer = PeerDetails.from("nodeId", "host", "9735")
val result = sut.openChannel(testPeer, 100000uL)
assertTrue(result.isFailure)
}
@@ -208,7 +209,7 @@ class LightningRepoTest : BaseUnitTest() {
@Test
fun `openChannel should succeed when node is running`() = test {
startNodeForTesting()
- val peer = LnPeer("nodeId", "host", "9735")
+ val peer = PeerDetails.from("nodeId", "host", "9735")
val userChannelId = "testChannelId"
val channelAmountSats = 100_000uL
whenever(lightningService.openChannel(peer, channelAmountSats, null, null))
@@ -279,7 +280,7 @@ class LightningRepoTest : BaseUnitTest() {
startNodeForTesting()
val testNodeId = "test_node_id"
val testStatus = mock()
- val testPeers = listOf(mock())
+ val testPeers = listOf(mock())
val testChannels = listOf(mock())
whenever(lightningService.nodeId).thenReturn(testNodeId)
@@ -344,7 +345,7 @@ class LightningRepoTest : BaseUnitTest() {
@Test
fun `disconnectPeer should fail when node is not running`() = test {
- val testPeer = LnPeer("nodeId", "host", "9735")
+ val testPeer = PeerDetails.from("nodeId", "host", "9735")
val result = sut.disconnectPeer(testPeer)
assertTrue(result.isFailure)
}
@@ -352,7 +353,7 @@ class LightningRepoTest : BaseUnitTest() {
@Test
fun `disconnectPeer should succeed when node is running`() = test {
startNodeForTesting()
- val testPeer = LnPeer("nodeId", "host", "9735")
+ val testPeer = PeerDetails.from("nodeId", "host", "9735")
whenever(lightningService.disconnectPeer(any())).thenReturn(Unit)
val result = sut.disconnectPeer(testPeer)
diff --git a/app/src/test/java/to/bitkit/repositories/TransferRepoTest.kt b/app/src/test/java/to/bitkit/repositories/TransferRepoTest.kt
index 4d3c4fbd0a..1a945c1c68 100644
--- a/app/src/test/java/to/bitkit/repositories/TransferRepoTest.kt
+++ b/app/src/test/java/to/bitkit/repositories/TransferRepoTest.kt
@@ -9,7 +9,9 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.datetime.Clock
import org.junit.Before
import org.junit.Test
+import org.lightningdevkit.ldknode.BalanceDetails
import org.lightningdevkit.ldknode.ChannelDetails
+import org.lightningdevkit.ldknode.LightningBalance
import org.lightningdevkit.ldknode.OutPoint
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
@@ -22,8 +24,6 @@ import org.mockito.kotlin.wheneverBlocking
import to.bitkit.data.dao.TransferDao
import to.bitkit.data.entities.TransferEntity
import to.bitkit.ext.createChannelDetails
-import to.bitkit.models.BalanceDetails
-import to.bitkit.models.LightningBalance
import to.bitkit.models.TransferType
import to.bitkit.test.BaseUnitTest
import kotlin.test.assertEquals
diff --git a/app/src/test/java/to/bitkit/ui/WalletViewModelTest.kt b/app/src/test/java/to/bitkit/ui/WalletViewModelTest.kt
index 790bf24d80..2a92cc8644 100644
--- a/app/src/test/java/to/bitkit/ui/WalletViewModelTest.kt
+++ b/app/src/test/java/to/bitkit/ui/WalletViewModelTest.kt
@@ -6,6 +6,7 @@ import kotlinx.coroutines.test.advanceUntilIdle
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
+import org.lightningdevkit.ldknode.PeerDetails
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.mock
@@ -13,7 +14,7 @@ import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import to.bitkit.data.SettingsStore
-import to.bitkit.models.LnPeer
+import to.bitkit.ext.from
import to.bitkit.repositories.BackupRepo
import to.bitkit.repositories.BlocktankRepo
import to.bitkit.repositories.LightningRepo
@@ -82,7 +83,7 @@ class WalletViewModelTest : BaseUnitTest() {
@Test
fun `disconnectPeer should call lightningRepo disconnectPeer and send success toast`() = test {
- val testPeer = LnPeer("nodeId", "host", "9735")
+ val testPeer = PeerDetails.from("nodeId", "host", "9735")
whenever(lightningRepo.disconnectPeer(testPeer)).thenReturn(Result.success(Unit))
sut.disconnectPeer(testPeer)
@@ -93,7 +94,7 @@ class WalletViewModelTest : BaseUnitTest() {
@Test
fun `disconnectPeer should call lightningRepo disconnectPeer and send failure toast`() = test {
- val testPeer = LnPeer("nodeId", "host", "9735")
+ val testPeer = PeerDetails.from("nodeId", "host", "9735")
val testError = Exception("Test error")
whenever(lightningRepo.disconnectPeer(testPeer)).thenReturn(Result.failure(testError))
diff --git a/app/src/test/java/to/bitkit/usecases/DeriveBalanceStateUseCaseTest.kt b/app/src/test/java/to/bitkit/usecases/DeriveBalanceStateUseCaseTest.kt
index e1525d61fa..b9a7e48c30 100644
--- a/app/src/test/java/to/bitkit/usecases/DeriveBalanceStateUseCaseTest.kt
+++ b/app/src/test/java/to/bitkit/usecases/DeriveBalanceStateUseCaseTest.kt
@@ -4,8 +4,10 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import org.junit.Before
import org.junit.Test
+import org.lightningdevkit.ldknode.BalanceDetails
import org.lightningdevkit.ldknode.BalanceSource
import org.lightningdevkit.ldknode.ChannelDetails
+import org.lightningdevkit.ldknode.LightningBalance
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.doReturn
@@ -15,8 +17,6 @@ import org.mockito.kotlin.wheneverBlocking
import to.bitkit.data.SettingsData
import to.bitkit.data.SettingsStore
import to.bitkit.data.entities.TransferEntity
-import to.bitkit.models.BalanceDetails
-import to.bitkit.models.LightningBalance
import to.bitkit.models.TransferType
import to.bitkit.repositories.LightningRepo
import to.bitkit.repositories.LightningState
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 274e2d6e47..c2bfef852b 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -44,7 +44,7 @@ activity-compose = { module = "androidx.activity:activity-compose", version.ref
appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version.ref = "barcodeScanning" }
biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" }
-bitkitcore = { module = "com.synonym:bitkit-core-android", version = "0.1.10" }
+bitkitcore = { module = "com.synonym:bitkit-core-android", version = "0.1.18" }
bouncycastle-provider-jdk = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bouncyCastle" }
camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camera" }
camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camera" }
@@ -82,7 +82,8 @@ ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "k
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
#ldk-node-android = { module = "org.lightningdevkit:ldk-node-android", version = "0.6.2" } # upstream
-ldk-node-android = { module = "com.github.synonymdev:ldk-node", version = "v0.6.2-rc.3" } # fork
+#ldk-node-android = { module = "org.lightningdevkit:ldk-node-android", version = "0.6.2-rc.4" } # local
+ldk-node-android = { module = "com.github.synonymdev:ldk-node", version = "v0.6.2-rc.4" } # fork
lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycle" }
lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" }
lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }