feat(cashu): add "Wallet Created" celebration before the mint picker

In the find-or-create wizard, choosing "Create a new wallet" jumped
straight to the mint manager. Add a short celebratory interstitial first:
an animated check badge springs in (bouncy overshoot) behind an expanding
pulse ring, "Wallet Created" + "Now pick a few mints to host your sats"
fade up, a haptic fires, and a "Pick mints" button continues to the mint
selection.

The screen is purely presentational — the kind:17375 still isn't
published until the user adds a mint on the next screen — so "Pick mints"
uses popUpTo to replace the interstitial in the back stack, avoiding an
awkward return to the celebration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqmMR2QiULS5QGosSgSQAe
This commit is contained in:
Claude
2026-06-29 18:02:31 +00:00
parent 39a8b7473e
commit d2dfd044cf
5 changed files with 189 additions and 1 deletions
@@ -236,6 +236,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.WalletReceiveScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.WalletScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.WalletSendScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.WalletTransactionsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.wizard.CashuWalletCreatedScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.wizard.CashuWalletWizardScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.webBookmarks.WebBookmarksScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.workouts.NewWorkoutScreen
@@ -381,6 +382,7 @@ fun BuildNavigation(
composableFromEndArgs<Route.WalletAddClinkDebit> { AddClinkDebitWalletScreen(accountViewModel, nav, it.ndebit) }
composableFromEnd<Route.CashuWallet> { CashuWalletScreen(accountViewModel, nav) }
composableFromEnd<Route.CashuWalletWizard> { CashuWalletWizardScreen(accountViewModel, nav) }
composableFromEnd<Route.CashuWalletCreated> { CashuWalletCreatedScreen(nav) }
composableFromEnd<Route.CashuWalletSettings> { CashuWalletSettingsScreen(accountViewModel, nav) }
composableFromEnd<Route.CashuMintRecommendations> { CashuMintRecommendationsScreen(accountViewModel, nav) }
composableFromBottomArgs<Route.SendPayment> { SendPaymentScreen(it.userHex, it.method, it.lnAddressOverride, it.btcAddressOverride, accountViewModel, nav) }
@@ -248,6 +248,9 @@ sealed class Route {
/** Find-or-create wizard, shown when no NIP-60 wallet is loaded. */
@Serializable object CashuWalletWizard : Route()
/** Celebratory interstitial after choosing to create a new wallet. */
@Serializable object CashuWalletCreated : Route()
@Serializable object CashuWalletSettings : Route()
@Serializable object CashuMintRecommendations : Route()
@@ -0,0 +1,180 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.wizard
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
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.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.shape.CircleShape
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.stringRes
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
/**
* Celebratory interstitial shown when the user chooses to create a new Cashu
* wallet in the find-or-create wizard, before the mint picker. An animated
* check badge springs in behind an expanding pulse ring, the "Wallet Created"
* headline + subtitle fade up, and a "Pick mints" button continues to the mint
* selection — turning the otherwise abrupt jump into the mint manager into a
* small reward.
*
* Purely presentational: the kind:17375 isn't published until the user actually
* adds a mint on the next screen, so "Pick mints" replaces this screen in the
* back stack (no awkward return to a celebration the user has moved past).
*/
@Composable
fun CashuWalletCreatedScreen(nav: INav) {
val haptic = LocalHapticFeedback.current
val badgeScale = remember { Animatable(0f) }
val ringScale = remember { Animatable(0.8f) }
val ringAlpha = remember { Animatable(0.45f) }
val contentAlpha = remember { Animatable(0f) }
LaunchedEffect(Unit) {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
// Badge pops in with a bouncy overshoot.
launch {
badgeScale.animateTo(
targetValue = 1f,
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow),
)
}
// A ring bursts outward from the badge and fades, one-shot.
launch { ringScale.animateTo(2.6f, tween(durationMillis = 650, easing = FastOutSlowInEasing)) }
launch { ringAlpha.animateTo(0f, tween(durationMillis = 650)) }
// Text + button fade up shortly after the badge lands.
launch {
delay(140)
contentAlpha.animateTo(1f, tween(durationMillis = 450))
}
}
Scaffold { padding ->
Column(
modifier =
Modifier
.fillMaxSize()
.padding(padding)
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Box(contentAlignment = Alignment.Center) {
// Pulse ring
Box(
modifier =
Modifier
.size(112.dp)
.graphicsLayer {
scaleX = ringScale.value
scaleY = ringScale.value
alpha = ringAlpha.value
}.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary),
)
// Check badge
Box(
modifier =
Modifier
.size(112.dp)
.graphicsLayer {
scaleX = badgeScale.value
scaleY = badgeScale.value
}.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary),
contentAlignment = Alignment.Center,
) {
Icon(
symbol = MaterialSymbols.Check,
contentDescription = null,
modifier = Modifier.size(60.dp),
tint = MaterialTheme.colorScheme.onPrimary,
)
}
}
Spacer(Modifier.height(32.dp))
Text(
text = stringRes(R.string.cashu_created_title),
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
modifier = Modifier.graphicsLayer { alpha = contentAlpha.value },
)
Spacer(Modifier.height(12.dp))
Text(
text = stringRes(R.string.cashu_created_subtitle),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier.graphicsLayer { alpha = contentAlpha.value },
)
Spacer(Modifier.height(40.dp))
Button(
onClick = {
// Replace this interstitial in the back stack so backing out
// of the mint picker doesn't return to the celebration.
nav.popUpTo(Route.CashuWalletMints, Route.CashuWalletCreated::class)
},
modifier =
Modifier
.fillMaxWidth()
.graphicsLayer { alpha = contentAlpha.value },
) {
Text(stringRes(R.string.cashu_created_pick_mints))
}
}
}
}
@@ -166,7 +166,7 @@ fun CashuWalletWizardScreen(
BusyState(stringRes(R.string.cashu_wizard_analyzing), null)
is WizardState.NoWallet ->
NoWalletContent(onCreate = { nav.nav(Route.CashuWalletMints) })
NoWalletContent(onCreate = { nav.nav(Route.CashuWalletCreated) })
is WizardState.Single ->
SingleContent(
+3
View File
@@ -2056,6 +2056,9 @@
<string name="cashu_wizard_adopting">Setting up your wallet…</string>
<string name="cashu_wizard_retry">Search again</string>
<string name="cashu_wizard_mints_label">Mints: %1$s</string>
<string name="cashu_created_title">Wallet Created</string>
<string name="cashu_created_subtitle">Now pick a few mints to host your sats.</string>
<string name="cashu_created_pick_mints">Pick mints</string>
<string name="cashu_balance">Balance</string>
<string name="cashu_mints">Mints</string>
<string name="cashu_mint_url">Mint URL</string>