mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-05 23:07:57 +00:00
ios, android: show the real end date and full price at badge checkout, and disable subscriptions
This commit is contained in:
@@ -141,11 +141,11 @@ final class BadgeStore: ObservableObject {
|
||||
return storePurchases.purchases
|
||||
}
|
||||
|
||||
func price(_ level: BadgeLevel, _ period: BadgePeriod) -> BadgePrice {
|
||||
func price(_ level: BadgeLevel, _ period: BadgePeriod, compact: Bool = true) -> BadgePrice {
|
||||
switch state {
|
||||
case .notLoaded, .loading: return .loading
|
||||
case .loaded, .failed:
|
||||
if let p = products[badgeProductId(level, period)] { return .price(compactPrice(p)) }
|
||||
if let p = products[badgeProductId(level, period)] { return .price(compact ? compactPrice(p) : p.displayPrice) }
|
||||
return .unavailable
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,15 +9,9 @@
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct BadgeUserPreview<Trailing: View>: View {
|
||||
struct BadgeUserPreview: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
let level: BadgeLevel
|
||||
let trailing: () -> Trailing
|
||||
|
||||
init(level: BadgeLevel, @ViewBuilder trailing: @escaping () -> Trailing = { EmptyView() }) {
|
||||
self.level = level
|
||||
self.trailing = trailing
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let user = chatModel.currentUser
|
||||
@@ -29,12 +23,9 @@ struct BadgeUserPreview<Trailing: View>: View {
|
||||
)
|
||||
return VStack(spacing: 12) {
|
||||
ProfileImage(imageStr: user?.image, size: 128)
|
||||
HStack(alignment: .center, spacing: 6) {
|
||||
NameWithBadge(Text(displayName).font(.largeTitle), previewBadge, .largeTitle)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.75)
|
||||
trailing()
|
||||
}
|
||||
NameWithBadge(Text(displayName).font(.largeTitle), previewBadge, .largeTitle)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.75)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,19 +30,19 @@ struct BadgesCheckOrderView: View {
|
||||
.multilineTextAlignment(.center)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
VStack(spacing: 12) {
|
||||
VStack(spacing: 0) {
|
||||
orderRow("Your badge", Text(level.title))
|
||||
orderRow("Duration", Text(period.label))
|
||||
orderRow("Total", period.priceText(store.price(level, period)))
|
||||
Divider()
|
||||
.padding(.top, 8)
|
||||
orderRow("Total", period.priceText(store.price(level, period, compact: false)), total: true)
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.vertical, 12)
|
||||
.background(Color(uiColor: .secondarySystemGroupedBackground))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
.padding(.top, 20)
|
||||
|
||||
Text("Charged by the App Store to the account on this device.")
|
||||
.font(.footnote)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Spacer(minLength: 20)
|
||||
|
||||
VStack(spacing: 10) {
|
||||
@@ -64,18 +64,22 @@ struct BadgesCheckOrderView: View {
|
||||
.alert(item: $alert) { $0.alert }
|
||||
}
|
||||
|
||||
private func orderRow(_ title: LocalizedStringKey, _ value: Text) -> some View {
|
||||
private func orderRow(_ title: LocalizedStringKey, _ value: Text, total: Bool = false) -> some View {
|
||||
HStack {
|
||||
Text(title)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.fontWeight(total ? .semibold : .medium)
|
||||
.foregroundColor(total ? nil : theme.colors.secondary)
|
||||
Spacer()
|
||||
value
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.font(.body)
|
||||
.font(total ? .title3 : .body)
|
||||
.padding(.top, total ? 14 : 8)
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
|
||||
private func payButton() -> some View {
|
||||
let price = store.price(level, period)
|
||||
let price = store.price(level, period, compact: false)
|
||||
let disabled = !price.canPurchase || purchasing || !store.canBuy(chatModel.currentUser?.userId)
|
||||
return Button {
|
||||
purchase()
|
||||
|
||||
@@ -23,10 +23,17 @@ enum BadgeLevel: String, CaseIterable, Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
var filesDescription: LocalizedStringKey {
|
||||
var fileSize: LocalizedStringKey {
|
||||
switch self {
|
||||
case .supporter: "Send 2GB files"
|
||||
case .legend: "Send 5GB files"
|
||||
case .supporter: "Files up to 2 GB"
|
||||
case .legend: "Files up to 5 GB"
|
||||
}
|
||||
}
|
||||
|
||||
var fileStorage: LocalizedStringKey {
|
||||
switch self {
|
||||
case .supporter: "Stored for 7 days"
|
||||
case .legend: "Stored for 21 days"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,12 +75,8 @@ struct BadgesChooseBadgeView: View {
|
||||
.multilineTextAlignment(.center)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
BadgeUserPreview(level: selectedLevel) {
|
||||
Image(systemName: "chevron.down")
|
||||
.font(.body)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
.padding(.top, 4)
|
||||
BadgeUserPreview(level: selectedLevel)
|
||||
.padding(.top, 4)
|
||||
|
||||
Spacer(minLength: 20)
|
||||
|
||||
@@ -127,9 +130,12 @@ struct BadgesChooseBadgeView: View {
|
||||
Text(level.title)
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
Text(level.filesDescription)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
VStack(spacing: 2) {
|
||||
Text(level.fileSize)
|
||||
Text(level.fileStorage)
|
||||
}
|
||||
.font(.subheadline)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
BadgePeriod.monthly.priceText(store.price(level, .monthly))
|
||||
.font(.body)
|
||||
.padding(.bottom, 20)
|
||||
|
||||
@@ -33,6 +33,14 @@ enum BadgePeriod: String, CaseIterable, Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
var months: Int {
|
||||
switch self {
|
||||
case .oneMonth: 1
|
||||
case .monthly: 1
|
||||
case .annual: 12
|
||||
}
|
||||
}
|
||||
|
||||
func priceText(_ price: BadgePrice) -> Text {
|
||||
switch price {
|
||||
case .loading: return Text(verbatim: "…")
|
||||
@@ -92,7 +100,7 @@ struct BadgesHowLongView: View {
|
||||
// fixedSize + maxHeight on the cards so they all match the tallest one -
|
||||
// only Annual carries a savings line, and prices wrap at large fonts
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
ForEach(badgePeriodsForSale) { periodCard($0) }
|
||||
ForEach(BadgePeriod.allCases) { periodCard($0) }
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
@@ -118,6 +126,7 @@ struct BadgesHowLongView: View {
|
||||
|
||||
private func periodCard(_ period: BadgePeriod) -> some View {
|
||||
let isSelected = period == selectedPeriod
|
||||
let forSale = badgePeriodsForSale.contains(period)
|
||||
return Button {
|
||||
selectedPeriod = period
|
||||
} label: {
|
||||
@@ -127,17 +136,19 @@ struct BadgesHowLongView: View {
|
||||
.scaledToFit()
|
||||
.frame(width: 32, height: 32)
|
||||
.foregroundColor(isSelected ? theme.colors.primary : theme.colors.secondary)
|
||||
.opacity(forSale ? 1 : 0.4)
|
||||
Text(period.label)
|
||||
.font(.title3)
|
||||
.fontWeight(.bold)
|
||||
period.priceText(store.price(level, period))
|
||||
.font(.body)
|
||||
period.priceText(store.price(level, period))
|
||||
.font(.title3)
|
||||
.fontWeight(.semibold)
|
||||
if let percent = savingsPercent(period) {
|
||||
Text("Save \(percent)%")
|
||||
.font(.footnote)
|
||||
.foregroundColor(isSelected ? theme.colors.primary : theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
.foregroundColor(forSale ? nil : theme.colors.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.vertical, 25)
|
||||
.padding(.horizontal, 12)
|
||||
@@ -150,6 +161,7 @@ struct BadgesHowLongView: View {
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(!forSale)
|
||||
}
|
||||
|
||||
private func savingsPercent(_ period: BadgePeriod) -> Int? {
|
||||
@@ -191,10 +203,10 @@ struct BadgeBillingFooter: View {
|
||||
}
|
||||
|
||||
private var billingFooter: LocalizedStringKey {
|
||||
// TODO [badges] source the actual date from the purchase state machine when wired.
|
||||
var comps = DateComponents(); comps.year = 2026; comps.month = 7; comps.day = 22
|
||||
let stubDate = Calendar.current.date(from: comps) ?? Date()
|
||||
let date = DateFormatter.localizedString(from: stubDate, dateStyle: .long, timeStyle: .none)
|
||||
// TODO [badges] from now only because a purchase is refused while a badge is held (refuseWhileBadgeHeld);
|
||||
// a top-up must count from the end of the existing balance
|
||||
let endDate = Calendar.current.date(byAdding: .month, value: period.months, to: Date()) ?? Date()
|
||||
let date = DateFormatter.localizedString(from: endDate, dateStyle: .long, timeStyle: .none)
|
||||
switch period {
|
||||
case .monthly, .annual: return "Renews on \(date). Cancel anytime."
|
||||
case .oneMonth: return "Ends on \(date)."
|
||||
|
||||
+2
-2
@@ -184,11 +184,11 @@ object BadgeStore {
|
||||
return if (rhId == chatModel.remoteHostId() && readFor == userId) purchases else emptyList()
|
||||
}
|
||||
|
||||
fun price(level: BadgeLevel, period: BadgePeriod): BadgePrice = when (state.value) {
|
||||
fun price(level: BadgeLevel, period: BadgePeriod, compact: Boolean = true): BadgePrice = when (state.value) {
|
||||
LoadState.NotLoaded, LoadState.Loading -> BadgePrice.Loading
|
||||
LoadState.Loaded, LoadState.Failed -> {
|
||||
val p = products.value[badgeStoreProductId(level, period)]
|
||||
if (p != null) BadgePrice.Price(compactPrice(p)) else BadgePrice.Unavailable
|
||||
if (p != null) BadgePrice.Price(if (compact) compactPrice(p) else p.displayPrice) else BadgePrice.Unavailable
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-11
@@ -19,7 +19,7 @@ import chat.simplex.res.MR
|
||||
import kotlinx.datetime.Instant
|
||||
|
||||
@Composable
|
||||
fun BadgeUserPreview(level: BadgeLevel, modifier: Modifier = Modifier, trailing: @Composable () -> Unit = {}) {
|
||||
fun BadgeUserPreview(level: BadgeLevel, modifier: Modifier = Modifier) {
|
||||
val user = chatModel.currentUser.value
|
||||
val displayName = user?.displayName ?: stringResource(MR.strings.badges_preview_my_nickname)
|
||||
val previewBadge = LocalBadge(
|
||||
@@ -29,15 +29,12 @@ fun BadgeUserPreview(level: BadgeLevel, modifier: Modifier = Modifier, trailing:
|
||||
)
|
||||
Column(modifier, horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
ProfileImage(size = 128.dp, image = user?.image)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
NameWithBadge(
|
||||
name = displayName,
|
||||
badge = previewBadge,
|
||||
style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
trailing()
|
||||
}
|
||||
NameWithBadge(
|
||||
name = displayName,
|
||||
badge = previewBadge,
|
||||
style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+24
-15
@@ -2,10 +2,13 @@ package chat.simplex.common.views.badges
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -39,20 +42,20 @@ fun BadgesCheckOrderView(level: BadgeLevel, period: BadgePeriod, modalManager: M
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Column(Modifier.fillMaxWidth().padding(top = 20.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 20.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(sectionCardColor())
|
||||
.padding(horizontal = 24.dp, vertical = 12.dp)
|
||||
) {
|
||||
OrderRow(MR.strings.badges_your_badge, stringResource(level.title))
|
||||
OrderRow(MR.strings.badges_order_duration, stringResource(period.label))
|
||||
OrderRow(MR.strings.badges_order_total, period.priceText(BadgeStore.price(level, period)))
|
||||
Divider(Modifier.padding(top = 8.dp))
|
||||
OrderRow(MR.strings.badges_order_total, period.priceText(BadgeStore.price(level, period, compact = false)), total = true)
|
||||
}
|
||||
|
||||
Text(
|
||||
stringResource(MR.strings.badges_charged_by_play),
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(Modifier.weight(1f).heightIn(min = 20.dp))
|
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
@@ -63,17 +66,23 @@ fun BadgesCheckOrderView(level: BadgeLevel, period: BadgePeriod, modalManager: M
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OrderRow(title: StringResource, value: String) {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Text(stringResource(title), style = MaterialTheme.typography.body1, color = MaterialTheme.colors.secondary)
|
||||
private fun OrderRow(title: StringResource, value: String, total: Boolean = false) {
|
||||
val style = if (total) MaterialTheme.typography.h3 else MaterialTheme.typography.body1
|
||||
Row(Modifier.fillMaxWidth().padding(top = if (total) 14.dp else 8.dp, bottom = 8.dp)) {
|
||||
Text(
|
||||
stringResource(title),
|
||||
style = style,
|
||||
fontWeight = if (total) FontWeight.SemiBold else FontWeight.Medium,
|
||||
color = if (total) Color.Unspecified else MaterialTheme.colors.secondary
|
||||
)
|
||||
Spacer(Modifier.weight(1f))
|
||||
Text(value, style = MaterialTheme.typography.body1)
|
||||
Text(value, style = style, fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PayButton(level: BadgeLevel, period: BadgePeriod, purchasing: MutableState<Boolean>) {
|
||||
val price = BadgeStore.price(level, period)
|
||||
val price = BadgeStore.price(level, period, compact = false)
|
||||
val (labelId, labelArg) = period.payLabel(price)
|
||||
OnboardingActionButton(
|
||||
modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp),
|
||||
|
||||
+14
-11
@@ -34,10 +34,16 @@ enum class BadgeLevel {
|
||||
Legend -> MR.strings.badges_level_legend
|
||||
}
|
||||
|
||||
val filesDescription: StringResource
|
||||
val fileSize: StringResource
|
||||
get() = when (this) {
|
||||
Supporter -> MR.strings.badges_level_supporter_files
|
||||
Legend -> MR.strings.badges_level_legend_files
|
||||
Supporter -> MR.strings.badges_level_supporter_file_size
|
||||
Legend -> MR.strings.badges_level_legend_file_size
|
||||
}
|
||||
|
||||
val fileStorage: StringResource
|
||||
get() = when (this) {
|
||||
Supporter -> MR.strings.badges_level_supporter_file_storage
|
||||
Legend -> MR.strings.badges_level_legend_file_storage
|
||||
}
|
||||
|
||||
val summary: StringResource
|
||||
@@ -83,13 +89,7 @@ fun BadgesChooseBadgeView(modalManager: ModalManager) {
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
BadgeUserPreview(level = selectedLevel, modifier = Modifier.padding(top = 4.dp)) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_keyboard_arrow_down),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
BadgeUserPreview(level = selectedLevel, modifier = Modifier.padding(top = 4.dp))
|
||||
|
||||
Spacer(Modifier.weight(1f).heightIn(min = 20.dp))
|
||||
|
||||
@@ -144,7 +144,10 @@ private fun LevelCard(level: BadgeLevel, selectedLevel: BadgeLevel, modifier: Mo
|
||||
modifier = Modifier.size(60.dp)
|
||||
)
|
||||
Text(stringResource(level.title), style = MaterialTheme.typography.h3, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center)
|
||||
Text(stringResource(level.filesDescription), style = MaterialTheme.typography.body2, color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center)
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(stringResource(level.fileSize), style = MaterialTheme.typography.body2, color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center)
|
||||
Text(stringResource(level.fileStorage), style = MaterialTheme.typography.body2, color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center)
|
||||
}
|
||||
Text(BadgePeriod.Monthly.priceText(BadgeStore.price(level, BadgePeriod.Monthly)), style = MaterialTheme.typography.body1, textAlign = TextAlign.Center)
|
||||
}
|
||||
}
|
||||
|
||||
+21
-9
@@ -9,7 +9,9 @@ import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -42,6 +44,13 @@ enum class BadgePeriod {
|
||||
Annual -> MR.strings.badges_period_annual
|
||||
}
|
||||
|
||||
val months: Int
|
||||
get() = when (this) {
|
||||
OneMonth -> 1
|
||||
Monthly -> 1
|
||||
Annual -> 12
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun priceText(price: BadgePrice): String = when (price) {
|
||||
is BadgePrice.Loading -> "…"
|
||||
@@ -104,7 +113,7 @@ fun BadgesHowLongView(level: BadgeLevel, modalManager: ModalManager) {
|
||||
Modifier.fillMaxWidth().height(IntrinsicSize.Max),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
badgePeriodsForSale.forEach { period ->
|
||||
BadgePeriod.entries.forEach { period ->
|
||||
PeriodCard(level, period, selectedPeriod, Modifier.weight(1f).fillMaxHeight()) { selectedPeriod = it }
|
||||
}
|
||||
}
|
||||
@@ -124,7 +133,7 @@ fun BadgesHowLongView(level: BadgeLevel, modalManager: ModalManager) {
|
||||
fun BadgeBillingFooter(period: BadgePeriod) {
|
||||
Box(Modifier.padding(top = 7.5.dp, bottom = 7.5.dp).padding(horizontal = 16.dp, vertical = 8.dp)) {
|
||||
Text(
|
||||
stringResource(billingFooter(period)).format(stubBillingDate()),
|
||||
stringResource(billingFooter(period)).format(billingDate(period)),
|
||||
Modifier.padding(vertical = 5.dp),
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.secondary,
|
||||
@@ -136,6 +145,8 @@ fun BadgeBillingFooter(period: BadgePeriod) {
|
||||
@Composable
|
||||
private fun PeriodCard(level: BadgeLevel, period: BadgePeriod, selectedPeriod: BadgePeriod, modifier: Modifier, onSelect: (BadgePeriod) -> Unit) {
|
||||
val isSelected = period == selectedPeriod
|
||||
val forSale = period in badgePeriodsForSale
|
||||
val textColor = if (forSale) Color.Unspecified else MaterialTheme.colors.secondary
|
||||
val borderColor = if (isSelected) MaterialTheme.colors.primary else MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.92f)
|
||||
// Light: transparent so card matches page background. Dark: subtle gray tint for visible contrast.
|
||||
val cardBackground = if (isInDarkTheme()) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f)
|
||||
@@ -146,7 +157,7 @@ private fun PeriodCard(level: BadgeLevel, period: BadgePeriod, selectedPeriod: B
|
||||
.clip(shape)
|
||||
.background(cardBackground, shape)
|
||||
.border(2.dp, borderColor, shape)
|
||||
.clickable { onSelect(period) }
|
||||
.clickable(enabled = forSale) { onSelect(period) }
|
||||
.padding(vertical = 20.dp, horizontal = 12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
@@ -155,10 +166,10 @@ private fun PeriodCard(level: BadgeLevel, period: BadgePeriod, selectedPeriod: B
|
||||
painterResource(period.icon),
|
||||
contentDescription = null,
|
||||
tint = if (isSelected) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
||||
modifier = Modifier.size(32.dp)
|
||||
modifier = Modifier.size(32.dp).alpha(if (forSale) 1f else 0.4f)
|
||||
)
|
||||
Text(stringResource(period.label), style = MaterialTheme.typography.h3, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center)
|
||||
Text(period.priceText(BadgeStore.price(level, period)), style = MaterialTheme.typography.body1, textAlign = TextAlign.Center)
|
||||
Text(stringResource(period.label), style = MaterialTheme.typography.body1, color = textColor, textAlign = TextAlign.Center)
|
||||
Text(period.priceText(BadgeStore.price(level, period)), style = MaterialTheme.typography.h3, fontWeight = FontWeight.SemiBold, color = textColor, textAlign = TextAlign.Center)
|
||||
val percent = savingsPercent(level, period)
|
||||
if (percent != null) {
|
||||
Text(
|
||||
@@ -191,9 +202,10 @@ private fun billingFooter(period: BadgePeriod): StringResource = when (period) {
|
||||
BadgePeriod.OneMonth -> MR.strings.badges_billing_footer_one_month
|
||||
}
|
||||
|
||||
// TODO [badges] source the actual date from the purchase state machine when wired.
|
||||
private fun stubBillingDate(): String {
|
||||
val date = java.time.LocalDate.of(2026, 7, 22)
|
||||
// TODO [badges] from now only because a purchase is refused while a badge is held (refuseWhileBadgeHeld);
|
||||
// a top-up must count from the end of the existing balance
|
||||
private fun billingDate(period: BadgePeriod): String {
|
||||
val date = java.time.LocalDate.now().plusMonths(period.months.toLong())
|
||||
val formatter = java.time.format.DateTimeFormatter.ofLocalizedDate(java.time.format.FormatStyle.LONG)
|
||||
return date.format(formatter)
|
||||
}
|
||||
|
||||
@@ -3142,8 +3142,10 @@
|
||||
<string name="badge_unknown_key_desc">The badge is signed with a key that this version of the app does not recognize. Update the app to verify this badge.</string>
|
||||
<string name="badges_level_supporter">Supporter</string>
|
||||
<string name="badges_level_legend">Legend</string>
|
||||
<string name="badges_level_supporter_files">Send 2GB files</string>
|
||||
<string name="badges_level_legend_files">Send 5GB files</string>
|
||||
<string name="badges_level_supporter_file_size">Files up to 2 GB</string>
|
||||
<string name="badges_level_supporter_file_storage">Stored for 7 days</string>
|
||||
<string name="badges_level_legend_file_size">Files up to 5 GB</string>
|
||||
<string name="badges_level_legend_file_storage">Stored for 21 days</string>
|
||||
<string name="badges_price_monthly">%1$s/month</string>
|
||||
<string name="badges_price_annual">%1$s/year</string>
|
||||
<string name="badges_pay_monthly">Pay %1$s/month</string>
|
||||
@@ -3184,7 +3186,6 @@
|
||||
<string name="badges_check_your_order_title">Check your order</string>
|
||||
<string name="badges_order_duration">Duration</string>
|
||||
<string name="badges_order_total">Total</string>
|
||||
<string name="badges_charged_by_play">Charged by Google Play to the account on this device.</string>
|
||||
<string name="badges_issuing_title">Issuing your badge</string>
|
||||
<string name="badges_issuing_body">The store has taken the payment. Keeps trying if you close the app.</string>
|
||||
<string name="badges_waiting_for_approval_title">Waiting for approval</string>
|
||||
|
||||
Reference in New Issue
Block a user