16
16
17
17
package fr.acinq.phoenix.android.payments.offer
18
18
19
+ import androidx.compose.foundation.layout.Column
19
20
import androidx.compose.foundation.layout.Row
20
21
import androidx.compose.foundation.layout.Spacer
22
+ import androidx.compose.foundation.layout.fillMaxWidth
21
23
import androidx.compose.foundation.layout.height
22
- import androidx.compose.foundation.text.selection.SelectionContainer
24
+ import androidx.compose.foundation.layout.offset
25
+ import androidx.compose.foundation.layout.padding
26
+ import androidx.compose.foundation.layout.sizeIn
27
+ import androidx.compose.foundation.rememberScrollState
28
+ import androidx.compose.foundation.shape.RoundedCornerShape
29
+ import androidx.compose.foundation.verticalScroll
23
30
import androidx.compose.material.MaterialTheme
24
31
import androidx.compose.material.Text
32
+ import androidx.compose.material3.ExperimentalMaterial3Api
33
+ import androidx.compose.material3.ModalBottomSheet
34
+ import androidx.compose.material3.rememberModalBottomSheetState
25
35
import androidx.compose.runtime.Composable
26
36
import androidx.compose.runtime.LaunchedEffect
27
37
import androidx.compose.runtime.collectAsState
@@ -33,7 +43,7 @@ import androidx.compose.ui.Alignment
33
43
import androidx.compose.ui.Modifier
34
44
import androidx.compose.ui.platform.LocalContext
35
45
import androidx.compose.ui.res.stringResource
36
- import androidx.compose.ui.text.style.TextOverflow
46
+ import androidx.compose.ui.text.style.TextAlign
37
47
import androidx.compose.ui.unit.dp
38
48
import androidx.compose.ui.unit.sp
39
49
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -46,10 +56,12 @@ import fr.acinq.phoenix.android.business
46
56
import fr.acinq.phoenix.android.components.AmountHeroInput
47
57
import fr.acinq.phoenix.android.components.AmountWithFiatRowView
48
58
import fr.acinq.phoenix.android.components.BackButtonWithBalance
59
+ import fr.acinq.phoenix.android.components.Clickable
49
60
import fr.acinq.phoenix.android.components.FilledButton
50
61
import fr.acinq.phoenix.android.components.ProgressView
51
62
import fr.acinq.phoenix.android.components.SplashLabelRow
52
63
import fr.acinq.phoenix.android.components.SplashLayout
64
+ import fr.acinq.phoenix.android.components.TextInput
53
65
import fr.acinq.phoenix.android.components.contact.ContactOrOfferView
54
66
import fr.acinq.phoenix.android.components.feedback.ErrorMessage
55
67
import fr.acinq.phoenix.android.payments.details.translatePaymentError
@@ -67,7 +79,7 @@ fun SendOfferView(
67
79
val balance = business.balanceManager.balance.collectAsState(null ).value
68
80
val prefBitcoinUnit = LocalBitcoinUnit .current
69
81
70
- val vm = viewModel<SendOfferViewModel >(factory = SendOfferViewModel .Factory (business.peerManager))
82
+ val vm = viewModel<SendOfferViewModel >(factory = SendOfferViewModel .Factory (business.peerManager, business.nodeParamsManager ))
71
83
val requestedAmount = offer.amount
72
84
var amount by remember { mutableStateOf(requestedAmount) }
73
85
val amountErrorMessage: String = remember(amount) {
@@ -90,6 +102,9 @@ fun SendOfferView(
90
102
}
91
103
val isOverpaymentEnabled by userPrefs.getIsOverpaymentEnabled.collectAsState(initial = false )
92
104
105
+ var message by remember { mutableStateOf(" " ) }
106
+ var showMessageDialog by remember { mutableStateOf(false ) }
107
+
93
108
SplashLayout (
94
109
header = { BackButtonWithBalance (onBackClick = onBackClick, balance = balance) },
95
110
topContent = {
@@ -112,10 +127,27 @@ fun SendOfferView(
112
127
Spacer (modifier = Modifier .height(8 .dp))
113
128
}
114
129
115
- SplashLabelRow (label = stringResource(id = R .string.send_destination_label)) {
130
+ SplashLabelRow (label = stringResource(id = R .string.send_destination_label), icon = R .drawable.ic_zap ) {
116
131
ContactOrOfferView (offer = offer)
117
132
}
118
133
134
+ Spacer (modifier = Modifier .height(8 .dp))
135
+ SplashLabelRow (label = stringResource(id = R .string.send_offer_payer_note_label), icon = R .drawable.ic_message_circle) {
136
+ Clickable (
137
+ onClick = { showMessageDialog = true },
138
+ modifier = Modifier
139
+ .fillMaxWidth()
140
+ .offset(x = (- 8 ).dp),
141
+ shape = RoundedCornerShape (12 .dp)
142
+ ) {
143
+ Column (modifier = Modifier .padding(8 .dp)) {
144
+ message.takeIf { it.isNotBlank() }?.let {
145
+ Text (text = it)
146
+ } ? : Text (text = stringResource(id = R .string.send_offer_payer_note_placeholder), style = MaterialTheme .typography.caption)
147
+ }
148
+ }
149
+ }
150
+
119
151
Spacer (modifier = Modifier .height(8 .dp))
120
152
SplashLabelRow (label = stringResource(id = R .string.send_trampoline_fee_label)) {
121
153
val amt = amount
@@ -134,10 +166,14 @@ fun SendOfferView(
134
166
offer = offer,
135
167
amount = amount,
136
168
isAmountInError = amountErrorMessage.isNotBlank(),
137
- onSendClick = { amount, offer -> vm.sendOffer(amount, offer) },
169
+ onSendClick = { amount, offer -> vm.sendOffer(amount, message, offer) },
138
170
onPaymentSent = onPaymentSent,
139
171
)
140
172
}
173
+
174
+ if (showMessageDialog) {
175
+ PayerNoteInput (initialMessage = message, onMessageChange = { message = it }, onDismiss = { showMessageDialog = false })
176
+ }
141
177
}
142
178
143
179
@Composable
@@ -158,6 +194,7 @@ private fun SendOfferStateButton(
158
194
is OfferState .Complete .Failed .CouldNotGetInvoice -> stringResource(id = R .string.send_offer_failure_timeout)
159
195
is OfferState .Complete .Failed .PaymentNotSent -> translatePaymentError(paymentFailure = state.reason)
160
196
is OfferState .Complete .Failed .Error -> state.throwable.message
197
+ is OfferState .Complete .Failed .PayerNoteTooLong -> " The message is too long (max. 64 chars)"
161
198
},
162
199
alignment = Alignment .CenterHorizontally ,
163
200
)
@@ -191,3 +228,43 @@ private fun SendOfferStateButton(
191
228
}
192
229
}
193
230
}
231
+
232
+ @OptIn(ExperimentalMaterial3Api ::class )
233
+ @Composable
234
+ private fun PayerNoteInput (
235
+ initialMessage : String ,
236
+ onMessageChange : (String ) -> Unit ,
237
+ onDismiss : () -> Unit ,
238
+ ) {
239
+ val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true )
240
+
241
+ ModalBottomSheet (
242
+ sheetState = sheetState,
243
+ onDismissRequest = onDismiss,
244
+ containerColor = MaterialTheme .colors.surface,
245
+ contentColor = MaterialTheme .colors.onSurface,
246
+ scrimColor = MaterialTheme .colors.onBackground.copy(alpha = 0.2f ),
247
+ ) {
248
+ Column (
249
+ modifier = Modifier
250
+ .verticalScroll(rememberScrollState())
251
+ .padding(horizontal = 32 .dp)
252
+ .sizeIn(minHeight = 400 .dp, maxHeight = 600 .dp),
253
+ horizontalAlignment = Alignment .CenterHorizontally
254
+ ) {
255
+ Text (text = " Attach a custom message" , style = MaterialTheme .typography.h4, textAlign = TextAlign .Center )
256
+ Text (text = " The recipient will see this message along with the payment" , style = MaterialTheme .typography.caption, textAlign = TextAlign .Center )
257
+ Spacer (modifier = Modifier .height(16 .dp))
258
+ TextInput (
259
+ text = initialMessage,
260
+ staticLabel = null ,
261
+ onTextChange = onMessageChange,
262
+ placeholder = { Text (text = " Enter a message" )},
263
+ maxChars = 64 ,
264
+ singleLine = true
265
+ )
266
+ Spacer (modifier = Modifier .height(16 .dp))
267
+ FilledButton (text = stringResource(id = R .string.btn_ok), onClick = onDismiss, modifier = Modifier .align(Alignment .End ))
268
+ }
269
+ }
270
+ }
0 commit comments