Skip to content

Commit

Permalink
Implement becoming Primal Legend without prior premium membership (#244)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Aleksandar Ilic <[email protected]>
  • Loading branch information
markocic and AleksandarIlic authored Dec 7, 2024
1 parent 28ca6eb commit 0fed8e9
Show file tree
Hide file tree
Showing 7 changed files with 244 additions and 114 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -24,6 +28,7 @@ fun BecomeLegendBottomBarButton(text: String, onClick: () -> Unit) {
modifier = Modifier.fillMaxWidth(),
text = text,
onClick = onClick,
enabled = enabled,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,47 @@ 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,
val selectedAmountInBtc: BigDecimal = minLegendThresholdInBtc,
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -58,6 +62,7 @@ private fun PremiumBecomeLegendScreen(
eventPublisher = eventPublisher,
onClose = onClose,
onLegendPurchased = onLegendPurchased,
isPremiumUser = state.isPremiumUser,
)

AnimatedContent(
Expand All @@ -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(" ") },
)
}

Expand All @@ -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) },
)
}
Expand All @@ -109,6 +138,7 @@ private fun PremiumBecomeLegendScreen(
@Composable
private fun BecomeLegendBackHandler(
stage: BecomeLegendStage,
isPremiumUser: Boolean,
onClose: () -> Unit,
onLegendPurchased: () -> Unit,
eventPublisher: (PremiumBecomeLegendContract.UiEvent) -> Unit,
Expand All @@ -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)

Expand All @@ -133,7 +171,7 @@ private fun AnimatedContentTransitionScope<BecomeLegendStage>.transitionSpecBetw
.togetherWith(slideOutHorizontally(targetOffsetX = { -it }))
}

BecomeLegendStage.PickAmount -> {
BecomeLegendStage.PickPrimalName -> {
when (targetState) {
BecomeLegendStage.Intro -> {
slideInHorizontally(initialOffsetX = { -it })
Expand All @@ -147,6 +185,20 @@ private fun AnimatedContentTransitionScope<BecomeLegendStage>.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 -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
)
}
}
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
Loading

0 comments on commit 0fed8e9

Please sign in to comment.