Skip to content

Commit 3b286a2

Browse files
committed
Merge branch 'master' into feat/widgets-headlines
2 parents a488d26 + 265ceb2 commit 3b286a2

File tree

8 files changed

+231
-6
lines changed

8 files changed

+231
-6
lines changed

app/src/main/java/to/bitkit/ext/Context.kt

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ package to.bitkit.ext
44

55
import android.app.Activity
66
import android.app.NotificationManager
7+
import android.content.ClipData
8+
import android.content.ClipboardManager
79
import android.content.Context
810
import android.content.Context.NOTIFICATION_SERVICE
911
import android.content.ContextWrapper
@@ -24,13 +26,14 @@ val Context.notificationManager: NotificationManager
2426
val Context.notificationManagerCompat: NotificationManagerCompat
2527
get() = NotificationManagerCompat.from(this)
2628

29+
val Context.clipboardManager: ClipboardManager
30+
get() = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
31+
2732
// Permissions
2833

2934
fun Context.requiresPermission(permission: String): Boolean =
3035
ContextCompat.checkSelfPermission(this, permission) != PERMISSION_GRANTED
3136

32-
// In-App Notifications
33-
3437
// File System
3538
fun Context.readAsset(path: String) = assets.open(path).use(InputStream::readBytes)
3639

@@ -58,3 +61,14 @@ fun Context.findActivity(): Activity? =
5861
is ContextWrapper -> baseContext.findActivity()
5962
else -> null
6063
}
64+
65+
// Clipboard
66+
fun Context.setClipboardText(label: String = "", text: String) {
67+
this.clipboardManager.setPrimaryClip(
68+
ClipData.newPlainText(label, text)
69+
)
70+
}
71+
72+
fun Context.getClipboardText(): String? {
73+
return this.clipboardManager.primaryClip?.getItemAt(0)?.text?.toString()
74+
}

app/src/main/java/to/bitkit/ui/ContentView.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import androidx.lifecycle.LifecycleEventObserver
1717
import androidx.lifecycle.compose.LocalLifecycleOwner
1818
import androidx.lifecycle.compose.collectAsStateWithLifecycle
1919
import androidx.navigation.NavController
20+
import androidx.navigation.NavDestination.Companion.hasRoute
2021
import androidx.navigation.NavGraphBuilder
2122
import androidx.navigation.NavHostController
2223
import androidx.navigation.NavOptions
@@ -29,7 +30,6 @@ import androidx.navigation.toRoute
2930
import kotlinx.coroutines.delay
3031
import kotlinx.coroutines.launch
3132
import kotlinx.serialization.Serializable
32-
import to.bitkit.currentActivity
3333
import to.bitkit.models.NewTransactionSheetDetails
3434
import to.bitkit.models.NodeLifecycleState
3535
import to.bitkit.ui.components.AuthCheckScreen
@@ -100,6 +100,7 @@ import to.bitkit.ui.settings.support.ReportIssueScreen
100100
import to.bitkit.ui.settings.support.SupportScreen
101101
import to.bitkit.ui.settings.transactionSpeed.CustomFeeSettingsScreen
102102
import to.bitkit.ui.settings.transactionSpeed.TransactionSpeedSettingsScreen
103+
import to.bitkit.ui.utils.AutoReadClipboardHandler
103104
import to.bitkit.ui.utils.composableWithDefaultTransitions
104105
import to.bitkit.ui.utils.screenSlideIn
105106
import to.bitkit.ui.utils.screenSlideOut
@@ -171,6 +172,15 @@ fun ContentView(
171172
appViewModel.mainScreenEffect.collect {
172173
when (it) {
173174
is MainScreenEffect.NavigateActivityDetail -> navController.navigate(Routes.ActivityDetail(it.activityId))
175+
is MainScreenEffect.ProcessClipboardAutoRead -> {
176+
val isOnHome = navController.currentDestination?.hasRoute<Routes.Home>() == true
177+
if (!isOnHome) {
178+
navController.navigateToHome()
179+
delay(100) // Small delay to ensure navigation completes
180+
}
181+
appViewModel.onScanSuccess(it.data)
182+
}
183+
174184
else -> Unit
175185
}
176186
}
@@ -250,6 +260,8 @@ fun ContentView(
250260
LocalBalances provides balance,
251261
LocalCurrencies provides currencies,
252262
) {
263+
AutoReadClipboardHandler()
264+
253265
NavHost(navController, startDestination = Routes.Home) {
254266
home(walletViewModel, appViewModel, activityListViewModel, settingsViewModel, navController)
255267
settings(navController, settingsViewModel)

app/src/main/java/to/bitkit/ui/MainActivity.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ class MainActivity : FragmentActivity() {
189189
}
190190
}
191191
} else {
192+
val isAuthenticated by appViewModel.isAuthenticated.collectAsStateWithLifecycle()
193+
192194
InactivityTracker(appViewModel, settingsViewModel) {
193195
ContentView(
194196
appViewModel = appViewModel,
@@ -201,7 +203,6 @@ class MainActivity : FragmentActivity() {
201203
)
202204
}
203205

204-
val isAuthenticated by appViewModel.isAuthenticated.collectAsStateWithLifecycle()
205206
AnimatedVisibility(
206207
visible = !isAuthenticated,
207208
enter = fadeIn(),
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package to.bitkit.ui.components
2+
3+
import androidx.compose.foundation.layout.Arrangement
4+
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.foundation.layout.Row
6+
import androidx.compose.foundation.layout.Spacer
7+
import androidx.compose.foundation.layout.fillMaxWidth
8+
import androidx.compose.foundation.layout.height
9+
import androidx.compose.foundation.layout.padding
10+
import androidx.compose.material3.Card
11+
import androidx.compose.material3.CardDefaults
12+
import androidx.compose.runtime.Composable
13+
import androidx.compose.ui.Modifier
14+
import androidx.compose.ui.res.stringResource
15+
import androidx.compose.ui.tooling.preview.Preview
16+
import androidx.compose.ui.unit.dp
17+
import androidx.compose.ui.window.Dialog
18+
import androidx.compose.ui.window.DialogProperties
19+
import to.bitkit.R
20+
import to.bitkit.ui.theme.AppThemeSurface
21+
import to.bitkit.ui.theme.Colors
22+
import to.bitkit.ui.theme.Shapes
23+
24+
@Composable
25+
fun ClipboardDataDialog(
26+
onConfirm: () -> Unit,
27+
onDismiss: () -> Unit,
28+
modifier: Modifier = Modifier,
29+
) {
30+
Dialog(
31+
onDismissRequest = onDismiss,
32+
properties = DialogProperties(
33+
dismissOnClickOutside = false,
34+
)
35+
) {
36+
Card(
37+
modifier = modifier.fillMaxWidth(),
38+
shape = Shapes.medium,
39+
colors = CardDefaults.cardColors(containerColor = Colors.Gray5),
40+
) {
41+
Column(
42+
modifier = Modifier.padding(24.dp),
43+
) {
44+
Title(
45+
text = stringResource(R.string.other__clipboard_redirect_title),
46+
color = Colors.White,
47+
)
48+
49+
Spacer(modifier = Modifier.height(16.dp))
50+
51+
BodyS(
52+
text = stringResource(R.string.other__clipboard_redirect_msg),
53+
color = Colors.White64,
54+
)
55+
56+
Spacer(modifier = Modifier.height(24.dp))
57+
58+
Row(
59+
modifier = Modifier.fillMaxWidth(),
60+
horizontalArrangement = Arrangement.spacedBy(8.dp)
61+
) {
62+
TertiaryButton(
63+
text = stringResource(R.string.common__dialog_cancel),
64+
size = ButtonSize.Small,
65+
onClick = onDismiss,
66+
modifier = Modifier.weight(1f)
67+
)
68+
PrimaryButton(
69+
text = stringResource(R.string.common__ok),
70+
size = ButtonSize.Small,
71+
onClick = onConfirm,
72+
modifier = Modifier.weight(1f)
73+
)
74+
}
75+
}
76+
}
77+
}
78+
}
79+
80+
@Preview
81+
@Composable
82+
private fun Preview() {
83+
AppThemeSurface {
84+
ClipboardDataDialog(
85+
onConfirm = {},
86+
onDismiss = {},
87+
)
88+
}
89+
}

app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import com.google.mlkit.vision.common.InputImage
6161
import kotlinx.coroutines.Dispatchers
6262
import kotlinx.coroutines.withContext
6363
import to.bitkit.R
64+
import to.bitkit.ext.clipboardManager
6465
import to.bitkit.ui.appViewModel
6566
import to.bitkit.ui.components.PrimaryButton
6667
import to.bitkit.ui.scaffold.AppTopBar
@@ -181,7 +182,7 @@ fun QrScanningScreen(
181182
}
182183
},
183184
onPasteFromClipboard = {
184-
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
185+
val clipboard = context.clipboardManager
185186
if (clipboard.hasPrimaryClip()) {
186187
val clipData: ClipData = clipboard.primaryClip ?: return@Content
187188
if (clipData.itemCount > 0) {

app/src/main/java/to/bitkit/ui/screens/wallets/send/SendOptionsView.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import androidx.compose.runtime.rememberCoroutineScope
1818
import androidx.compose.ui.Modifier
1919
import androidx.compose.ui.layout.ContentScale
2020
import androidx.compose.ui.platform.LocalClipboardManager
21+
import androidx.compose.ui.platform.LocalContext
2122
import androidx.compose.ui.res.painterResource
2223
import androidx.compose.ui.res.stringResource
2324
import androidx.compose.ui.tooling.preview.Preview
@@ -29,6 +30,7 @@ import androidx.navigation.toRoute
2930
import kotlinx.coroutines.launch
3031
import kotlinx.serialization.Serializable
3132
import to.bitkit.R
33+
import to.bitkit.ext.setClipboardText
3234
import to.bitkit.models.NewTransactionSheetDetails
3335
import to.bitkit.ui.appViewModel
3436
import to.bitkit.ui.components.Caption13Up
@@ -51,6 +53,7 @@ fun SendOptionsView(
5153
startDestination: SendRoute = SendRoute.Options,
5254
onComplete: (NewTransactionSheetDetails?) -> Unit,
5355
) {
56+
val context = LocalContext.current
5457
Column(
5558
modifier = Modifier
5659
.fillMaxWidth()
@@ -65,7 +68,10 @@ fun SendOptionsView(
6568
is SendEffect.NavigateToAddress -> navController.navigate(SendRoute.Address)
6669
is SendEffect.NavigateToScan -> navController.navigate(SendRoute.QrScanner)
6770
is SendEffect.NavigateToReview -> navController.navigate(SendRoute.ReviewAndSend)
68-
is SendEffect.PaymentSuccess -> onComplete(it.sheet)
71+
is SendEffect.PaymentSuccess -> {
72+
onComplete(it.sheet)
73+
context.setClipboardText(text = "")
74+
}
6975
is SendEffect.NavigateToQuickPay -> {
7076
navController.navigate(SendRoute.QuickPay(it.invoice, it.amount))
7177
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package to.bitkit.ui.utils
2+
3+
import android.content.Context
4+
import androidx.compose.runtime.Composable
5+
import androidx.compose.runtime.DisposableEffect
6+
import androidx.compose.runtime.LaunchedEffect
7+
import androidx.compose.runtime.collectAsState
8+
import androidx.compose.runtime.getValue
9+
import androidx.compose.runtime.mutableStateOf
10+
import androidx.compose.runtime.remember
11+
import androidx.compose.runtime.rememberCoroutineScope
12+
import androidx.compose.runtime.setValue
13+
import androidx.compose.ui.platform.LocalContext
14+
import androidx.lifecycle.Lifecycle
15+
import androidx.lifecycle.LifecycleEventObserver
16+
import androidx.lifecycle.compose.LocalLifecycleOwner
17+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
18+
import kotlinx.coroutines.delay
19+
import kotlinx.coroutines.launch
20+
import to.bitkit.ext.getClipboardText
21+
import to.bitkit.ui.appViewModel
22+
import to.bitkit.ui.components.ClipboardDataDialog
23+
import to.bitkit.ui.settingsViewModel
24+
import uniffi.bitkitcore.decode
25+
26+
@Composable
27+
fun AutoReadClipboardHandler() {
28+
val appViewModel = appViewModel ?: return
29+
val settings = settingsViewModel ?: return
30+
31+
val context = LocalContext.current
32+
val lifecycleOwner = LocalLifecycleOwner.current
33+
val scope = rememberCoroutineScope()
34+
35+
val isAuthenticated by appViewModel.isAuthenticated.collectAsStateWithLifecycle()
36+
val isAutoReadClipboardEnabled by settings.enableAutoReadClipboard.collectAsState()
37+
38+
var showClipboardDialog by remember { mutableStateOf(false) }
39+
var hasCheckedOnStartup by remember { mutableStateOf(false) }
40+
41+
// Check clipboard on app startup - only after authentication
42+
LaunchedEffect(isAuthenticated, isAutoReadClipboardEnabled) {
43+
if (isAuthenticated && isAutoReadClipboardEnabled && !hasCheckedOnStartup) {
44+
showClipboardDialog = context.hasScanDataInClipboard()
45+
hasCheckedOnStartup = true
46+
}
47+
}
48+
49+
LaunchedEffect(isAutoReadClipboardEnabled) {
50+
if (!isAutoReadClipboardEnabled) {
51+
showClipboardDialog = false
52+
}
53+
}
54+
55+
// Check clipboard on app fg
56+
DisposableEffect(lifecycleOwner, isAuthenticated) {
57+
val observer = LifecycleEventObserver { _, event ->
58+
if (event == Lifecycle.Event.ON_RESUME && isAuthenticated) {
59+
if (hasCheckedOnStartup && isAutoReadClipboardEnabled) {
60+
scope.launch {
61+
showClipboardDialog = context.hasScanDataInClipboard()
62+
}
63+
}
64+
}
65+
}
66+
lifecycleOwner.lifecycle.addObserver(observer)
67+
onDispose {
68+
lifecycleOwner.lifecycle.removeObserver(observer)
69+
}
70+
}
71+
72+
if (showClipboardDialog && isAuthenticated) {
73+
ClipboardDataDialog(
74+
onConfirm = {
75+
context.getClipboardText()?.let { data ->
76+
appViewModel.onClipboardAutoRead(data)
77+
}
78+
showClipboardDialog = false
79+
},
80+
onDismiss = {
81+
showClipboardDialog = false
82+
}
83+
)
84+
}
85+
}
86+
87+
private suspend fun Context.hasScanDataInClipboard(): Boolean {
88+
delay(1000) // delay needed for Android clipboard accessibility on start
89+
90+
val clipText = this.getClipboardText()
91+
if (clipText.isNullOrBlank()) return false
92+
93+
val scanResult = runCatching { decode(clipText) }
94+
return scanResult.isSuccess
95+
}

app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -820,6 +820,12 @@ class AppViewModel @Inject constructor(
820820
}
821821
}
822822
// endregion
823+
824+
fun onClipboardAutoRead(data: String) {
825+
viewModelScope.launch {
826+
mainScreenEffect(MainScreenEffect.ProcessClipboardAutoRead(data))
827+
}
828+
}
823829
}
824830

825831
// region send contract
@@ -852,6 +858,7 @@ sealed class SendEffect {
852858
sealed class MainScreenEffect {
853859
data class NavigateActivityDetail(val activityId: String) : MainScreenEffect()
854860
data object WipeStorage : MainScreenEffect()
861+
data class ProcessClipboardAutoRead(val data: String) : MainScreenEffect()
855862
}
856863

857864
sealed class SendEvent {

0 commit comments

Comments
 (0)