feat: improve onboarding
This commit is contained in:
@@ -74,6 +74,7 @@ dependencies {
|
|||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||||
|
implementation(libs.androidx.material3)
|
||||||
implementation(libs.androidx.ui)
|
implementation(libs.androidx.ui)
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import kotlinx.coroutines.flow.SharingStarted
|
|||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import com.bitcointxoko.gudariwallet.security.EncryptedSecretStore
|
import com.bitcointxoko.gudariwallet.security.EncryptedSecretStore
|
||||||
import com.bitcointxoko.gudariwallet.service.WalletNotificationService
|
import com.bitcointxoko.gudariwallet.service.WalletNotificationService
|
||||||
import com.bitcointxoko.gudariwallet.ui.OnboardingScreen
|
import com.bitcointxoko.gudariwallet.ui.onboarding.OnboardingScreen
|
||||||
import com.bitcointxoko.gudariwallet.ui.WalletScreen
|
import com.bitcointxoko.gudariwallet.ui.WalletScreen
|
||||||
import com.bitcointxoko.gudariwallet.ui.WalletViewModel
|
import com.bitcointxoko.gudariwallet.ui.WalletViewModel
|
||||||
import com.bitcointxoko.gudariwallet.ui.WalletViewModelFactory
|
import com.bitcointxoko.gudariwallet.ui.WalletViewModelFactory
|
||||||
@@ -37,8 +37,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
|||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import cafe.adriel.lyricist.ProvideStrings
|
import cafe.adriel.lyricist.ProvideStrings
|
||||||
import cafe.adriel.lyricist.rememberStrings
|
|
||||||
import com.bitcointxoko.gudariwallet.i18n.AppStrings
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@@ -129,23 +127,26 @@ class MainActivity : AppCompatActivity() {
|
|||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
WalletNotificationService.start(this@MainActivity)
|
WalletNotificationService.start(this@MainActivity)
|
||||||
requestNotificationPermissionIfNeeded()
|
requestNotificationPermissionIfNeeded()
|
||||||
promptBatteryOptimizationIfNeeded()
|
// promptBatteryOptimizationIfNeeded()
|
||||||
}
|
}
|
||||||
WalletScreen(
|
WalletScreen(
|
||||||
vm = vm,
|
vm = vm,
|
||||||
nfcVm = nfcVm,
|
nfcVm = nfcVm,
|
||||||
pendingPaymentHash = pendingPaymentHash,
|
pendingPaymentHash = pendingPaymentHash,
|
||||||
pendingPaymentUri = pendingPaymentUri,
|
pendingPaymentUri = pendingPaymentUri,
|
||||||
lyricist = lyricist // ← pass down
|
lyricist = lyricist
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
OnboardingScreen(
|
OnboardingScreen(
|
||||||
secretStore = secretStore,
|
secretStore = secretStore,
|
||||||
onComplete = { }
|
onRequestNotificationPermission = { requestNotificationPermissionIfNeeded() }, // ← new
|
||||||
|
onComplete = {
|
||||||
|
// WalletNotificationService.start(this@MainActivity)
|
||||||
|
// No permission/battery calls here — handled in-flow
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} // end ProvideStrings
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -307,16 +307,30 @@ val EnHomeStrings = AppStrings(
|
|||||||
paymentRowNoMemo = "No memo",
|
paymentRowNoMemo = "No memo",
|
||||||
paymentRowPending = "Pending",
|
paymentRowPending = "Pending",
|
||||||
|
|
||||||
// ── OnboardingScreen ──────────────────────────────────────────────────────
|
onboarding = OnboardingStrings(
|
||||||
onboardingTitle = "Connect to LNbits",
|
back = "Back",
|
||||||
onboardingSubtitle = "Enter your LNbits server URL and API keys.",
|
next = "Next",
|
||||||
onboardingServerUrl = "Server URL",
|
done = "Done",
|
||||||
onboardingServerUrlHint = "https://bitcointxoko.org",
|
title = "Connect to LNbits",
|
||||||
onboardingInvoiceKey = "Invoice Key (read-only)",
|
subtitle = "Enter your LNbits server URL and API keys.",
|
||||||
onboardingAdminKey = "Admin Key (for sending)",
|
serverUrl = "Server URL",
|
||||||
onboardingHideAdminKey = "Hide",
|
serverUrlHint = "https://bitcointxoko.org",
|
||||||
onboardingShowAdminKey = "Show",
|
invoiceKey = "Invoice Key (read-only)",
|
||||||
onboardingFieldsRequired = "All fields are required.",
|
adminKey = "Admin Key (for sending)",
|
||||||
onboardingConnect = "Connect",
|
hideAdminKey = "Hide",
|
||||||
|
showAdminKey = "Show",
|
||||||
|
fieldsRequired = "All fields are required.",
|
||||||
|
connect = "Connect",
|
||||||
|
notificationsTitle = "Stay in the loop",
|
||||||
|
notificationsSubtitle = "Get notified instantly when you receive a payment.",
|
||||||
|
allowNotifications = "Allow Notifications",
|
||||||
|
notificationsNote = "You can change this later in system Settings.",
|
||||||
|
notificationsAutoGranted = "Notifications are enabled automatically on your device.",
|
||||||
|
batteryTitle = "Background activity",
|
||||||
|
batterySubtitle = "Allow Gudari Wallet to run unrestricted so notifications arrive even when the app is closed.",
|
||||||
|
openSettings = "Open Settings",
|
||||||
|
batteryNote = "Tap 'Battery' → 'Unrestricted'. You can skip this and change it later.",
|
||||||
|
batteryAlreadyUnrestricted = "You're all set — no restrictions detected."
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -306,15 +306,29 @@ val EsHomeStrings = AppStrings(
|
|||||||
paymentRowNoMemo = "Sin nota",
|
paymentRowNoMemo = "Sin nota",
|
||||||
paymentRowPending = "Pendiente",
|
paymentRowPending = "Pendiente",
|
||||||
|
|
||||||
// ── OnboardingScreen ──────────────────────────────────────────────────────
|
onboarding = OnboardingStrings(
|
||||||
onboardingTitle = "Conectar a LNbits",
|
back = "Back",
|
||||||
onboardingSubtitle = "Introduce la URL de tu servidor LNbits y las claves API.",
|
next = "Next",
|
||||||
onboardingServerUrl = "URL del servidor",
|
done = "Done",
|
||||||
onboardingServerUrlHint = "https://bitcointxoko.org",
|
title = "Connect to LNbits",
|
||||||
onboardingInvoiceKey = "Clave de factura (solo lectura)",
|
subtitle = "Enter your LNbits server URL and API keys.",
|
||||||
onboardingAdminKey = "Clave de administrador (para enviar)",
|
serverUrl = "Server URL",
|
||||||
onboardingHideAdminKey = "Ocultar",
|
serverUrlHint = "https://bitcointxoko.org",
|
||||||
onboardingShowAdminKey = "Mostrar",
|
invoiceKey = "Invoice Key (read-only)",
|
||||||
onboardingFieldsRequired = "Todos los campos son obligatorios.",
|
adminKey = "Admin Key (for sending)",
|
||||||
onboardingConnect = "Conectar",
|
hideAdminKey = "Hide",
|
||||||
|
showAdminKey = "Show",
|
||||||
|
fieldsRequired = "All fields are required.",
|
||||||
|
connect = "Connect",
|
||||||
|
notificationsTitle = "Stay in the loop",
|
||||||
|
notificationsSubtitle = "Get notified instantly when you receive a payment.",
|
||||||
|
allowNotifications = "Allow Notifications",
|
||||||
|
notificationsNote = "You can change this later in system Settings.",
|
||||||
|
notificationsAutoGranted = "Notifications are enabled automatically on your device.",
|
||||||
|
batteryTitle = "Background activity",
|
||||||
|
batterySubtitle = "Allow Gudari Wallet to run unrestricted so notifications arrive even when the app is closed.",
|
||||||
|
openSettings = "Open Settings",
|
||||||
|
batteryNote = "Tap 'Battery' → 'Unrestricted'. You can skip this and change it later.",
|
||||||
|
batteryAlreadyUnrestricted = "You're all set — no restrictions detected."
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -306,15 +306,29 @@ val EuStrings = AppStrings(
|
|||||||
paymentRowNoMemo = "Oharrik gabe",
|
paymentRowNoMemo = "Oharrik gabe",
|
||||||
paymentRowPending = "Zain",
|
paymentRowPending = "Zain",
|
||||||
|
|
||||||
// ── OnboardingScreen ──────────────────────────────────────────────────────
|
onboarding = OnboardingStrings(
|
||||||
onboardingTitle = "Konektatu LNbits-era",
|
back = "Back",
|
||||||
onboardingSubtitle = "Sartu zure LNbits zerbitzariaren URLa eta API gakoak.",
|
next = "Next",
|
||||||
onboardingServerUrl = "Zerbitzariaren URLa",
|
done = "Done",
|
||||||
onboardingServerUrlHint = "https://bitcointxoko.org",
|
title = "Connect to LNbits",
|
||||||
onboardingInvoiceKey = "Faktura-gakoa (irakurtzeko soilik)",
|
subtitle = "Enter your LNbits server URL and API keys.",
|
||||||
onboardingAdminKey = "Administrazio-gakoa (bidaltzeko)",
|
serverUrl = "Server URL",
|
||||||
onboardingHideAdminKey = "Ezkutatu",
|
serverUrlHint = "https://bitcointxoko.org",
|
||||||
onboardingShowAdminKey = "Erakutsi",
|
invoiceKey = "Invoice Key (read-only)",
|
||||||
onboardingFieldsRequired = "Eremu guztiak beharrezkoak dira.",
|
adminKey = "Admin Key (for sending)",
|
||||||
onboardingConnect = "Konektatu",
|
hideAdminKey = "Hide",
|
||||||
|
showAdminKey = "Show",
|
||||||
|
fieldsRequired = "All fields are required.",
|
||||||
|
connect = "Connect",
|
||||||
|
notificationsTitle = "Stay in the loop",
|
||||||
|
notificationsSubtitle = "Get notified instantly when you receive a payment.",
|
||||||
|
allowNotifications = "Allow Notifications",
|
||||||
|
notificationsNote = "You can change this later in system Settings.",
|
||||||
|
notificationsAutoGranted = "Notifications are enabled automatically on your device.",
|
||||||
|
batteryTitle = "Background activity",
|
||||||
|
batterySubtitle = "Allow Gudari Wallet to run unrestricted so notifications arrive even when the app is closed.",
|
||||||
|
openSettings = "Open Settings",
|
||||||
|
batteryNote = "Tap 'Battery' → 'Unrestricted'. You can skip this and change it later.",
|
||||||
|
batteryAlreadyUnrestricted = "You're all set — no restrictions detected."
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -303,15 +303,31 @@ data class AppStrings(
|
|||||||
val paymentRowNoMemo : String,
|
val paymentRowNoMemo : String,
|
||||||
val paymentRowPending : String,
|
val paymentRowPending : String,
|
||||||
|
|
||||||
// ── OnboardingScreen ──────────────────────────────────────────────────────
|
val onboarding: OnboardingStrings
|
||||||
val onboardingTitle : String,
|
)
|
||||||
val onboardingSubtitle : String,
|
|
||||||
val onboardingServerUrl : String,
|
data class OnboardingStrings(
|
||||||
val onboardingServerUrlHint : String,
|
val title: String,
|
||||||
val onboardingInvoiceKey : String,
|
val subtitle: String,
|
||||||
val onboardingAdminKey : String,
|
val serverUrl: String,
|
||||||
val onboardingHideAdminKey : String,
|
val serverUrlHint: String,
|
||||||
val onboardingShowAdminKey : String,
|
val invoiceKey: String,
|
||||||
val onboardingFieldsRequired : String,
|
val adminKey: String,
|
||||||
val onboardingConnect : String,
|
val showAdminKey: String,
|
||||||
|
val hideAdminKey: String,
|
||||||
|
val connect: String,
|
||||||
|
val fieldsRequired: String,
|
||||||
|
val back: String,
|
||||||
|
val next: String,
|
||||||
|
val done: String,
|
||||||
|
val notificationsTitle: String,
|
||||||
|
val notificationsSubtitle: String,
|
||||||
|
val allowNotifications: String,
|
||||||
|
val notificationsNote: String,
|
||||||
|
val notificationsAutoGranted: String,
|
||||||
|
val batteryTitle: String,
|
||||||
|
val batterySubtitle: String,
|
||||||
|
val openSettings: String,
|
||||||
|
val batteryNote: String,
|
||||||
|
val batteryAlreadyUnrestricted: String
|
||||||
)
|
)
|
||||||
@@ -18,6 +18,7 @@ interface SecretStore {
|
|||||||
suspend fun isOnboarded(): Boolean
|
suspend fun isOnboarded(): Boolean
|
||||||
|
|
||||||
suspend fun saveCredentials(baseUrl: String, invoiceKey: String, adminKey: String)
|
suspend fun saveCredentials(baseUrl: String, invoiceKey: String, adminKey: String)
|
||||||
|
suspend fun setOnboarded()
|
||||||
}
|
}
|
||||||
|
|
||||||
class EncryptedSecretStore(context: Context) : SecretStore {
|
class EncryptedSecretStore(context: Context) : SecretStore {
|
||||||
@@ -53,9 +54,15 @@ class EncryptedSecretStore(context: Context) : SecretStore {
|
|||||||
.setBaseUrl(baseUrl)
|
.setBaseUrl(baseUrl)
|
||||||
.setInvoiceKey(invoiceKey)
|
.setInvoiceKey(invoiceKey)
|
||||||
.setAdminKey(adminKey)
|
.setAdminKey(adminKey)
|
||||||
.setIsOnboarded(true)
|
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
Log.i(TAG, "Credentials saved and DataStore updated")
|
Log.i(TAG, "Credentials saved and DataStore updated")
|
||||||
}
|
}
|
||||||
|
override suspend fun setOnboarded() {
|
||||||
|
dataStore.updateData { current ->
|
||||||
|
current.toBuilder()
|
||||||
|
.setIsOnboarded(true)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
package com.bitcointxoko.gudariwallet.ui
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Visibility
|
|
||||||
import androidx.compose.material.icons.filled.VisibilityOff
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
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.text.input.KeyboardType
|
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.bitcointxoko.gudariwallet.LocalAppStrings
|
|
||||||
import com.bitcointxoko.gudariwallet.security.SecretStore
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun OnboardingScreen(
|
|
||||||
secretStore: SecretStore,
|
|
||||||
onComplete : () -> Unit
|
|
||||||
) {
|
|
||||||
val strings = LocalAppStrings.current
|
|
||||||
|
|
||||||
var baseUrl by remember { mutableStateOf("https://bitcointxoko.org") }
|
|
||||||
var invoiceKey by remember { mutableStateOf("") }
|
|
||||||
var adminKey by remember { mutableStateOf("") }
|
|
||||||
var adminVisible by remember { mutableStateOf(false) }
|
|
||||||
var error by remember { mutableStateOf<String?>(null) }
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(32.dp),
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = strings.onboardingTitle, // ← "Connect to LNbits"
|
|
||||||
style = MaterialTheme.typography.headlineMedium
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(8.dp))
|
|
||||||
Text(
|
|
||||||
text = strings.onboardingSubtitle, // ← "Enter your LNbits server URL and API keys."
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(32.dp))
|
|
||||||
|
|
||||||
OutlinedTextField(
|
|
||||||
value = baseUrl,
|
|
||||||
onValueChange = { baseUrl = it },
|
|
||||||
label = { Text(strings.onboardingServerUrl) }, // ← "Server URL"
|
|
||||||
placeholder = { Text(strings.onboardingServerUrlHint) }, // ← "https://bitcointxoko.org"
|
|
||||||
singleLine = true,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri)
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(16.dp))
|
|
||||||
|
|
||||||
OutlinedTextField(
|
|
||||||
value = invoiceKey,
|
|
||||||
onValueChange = { invoiceKey = it },
|
|
||||||
label = { Text(strings.onboardingInvoiceKey) }, // ← "Invoice Key (read-only)"
|
|
||||||
singleLine = true,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
|
||||||
visualTransformation = PasswordVisualTransformation()
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(16.dp))
|
|
||||||
|
|
||||||
OutlinedTextField(
|
|
||||||
value = adminKey,
|
|
||||||
onValueChange = { adminKey = it },
|
|
||||||
label = { Text(strings.onboardingAdminKey) }, // ← "Admin Key (for sending)"
|
|
||||||
singleLine = true,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
|
||||||
visualTransformation = if (adminVisible) VisualTransformation.None
|
|
||||||
else PasswordVisualTransformation(),
|
|
||||||
trailingIcon = {
|
|
||||||
IconButton(onClick = { adminVisible = !adminVisible }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = if (adminVisible) Icons.Default.VisibilityOff
|
|
||||||
else Icons.Default.Visibility,
|
|
||||||
contentDescription = if (adminVisible) strings.onboardingHideAdminKey // ← "Hide"
|
|
||||||
else strings.onboardingShowAdminKey // ← "Show"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
error?.let {
|
|
||||||
Spacer(Modifier.height(8.dp))
|
|
||||||
Text(
|
|
||||||
text = it,
|
|
||||||
color = MaterialTheme.colorScheme.error,
|
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(Modifier.height(32.dp))
|
|
||||||
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
if (baseUrl.isBlank() || invoiceKey.isBlank() || adminKey.isBlank()) {
|
|
||||||
error = strings.onboardingFieldsRequired // ← "All fields are required."
|
|
||||||
return@Button
|
|
||||||
}
|
|
||||||
scope.launch {
|
|
||||||
secretStore.saveCredentials(baseUrl, invoiceKey, adminKey)
|
|
||||||
onComplete()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Text(strings.onboardingConnect) // ← "Connect"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package com.bitcointxoko.gudariwallet.ui.onboarding
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.pager.PagerState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
// ── Reusable page shell ────────────────────────────────────────────────────────
|
||||||
|
@Composable
|
||||||
|
internal fun OnboardingPageScaffold(
|
||||||
|
title: String,
|
||||||
|
subtitle: String,
|
||||||
|
content: @Composable ColumnScope.() -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(32.dp),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(text = title, style = MaterialTheme.typography.headlineSmall)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = subtitle,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(32.dp))
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Inline error text ──────────────────────────────────────────────────────────
|
||||||
|
@Composable
|
||||||
|
internal fun ErrorText(error: String?) {
|
||||||
|
if (error != null) {
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = error,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Dot indicator ──────────────────────────────────────────────────────────────
|
||||||
|
@Composable
|
||||||
|
internal fun PagerDotIndicator(
|
||||||
|
pagerState: PagerState,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
repeat(pagerState.pageCount) { index ->
|
||||||
|
val isSelected = pagerState.currentPage == index
|
||||||
|
val color by animateColorAsState(
|
||||||
|
targetValue = if (isSelected) MaterialTheme.colorScheme.primary
|
||||||
|
else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f),
|
||||||
|
label = "dotColor"
|
||||||
|
)
|
||||||
|
val size = if (isSelected) 10.dp else 8.dp
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(size)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(color)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
package com.bitcointxoko.gudariwallet.ui.onboarding
|
||||||
|
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.PowerManager
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.BatteryFull
|
||||||
|
import androidx.compose.material.icons.filled.Notifications
|
||||||
|
import androidx.compose.material.icons.filled.Visibility
|
||||||
|
import androidx.compose.material.icons.filled.VisibilityOff
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.bitcointxoko.gudariwallet.i18n.AppStrings
|
||||||
|
|
||||||
|
// ── Page 0: Welcome ────────────────────────────────────────────────────────────
|
||||||
|
@Composable
|
||||||
|
internal fun WelcomePage(strings: AppStrings) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(32.dp),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
// Replace with your actual illustration / logo
|
||||||
|
Text(text = "⚡", style = MaterialTheme.typography.displayLarge)
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
Text(
|
||||||
|
text = strings.onboarding.title, // ← "Connect to LNbits"
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = strings.onboarding.subtitle, // ← "Enter your LNbits server URL and API keys."
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Page 1: Server URL ─────────────────────────────────────────────────────────
|
||||||
|
@Composable
|
||||||
|
internal fun ServerUrlPage(
|
||||||
|
strings: AppStrings,
|
||||||
|
baseUrl: String,
|
||||||
|
onBaseUrl: (String) -> Unit,
|
||||||
|
error: String?
|
||||||
|
) {
|
||||||
|
OnboardingPageScaffold(
|
||||||
|
title = strings.onboarding.serverUrl, // ← "Server URL"
|
||||||
|
subtitle = strings.onboarding.serverUrlHint // ← "https://bitcointxoko.org"
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = baseUrl,
|
||||||
|
onValueChange = onBaseUrl,
|
||||||
|
label = { Text(strings.onboarding.serverUrl) },
|
||||||
|
placeholder = { Text(strings.onboarding.serverUrlHint) },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri),
|
||||||
|
isError = error != null
|
||||||
|
)
|
||||||
|
ErrorText(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Page 2: Invoice Key ────────────────────────────────────────────────────────
|
||||||
|
@Composable
|
||||||
|
internal fun InvoiceKeyPage(
|
||||||
|
strings: AppStrings,
|
||||||
|
invoiceKey: String,
|
||||||
|
onInvoiceKey: (String) -> Unit,
|
||||||
|
error: String?
|
||||||
|
) {
|
||||||
|
OnboardingPageScaffold(
|
||||||
|
title = strings.onboarding.invoiceKey, // ← "Invoice Key (read-only)"
|
||||||
|
subtitle = "Used to generate payment requests."
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = invoiceKey,
|
||||||
|
onValueChange = onInvoiceKey,
|
||||||
|
label = { Text(strings.onboarding.invoiceKey) },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||||
|
visualTransformation = PasswordVisualTransformation(),
|
||||||
|
isError = error != null
|
||||||
|
)
|
||||||
|
ErrorText(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Page 3: Admin Key ──────────────────────────────────────────────────────────
|
||||||
|
@Composable
|
||||||
|
internal fun AdminKeyPage(
|
||||||
|
strings: AppStrings,
|
||||||
|
adminKey: String,
|
||||||
|
onAdminKey: (String) -> Unit,
|
||||||
|
adminVisible: Boolean,
|
||||||
|
onToggleVis: () -> Unit,
|
||||||
|
error: String?
|
||||||
|
) {
|
||||||
|
OnboardingPageScaffold(
|
||||||
|
title = strings.onboarding.adminKey, // ← "Admin Key (for sending)"
|
||||||
|
subtitle = "Required to send payments."
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = adminKey,
|
||||||
|
onValueChange = onAdminKey,
|
||||||
|
label = { Text(strings.onboarding.adminKey) },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||||
|
visualTransformation = if (adminVisible) VisualTransformation.None
|
||||||
|
else PasswordVisualTransformation(),
|
||||||
|
isError = error != null,
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = onToggleVis) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (adminVisible) Icons.Default.VisibilityOff
|
||||||
|
else Icons.Default.Visibility,
|
||||||
|
contentDescription = if (adminVisible) strings.onboarding.hideAdminKey
|
||||||
|
else strings.onboarding.showAdminKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ErrorText(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Page 4: Notifications ──────────────────────────────────────────────────────
|
||||||
|
@Composable
|
||||||
|
fun NotificationsPage(
|
||||||
|
strings: AppStrings,
|
||||||
|
onRequestNotificationPermission: () -> Unit
|
||||||
|
) {
|
||||||
|
// On API < 33, the permission doesn't exist — show a reduced UI
|
||||||
|
val needsPermission = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
|
||||||
|
|
||||||
|
OnboardingPageScaffold(
|
||||||
|
title = strings.onboarding.notificationsTitle, // ← "Stay in the loop"
|
||||||
|
subtitle = strings.onboarding.notificationsSubtitle // ← "Get notified instantly when you receive a payment."
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Notifications,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(72.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(32.dp))
|
||||||
|
|
||||||
|
if (needsPermission) {
|
||||||
|
Button(
|
||||||
|
onClick = onRequestNotificationPermission,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(strings.onboarding.allowNotifications) // ← "Allow Notifications"
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = strings.onboarding.notificationsNote,
|
||||||
|
// ← "You can change this later in system Settings."
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// API < 33: permission granted implicitly, nothing to request
|
||||||
|
Text(
|
||||||
|
text = strings.onboarding.notificationsAutoGranted,
|
||||||
|
// ← "Notifications are enabled automatically on your device."
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Page 5: Battery optimisation ──────────────────────────────────────────────
|
||||||
|
@Composable
|
||||||
|
fun BatteryPage(strings: AppStrings) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||||
|
val alreadyUnrestricted = pm.isIgnoringBatteryOptimizations(context.packageName)
|
||||||
|
|
||||||
|
OnboardingPageScaffold(
|
||||||
|
title = strings.onboarding.batteryTitle, // ← "Background activity"
|
||||||
|
subtitle = strings.onboarding.batterySubtitle // ← "Allow Gudari Wallet to run unrestricted so notifications arrive even when the app is closed."
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.BatteryFull,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(72.dp),
|
||||||
|
tint = if (alreadyUnrestricted)
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(32.dp))
|
||||||
|
|
||||||
|
if (alreadyUnrestricted) {
|
||||||
|
Text(
|
||||||
|
text = strings.onboarding.batteryAlreadyUnrestricted,
|
||||||
|
// ← "You're all set — no restrictions detected."
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Button(
|
||||||
|
onClick = { openBatterySettings(context) },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(strings.onboarding.openSettings) // ← "Open Settings"
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = strings.onboarding.batteryNote,
|
||||||
|
// ← "Tap 'Battery' → 'Unrestricted'. You can skip this and change it later."
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Battery settings helper (moved from MainActivity) ─────────────────────────
|
||||||
|
private fun openBatterySettings(context: Context) {
|
||||||
|
val appDetailsIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||||
|
data = "package:${context.packageName}".toUri()
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
context.startActivity(appDetailsIntent)
|
||||||
|
} catch (_: ActivityNotFoundException) {
|
||||||
|
try {
|
||||||
|
context.startActivity(
|
||||||
|
Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (_: ActivityNotFoundException) { /* nothing to do */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
package com.bitcointxoko.gudariwallet.ui.onboarding
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.bitcointxoko.gudariwallet.LocalAppStrings
|
||||||
|
import com.bitcointxoko.gudariwallet.security.SecretStore
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
private const val PAGE_WELCOME = 0
|
||||||
|
private const val PAGE_SERVER_URL = 1
|
||||||
|
private const val PAGE_INVOICE = 2
|
||||||
|
private const val PAGE_ADMIN = 3
|
||||||
|
private const val PAGE_NOTIFICATIONS = 4
|
||||||
|
private const val PAGE_BATTERY = 5
|
||||||
|
private const val PAGE_COUNT = 6
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OnboardingScreen(
|
||||||
|
secretStore: SecretStore,
|
||||||
|
onRequestNotificationPermission: () -> Unit, // ← new parameter
|
||||||
|
onComplete: () -> Unit
|
||||||
|
) {
|
||||||
|
val strings = LocalAppStrings.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
var baseUrl by remember { mutableStateOf("https://bitcointxoko.org") }
|
||||||
|
var invoiceKey by remember { mutableStateOf("") }
|
||||||
|
var adminKey by remember { mutableStateOf("") }
|
||||||
|
var adminVisible by remember { mutableStateOf(false) }
|
||||||
|
var error by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
// Track whether credentials have been saved (so we don't save twice on back/forward)
|
||||||
|
var credentialsSaved by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val pagerState = rememberPagerState(pageCount = { PAGE_COUNT })
|
||||||
|
|
||||||
|
LaunchedEffect(pagerState.currentPage) { error = null }
|
||||||
|
|
||||||
|
fun validateCurrentPage(): Boolean {
|
||||||
|
error = when (pagerState.currentPage) {
|
||||||
|
PAGE_SERVER_URL -> if (baseUrl.isBlank()) strings.onboarding.fieldsRequired else null
|
||||||
|
PAGE_INVOICE -> if (invoiceKey.isBlank()) strings.onboarding.fieldsRequired else null
|
||||||
|
PAGE_ADMIN -> if (adminKey.isBlank()) strings.onboarding.fieldsRequired else null
|
||||||
|
else -> null // PAGE_NOTIFICATIONS and PAGE_BATTERY always pass
|
||||||
|
}
|
||||||
|
return error == null
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold { innerPadding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(innerPadding)
|
||||||
|
) {
|
||||||
|
HorizontalPager(
|
||||||
|
state = pagerState,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
// Prevent swiping back past the admin page once credentials are saved,
|
||||||
|
// and prevent skipping the permission pages entirely
|
||||||
|
userScrollEnabled = true
|
||||||
|
) { page ->
|
||||||
|
when (page) {
|
||||||
|
PAGE_WELCOME -> WelcomePage(strings)
|
||||||
|
PAGE_SERVER_URL -> ServerUrlPage(strings, baseUrl, { baseUrl = it }, error)
|
||||||
|
PAGE_INVOICE -> InvoiceKeyPage(strings, invoiceKey, { invoiceKey = it }, error)
|
||||||
|
PAGE_ADMIN -> AdminKeyPage(
|
||||||
|
strings = strings,
|
||||||
|
adminKey = adminKey,
|
||||||
|
onAdminKey = { adminKey = it },
|
||||||
|
adminVisible = adminVisible,
|
||||||
|
onToggleVis = { adminVisible = !adminVisible },
|
||||||
|
error = error
|
||||||
|
)
|
||||||
|
PAGE_NOTIFICATIONS -> NotificationsPage(
|
||||||
|
strings = strings,
|
||||||
|
onRequestNotificationPermission = onRequestNotificationPermission
|
||||||
|
)
|
||||||
|
PAGE_BATTERY -> BatteryPage(strings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PagerDotIndicator(
|
||||||
|
pagerState = pagerState,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.padding(vertical = 12.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp, vertical = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
if (pagerState.currentPage > PAGE_WELCOME) {
|
||||||
|
OutlinedButton(onClick = {
|
||||||
|
scope.launch {
|
||||||
|
pagerState.animateScrollToPage(pagerState.currentPage - 1)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
||||||
|
Spacer(Modifier.width(4.dp))
|
||||||
|
Text(strings.onboarding.back)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Spacer(Modifier.width(1.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
val isLastPage = pagerState.currentPage == PAGE_BATTERY
|
||||||
|
|
||||||
|
Button(onClick = {
|
||||||
|
if (!validateCurrentPage()) return@Button
|
||||||
|
|
||||||
|
when (pagerState.currentPage) {
|
||||||
|
PAGE_ADMIN -> {
|
||||||
|
if (!credentialsSaved) {
|
||||||
|
scope.launch {
|
||||||
|
secretStore.saveCredentials(baseUrl, invoiceKey, adminKey)
|
||||||
|
credentialsSaved = true
|
||||||
|
pagerState.animateScrollToPage(PAGE_NOTIFICATIONS)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
scope.launch { pagerState.animateScrollToPage(PAGE_NOTIFICATIONS) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PAGE_BATTERY -> {
|
||||||
|
scope.launch {
|
||||||
|
secretStore.setOnboarded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> scope.launch {
|
||||||
|
pagerState.animateScrollToPage(pagerState.currentPage + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text(if (isLastPage) strings.onboarding.done else strings.onboarding.next)
|
||||||
|
if (!isLastPage) {
|
||||||
|
Spacer(Modifier.width(4.dp))
|
||||||
|
Icon(Icons.AutoMirrored.Filled.ArrowForward, contentDescription = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.bitcointxoko.gudariwallet.ui.onboarding
|
||||||
|
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.bitcointxoko.gudariwallet.security.SecretStore
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class OnboardingViewModel(private val secretStore: SecretStore) : ViewModel() {
|
||||||
|
var baseUrl by mutableStateOf("https://bitcointxoko.org")
|
||||||
|
var invoiceKey by mutableStateOf("")
|
||||||
|
var adminKey by mutableStateOf("")
|
||||||
|
var adminKeyVisible by mutableStateOf(false)
|
||||||
|
var error by mutableStateOf<String?>(null)
|
||||||
|
|
||||||
|
fun saveAndComplete(onComplete: () -> Unit) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
secretStore.saveCredentials(baseUrl, invoiceKey, adminKey)
|
||||||
|
onComplete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ tink = "1.13.0"
|
|||||||
ui = "1.11.2"
|
ui = "1.11.2"
|
||||||
secp256k1-kmp = "0.23.0"
|
secp256k1-kmp = "0.23.0"
|
||||||
lyricist = "1.8.0"
|
lyricist = "1.8.0"
|
||||||
|
material3 = "1.4.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" }
|
androidx-biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" }
|
||||||
@@ -71,6 +72,7 @@ androidx-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "ui" }
|
|||||||
secp256k1-kmp-android = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-android", version.ref = "secp256k1-kmp" }
|
secp256k1-kmp-android = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-android", version.ref = "secp256k1-kmp" }
|
||||||
lyricist = { module = "cafe.adriel.lyricist:lyricist", version.ref = "lyricist" }
|
lyricist = { module = "cafe.adriel.lyricist:lyricist", version.ref = "lyricist" }
|
||||||
lyricist-processor = { module = "cafe.adriel.lyricist:lyricist-processor", version.ref = "lyricist" }
|
lyricist-processor = { module = "cafe.adriel.lyricist:lyricist-processor", version.ref = "lyricist" }
|
||||||
|
androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|||||||
Reference in New Issue
Block a user