diff --git a/app/src/main/kotlin/net/primal/android/premium/legend/become/BecomeLegendBottomBarButton.kt b/app/src/main/kotlin/net/primal/android/premium/legend/become/BecomeLegendBottomBarButton.kt index dd337be1..8d4eb4f2 100644 --- a/app/src/main/kotlin/net/primal/android/premium/legend/become/BecomeLegendBottomBarButton.kt +++ b/app/src/main/kotlin/net/primal/android/premium/legend/become/BecomeLegendBottomBarButton.kt @@ -11,7 +11,11 @@ import androidx.compose.ui.unit.dp import net.primal.android.core.compose.button.PrimalLoadingButton @Composable -fun BecomeLegendBottomBarButton(text: String, onClick: () -> Unit) { +fun BecomeLegendBottomBarButton( + text: String, + onClick: () -> Unit, + enabled: Boolean = true, +) { Box( modifier = Modifier .fillMaxWidth() @@ -24,6 +28,7 @@ fun BecomeLegendBottomBarButton(text: String, onClick: () -> Unit) { modifier = Modifier.fillMaxWidth(), text = text, onClick = onClick, + enabled = enabled, ) } } diff --git a/app/src/main/kotlin/net/primal/android/premium/legend/become/PremiumBecomeLegendContract.kt b/app/src/main/kotlin/net/primal/android/premium/legend/become/PremiumBecomeLegendContract.kt index bf55a2bf..d1bbb02e 100644 --- a/app/src/main/kotlin/net/primal/android/premium/legend/become/PremiumBecomeLegendContract.kt +++ b/app/src/main/kotlin/net/primal/android/premium/legend/become/PremiumBecomeLegendContract.kt @@ -2,18 +2,17 @@ package net.primal.android.premium.legend.become import java.math.BigDecimal import net.primal.android.attachments.domain.CdnImage -import net.primal.android.premium.domain.PremiumMembership class PremiumBecomeLegendContract { data class UiState( val isPremiumBadgeOrigin: Boolean = false, val stage: BecomeLegendStage = BecomeLegendStage.Intro, - val displayName: String = "", val avatarCdnImage: CdnImage? = null, - val profileNostrAddress: String? = null, - val profileLightningAddress: String? = null, - val membership: PremiumMembership? = null, + val userHandle: String? = null, + val isPremiumUser: Boolean = false, + val primalName: String? = null, + val isFetchingPaymentInstructions: Boolean = false, val minLegendThresholdInBtc: BigDecimal = BigDecimal.ZERO, val maxLegendThresholdInBtc: BigDecimal = BigDecimal.ONE, val exchangeBtcUsdRate: Double? = null, @@ -21,19 +20,29 @@ class PremiumBecomeLegendContract { val bitcoinAddress: String? = null, val qrCodeValue: String? = null, val membershipQuoteId: String? = null, - ) + ) { + fun arePaymentInstructionsAvailable() = + this.minLegendThresholdInBtc != BigDecimal.ZERO && + this.selectedAmountInBtc != BigDecimal.ONE && + this.bitcoinAddress != null && + this.membershipQuoteId != null + } sealed class UiEvent { data object ShowAmountEditor : UiEvent() data object GoBackToIntro : UiEvent() + data object GoToFindPrimalNameStage : UiEvent() + data class PrimalNamePicked(val primalName: String) : UiEvent() data object ShowPaymentInstructions : UiEvent() data class UpdateSelectedAmount(val newAmount: Float) : UiEvent() data object StartPurchaseMonitor : UiEvent() data object StopPurchaseMonitor : UiEvent() + data object FetchPaymentInstructions : UiEvent() } enum class BecomeLegendStage { Intro, + PickPrimalName, PickAmount, Payment, Success, diff --git a/app/src/main/kotlin/net/primal/android/premium/legend/become/PremiumBecomeLegendScreen.kt b/app/src/main/kotlin/net/primal/android/premium/legend/become/PremiumBecomeLegendScreen.kt index 0c823029..5b6109f7 100644 --- a/app/src/main/kotlin/net/primal/android/premium/legend/become/PremiumBecomeLegendScreen.kt +++ b/app/src/main/kotlin/net/primal/android/premium/legend/become/PremiumBecomeLegendScreen.kt @@ -12,9 +12,13 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.lifecycle.Lifecycle +import net.primal.android.R import net.primal.android.core.compose.runtime.DisposableLifecycleObserverEffect +import net.primal.android.premium.buying.name.PremiumPrimalNameStage import net.primal.android.premium.legend.become.PremiumBecomeLegendContract.BecomeLegendStage +import net.primal.android.premium.legend.become.PremiumBecomeLegendContract.UiState import net.primal.android.premium.legend.become.amount.BecomeLegendAmountStage import net.primal.android.premium.legend.become.intro.BecomeLegendIntroStage import net.primal.android.premium.legend.become.payment.BecomeLegendPaymentStage @@ -48,7 +52,7 @@ fun PremiumBecomeLegendScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable private fun PremiumBecomeLegendScreen( - state: PremiumBecomeLegendContract.UiState, + state: UiState, eventPublisher: (PremiumBecomeLegendContract.UiEvent) -> Unit, onClose: () -> Unit, onLegendPurchased: () -> Unit, @@ -58,6 +62,7 @@ private fun PremiumBecomeLegendScreen( eventPublisher = eventPublisher, onClose = onClose, onLegendPurchased = onLegendPurchased, + isPremiumUser = state.isPremiumUser, ) AnimatedContent( @@ -74,7 +79,25 @@ private fun PremiumBecomeLegendScreen( modifier = Modifier.fillMaxSize(), isPremiumBadgeOrigin = state.isPremiumBadgeOrigin, onClose = onClose, - onNext = { eventPublisher(PremiumBecomeLegendContract.UiEvent.ShowAmountEditor) }, + onNext = { + if (state.isPremiumUser) { + eventPublisher(PremiumBecomeLegendContract.UiEvent.ShowAmountEditor) + } else { + eventPublisher(PremiumBecomeLegendContract.UiEvent.GoToFindPrimalNameStage) + } + }, + ) + } + + BecomeLegendStage.PickPrimalName -> { + PremiumPrimalNameStage( + titleText = stringResource(R.string.premium_primal_name_title), + onBack = { eventPublisher(PremiumBecomeLegendContract.UiEvent.GoBackToIntro) }, + onPrimalNameAvailable = { + eventPublisher(PremiumBecomeLegendContract.UiEvent.PrimalNamePicked(it)) + eventPublisher(PremiumBecomeLegendContract.UiEvent.ShowAmountEditor) + }, + initialName = state.primalName ?: state.userHandle?.takeIf { !it.contains(" ") }, ) } @@ -83,7 +106,13 @@ private fun PremiumBecomeLegendScreen( modifier = Modifier.fillMaxSize(), state = state, eventPublisher = eventPublisher, - onClose = { eventPublisher(PremiumBecomeLegendContract.UiEvent.GoBackToIntro) }, + onClose = { + if (state.isPremiumUser) { + eventPublisher(PremiumBecomeLegendContract.UiEvent.GoBackToIntro) + } else { + eventPublisher(PremiumBecomeLegendContract.UiEvent.GoToFindPrimalNameStage) + } + }, onNext = { eventPublisher(PremiumBecomeLegendContract.UiEvent.ShowPaymentInstructions) }, ) } @@ -109,6 +138,7 @@ private fun PremiumBecomeLegendScreen( @Composable private fun BecomeLegendBackHandler( stage: BecomeLegendStage, + isPremiumUser: Boolean, onClose: () -> Unit, onLegendPurchased: () -> Unit, eventPublisher: (PremiumBecomeLegendContract.UiEvent) -> Unit, @@ -117,7 +147,15 @@ private fun BecomeLegendBackHandler( when (stage) { BecomeLegendStage.Intro -> onClose() - BecomeLegendStage.PickAmount -> eventPublisher(PremiumBecomeLegendContract.UiEvent.GoBackToIntro) + BecomeLegendStage.PickPrimalName -> eventPublisher(PremiumBecomeLegendContract.UiEvent.GoBackToIntro) + + BecomeLegendStage.PickAmount -> { + if (isPremiumUser) { + eventPublisher(PremiumBecomeLegendContract.UiEvent.GoBackToIntro) + } else { + eventPublisher(PremiumBecomeLegendContract.UiEvent.GoToFindPrimalNameStage) + } + } BecomeLegendStage.Payment -> eventPublisher(PremiumBecomeLegendContract.UiEvent.ShowAmountEditor) @@ -133,7 +171,7 @@ private fun AnimatedContentTransitionScope.transitionSpecBetw .togetherWith(slideOutHorizontally(targetOffsetX = { -it })) } - BecomeLegendStage.PickAmount -> { + BecomeLegendStage.PickPrimalName -> { when (targetState) { BecomeLegendStage.Intro -> { slideInHorizontally(initialOffsetX = { -it }) @@ -147,6 +185,20 @@ private fun AnimatedContentTransitionScope.transitionSpecBetw } } + BecomeLegendStage.PickAmount -> { + when (targetState) { + BecomeLegendStage.PickPrimalName, BecomeLegendStage.Intro -> { + slideInHorizontally(initialOffsetX = { -it }) + .togetherWith(slideOutHorizontally(targetOffsetX = { it })) + } + + else -> { + slideInHorizontally(initialOffsetX = { it }) + .togetherWith(slideOutHorizontally(targetOffsetX = { -it })) + } + } + } + BecomeLegendStage.Payment -> { when (targetState) { BecomeLegendStage.Success -> { diff --git a/app/src/main/kotlin/net/primal/android/premium/legend/become/PremiumBecomeLegendViewModel.kt b/app/src/main/kotlin/net/primal/android/premium/legend/become/PremiumBecomeLegendViewModel.kt index abd68e3f..8035e689 100644 --- a/app/src/main/kotlin/net/primal/android/premium/legend/become/PremiumBecomeLegendViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/premium/legend/become/PremiumBecomeLegendViewModel.kt @@ -68,17 +68,22 @@ class PremiumBecomeLegendViewModel @Inject constructor( observeEvents() observeActiveAccount() fetchExchangeRate() - fetchLegendPaymentInstructions() } private fun observeEvents() = viewModelScope.launch { events.collect { when (it) { + UiEvent.GoToFindPrimalNameStage -> setState { + copy(stage = PremiumBecomeLegendContract.BecomeLegendStage.PickPrimalName) + } + UiEvent.ShowAmountEditor -> { - if (_state.value.minLegendThresholdInBtc != BigDecimal.ONE) { - setState { copy(stage = PremiumBecomeLegendContract.BecomeLegendStage.PickAmount) } + val state = _state.value + if (!state.arePaymentInstructionsAvailable() && state.primalName != null) { + fetchLegendPaymentInstructions(primalName = state.primalName) } + setState { copy(stage = PremiumBecomeLegendContract.BecomeLegendStage.PickAmount) } } UiEvent.GoBackToIntro -> setState { @@ -90,35 +95,44 @@ class PremiumBecomeLegendViewModel @Inject constructor( } is UiEvent.UpdateSelectedAmount -> { - val newAmountInBtc = it.newAmount.toBigDecimal().setScale( - BTC_DECIMAL_PLACES, - RoundingMode.HALF_UP, + updatePaymentAmount( + amount = it.newAmount.toBigDecimal().setScale(BTC_DECIMAL_PLACES, RoundingMode.HALF_UP), ) - setState { - copy( - selectedAmountInBtc = newAmountInBtc, - qrCodeValue = "bitcoin:${this.bitcoinAddress}?amount=$newAmountInBtc", - ) - } } UiEvent.StartPurchaseMonitor -> startPurchaseMonitorIfStopped() UiEvent.StopPurchaseMonitor -> stopPurchaseMonitor() + + is UiEvent.PrimalNamePicked -> setState { copy(primalName = it.primalName) } + + UiEvent.FetchPaymentInstructions -> { + _state.value.primalName?.let { primalName -> + fetchLegendPaymentInstructions(primalName = primalName) + } + } } } } + private fun updatePaymentAmount(amount: BigDecimal) { + setState { + copy( + selectedAmountInBtc = amount, + qrCodeValue = "bitcoin:${this.bitcoinAddress}?amount=$amount", + ) + } + } + private fun observeActiveAccount() = viewModelScope.launch { activeAccountStore.activeUserAccount.collect { setState { copy( - displayName = it.authorDisplayName, avatarCdnImage = it.avatarCdnImage, - profileNostrAddress = it.internetIdentifier, - profileLightningAddress = it.lightningAddress, - membership = it.premiumMembership, + userHandle = it.userDisplayName, + isPremiumUser = it.premiumMembership?.isExpired() == false, + primalName = it.premiumMembership?.premiumName, ) } } @@ -151,31 +165,33 @@ class PremiumBecomeLegendViewModel @Inject constructor( } } - private fun fetchLegendPaymentInstructions() { - _state.value.membership?.premiumName?.let { primalName -> - viewModelScope.launch { - try { - val response = premiumRepository.fetchPrimalLegendPaymentInstructions( - userId = activeAccountStore.activeUserId(), - primalName = primalName, - ) - - setState { - copy( - minLegendThresholdInBtc = response.amountBtc.toBigDecimal(), - selectedAmountInBtc = response.amountBtc.toBigDecimal(), - bitcoinAddress = response.qrCode.parseBitcoinPaymentInstructions()?.address, - membershipQuoteId = response.membershipQuoteId, - ) - } + private fun fetchLegendPaymentInstructions(primalName: String) = + viewModelScope.launch { + try { + setState { copy(isFetchingPaymentInstructions = true) } + val response = premiumRepository.fetchPrimalLegendPaymentInstructions( + userId = activeAccountStore.activeUserId(), + primalName = primalName, + ) - startPurchaseMonitorIfStopped() - } catch (error: WssException) { - Timber.e(error) + val minAmount = response.amountBtc.toBigDecimal().setScale(BTC_DECIMAL_PLACES, RoundingMode.HALF_UP) + setState { + copy( + minLegendThresholdInBtc = minAmount, + selectedAmountInBtc = minAmount, + bitcoinAddress = response.qrCode.parseBitcoinPaymentInstructions()?.address, + membershipQuoteId = response.membershipQuoteId, + ) } + updatePaymentAmount(amount = minAmount) + + startPurchaseMonitorIfStopped() + } catch (error: WssException) { + Timber.e(error) + } finally { + setState { copy(isFetchingPaymentInstructions = false) } } } - } private fun subscribeToPurchaseMonitor(quoteId: String) = PrimalSocketSubscription.launch( diff --git a/app/src/main/kotlin/net/primal/android/premium/legend/become/PrimalLegendAmount.kt b/app/src/main/kotlin/net/primal/android/premium/legend/become/PrimalLegendAmount.kt index 45536e09..5c69a44f 100644 --- a/app/src/main/kotlin/net/primal/android/premium/legend/become/PrimalLegendAmount.kt +++ b/app/src/main/kotlin/net/primal/android/premium/legend/become/PrimalLegendAmount.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import java.math.BigDecimal @@ -21,7 +22,9 @@ fun PrimalLegendAmount(btcValue: BigDecimal, exchangeBtcUsdRate: Double?) { horizontalAlignment = Alignment.CenterHorizontally, ) { MainAmountText( - modifier = Modifier.padding(start = 32.dp), + modifier = Modifier + .padding(start = 32.dp) + .alpha(alpha = if (btcValue == BigDecimal.ZERO) 0.5f else 1.0f), amount = String.format(Locale.US, "%.8f", btcValue), currency = "BTC", textSize = 44.sp, diff --git a/app/src/main/kotlin/net/primal/android/premium/legend/become/amount/BecomeLegendAmountStage.kt b/app/src/main/kotlin/net/primal/android/premium/legend/become/amount/BecomeLegendAmountStage.kt index 050d3435..6ba493fd 100644 --- a/app/src/main/kotlin/net/primal/android/premium/legend/become/amount/BecomeLegendAmountStage.kt +++ b/app/src/main/kotlin/net/primal/android/premium/legend/become/amount/BecomeLegendAmountStage.kt @@ -16,6 +16,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Slider import androidx.compose.material3.SliderDefaults import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf @@ -30,6 +31,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import java.time.Year import net.primal.android.R import net.primal.android.core.compose.NostrUserText import net.primal.android.core.compose.PrimalSliderThumb @@ -41,6 +43,7 @@ import net.primal.android.premium.legend.LegendaryCustomization import net.primal.android.premium.legend.LegendaryStyle import net.primal.android.premium.legend.become.BecomeLegendBottomBarButton import net.primal.android.premium.legend.become.PremiumBecomeLegendContract +import net.primal.android.premium.legend.become.PremiumBecomeLegendContract.UiState import net.primal.android.premium.legend.become.PrimalLegendAmount import net.primal.android.premium.ui.PremiumBadge import net.primal.android.theme.AppTheme @@ -49,7 +52,7 @@ import net.primal.android.theme.AppTheme @Composable fun BecomeLegendAmountStage( modifier: Modifier, - state: PremiumBecomeLegendContract.UiState, + state: UiState, eventPublisher: (PremiumBecomeLegendContract.UiEvent) -> Unit, onClose: () -> Unit, onNext: () -> Unit, @@ -68,6 +71,7 @@ fun BecomeLegendAmountStage( BecomeLegendBottomBarButton( text = stringResource(R.string.premium_become_legend_button_pay_now), onClick = onNext, + enabled = state.arePaymentInstructionsAvailable(), ) }, ) { paddingValues -> @@ -94,87 +98,126 @@ fun BecomeLegendAmountStage( Spacer(modifier = Modifier.height(16.dp)) NostrUserText( modifier = Modifier.padding(start = 8.dp), - displayName = state.displayName, - internetIdentifier = state.profileNostrAddress, + displayName = state.primalName ?: "", + internetIdentifier = "${state.primalName}@primal.net", internetIdentifierBadgeSize = 24.dp, fontSize = 20.sp, customBadgeStyle = LegendaryStyle.GOLD, ) } - state.membership?.let { - PremiumBadge( - firstCohort = "Legend", - secondCohort = it.cohort2, - membershipExpired = it.isExpired(), - legendaryStyle = LegendaryStyle.GOLD, + PremiumBadge( + firstCohort = "Legend", + secondCohort = Year.now().value.toString(), + membershipExpired = false, + legendaryStyle = LegendaryStyle.GOLD, + ) + + Spacer(modifier = Modifier.height(48.dp)) + + if (state.arePaymentInstructionsAvailable() || state.isFetchingPaymentInstructions) { + SelectAmountSlider( + state = state, + eventPublisher = eventPublisher, + ) + } else { + NoPaymentInstructionsColumn( + onRetryClick = { eventPublisher(PremiumBecomeLegendContract.UiEvent.FetchPaymentInstructions) }, ) } Spacer(modifier = Modifier.height(48.dp)) + } + } +} - var slideValue by remember { mutableFloatStateOf(state.selectedAmountInBtc.toFloat()) } - - PrimalLegendAmount( - btcValue = state.selectedAmountInBtc, - exchangeBtcUsdRate = state.exchangeBtcUsdRate, +@Composable +fun NoPaymentInstructionsColumn(onRetryClick: () -> Unit) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text( + modifier = Modifier.padding(horizontal = 20.dp), + text = stringResource(R.string.premium_become_legend_failed_fetching_payment_instructions), + textAlign = TextAlign.Center, + style = AppTheme.typography.bodyMedium, + color = AppTheme.extraColorScheme.onSurfaceVariantAlt2, + ) + TextButton( + onClick = onRetryClick, + ) { + Text( + text = stringResource(R.string.premium_become_legend_retry_button), + style = AppTheme.typography.bodyLarge, + color = AppTheme.colorScheme.primary, ) + } + } +} - Column( - modifier = Modifier.padding(horizontal = 10.dp), - ) { - val sliderColors = sliderColors(value = slideValue.toInt()) - val interactionSource = remember { MutableInteractionSource() } - Slider( - modifier = Modifier.fillMaxWidth(), +@ExperimentalMaterial3Api +@Composable +private fun SelectAmountSlider(state: UiState, eventPublisher: (PremiumBecomeLegendContract.UiEvent) -> Unit) { + var slideValue by remember { mutableFloatStateOf(state.selectedAmountInBtc.toFloat()) } + + PrimalLegendAmount( + btcValue = state.selectedAmountInBtc, + exchangeBtcUsdRate = state.exchangeBtcUsdRate, + ) + + Column( + modifier = Modifier.padding(horizontal = 10.dp), + ) { + val sliderColors = sliderColors(value = slideValue.toInt()) + val interactionSource = remember { MutableInteractionSource() } + Slider( + modifier = Modifier.fillMaxWidth(), + interactionSource = interactionSource, + colors = sliderColors, + track = { + SliderDefaults.Track( + sliderState = it, + modifier = Modifier.scale(scaleX = 1f, scaleY = 0.35f), + colors = sliderColors, + drawStopIndicator = null, + drawTick = { _, _ -> }, + thumbTrackGapSize = 0.dp, + ) + }, + thumb = { + PrimalSliderThumb( interactionSource = interactionSource, colors = sliderColors, - track = { - SliderDefaults.Track( - sliderState = it, - modifier = Modifier.scale(scaleX = 1f, scaleY = 0.35f), - colors = sliderColors, - drawStopIndicator = null, - drawTick = { _, _ -> }, - thumbTrackGapSize = 0.dp, - ) - }, - thumb = { - PrimalSliderThumb( - interactionSource = interactionSource, - colors = sliderColors, - ) - }, - value = slideValue, - onValueChange = { - slideValue = it - eventPublisher(PremiumBecomeLegendContract.UiEvent.UpdateSelectedAmount(newAmount = it)) - }, - steps = (state.maxLegendThresholdInBtc - state.minLegendThresholdInBtc).toInt(), - valueRange = state.minLegendThresholdInBtc.toFloat()..state.maxLegendThresholdInBtc.toFloat(), ) + }, + value = slideValue, + onValueChange = { + slideValue = it + eventPublisher(PremiumBecomeLegendContract.UiEvent.UpdateSelectedAmount(newAmount = it)) + }, + steps = (state.maxLegendThresholdInBtc - state.minLegendThresholdInBtc).toInt(), + valueRange = state.minLegendThresholdInBtc.toFloat()..state.maxLegendThresholdInBtc.toFloat(), + ) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = "$1000", - style = AppTheme.typography.bodyMedium, - color = AppTheme.extraColorScheme.onSurfaceVariantAlt3, - ) - - Text( - text = "1 BTC", - style = AppTheme.typography.bodyMedium, - color = AppTheme.extraColorScheme.onSurfaceVariantAlt3, - ) - } - } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = "$1000", + style = AppTheme.typography.bodyMedium, + color = AppTheme.extraColorScheme.onSurfaceVariantAlt3, + ) - Spacer(modifier = Modifier.height(48.dp)) + Text( + text = "1 BTC", + style = AppTheme.typography.bodyMedium, + color = AppTheme.extraColorScheme.onSurfaceVariantAlt3, + ) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 35873a80..2aa0fd0d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -198,6 +198,8 @@ Success, payment received! You are now a Primal Legend Done + Unable to get payment instructions. Please try again. + Retry Manage Premium Nostr Tools