From c5cc01bb9431c62decb99ccfe2faafefb66e71f9 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 31 Jul 2026 13:27:29 +0400 Subject: [PATCH] kotlin --- .../Badges/BadgesSupportSimplexView.swift | 2 +- .../Shared/Views/Onboarding/HowItWorks.swift | 2 + .../chat/simplex/common/model/SimpleXAPI.kt | 3 + .../common/views/badges/BadgeUserPreview.kt | 44 +++++ .../views/badges/BadgesHowItWorksView.kt | 35 ++++ .../common/views/badges/BadgesPayView.kt | 136 +++++++++++++ .../views/badges/BadgesRedeemCodeView.kt | 30 +++ .../views/badges/BadgesSupportSimplexView.kt | 153 +++++++++++++++ .../views/badges/BadgesYourLevelView.kt | 179 ++++++++++++++++++ .../views/badges/SupportSimpleXBanner.kt | 153 +++++++++++++++ .../common/views/chatlist/ChatListView.kt | 14 ++ .../common/views/onboarding/HowItWorks.kt | 8 +- .../common/views/onboarding/WhatsNewView.kt | 48 +++++ .../common/views/usersettings/SettingsView.kt | 10 + .../commonMain/resources/MR/base/strings.xml | 38 ++++ .../default/MR/images/phone_supporter.svg | 4 + .../MR/images/phone_supporter_light.svg | 4 + 17 files changed, 860 insertions(+), 3 deletions(-) create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgeUserPreview.kt create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesHowItWorksView.kt create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesPayView.kt create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesRedeemCodeView.kt create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesSupportSimplexView.kt create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesYourLevelView.kt create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/SupportSimpleXBanner.kt create mode 100644 apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/phone_supporter.svg create mode 100644 apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/phone_supporter_light.svg diff --git a/apps/ios/Shared/Views/Badges/BadgesSupportSimplexView.swift b/apps/ios/Shared/Views/Badges/BadgesSupportSimplexView.swift index fe3fa16c7a..c64635c707 100644 --- a/apps/ios/Shared/Views/Badges/BadgesSupportSimplexView.swift +++ b/apps/ios/Shared/Views/Badges/BadgesSupportSimplexView.swift @@ -64,7 +64,7 @@ struct BadgesSupportSimplexView: View { .modifier(ThemedBackground()) .navigationBarHidden(true) .sheet(isPresented: $showWhySimpleX) { - WhySimpleX(onboarding: false, createProfileNavLinkActive: .constant(false)) + WhySimpleX(onboarding: false, titleColor: theme.colors.primary, createProfileNavLinkActive: .constant(false)) } } diff --git a/apps/ios/Shared/Views/Onboarding/HowItWorks.swift b/apps/ios/Shared/Views/Onboarding/HowItWorks.swift index e9b9c6b970..dd01534dfb 100644 --- a/apps/ios/Shared/Views/Onboarding/HowItWorks.swift +++ b/apps/ios/Shared/Views/Onboarding/HowItWorks.swift @@ -65,6 +65,7 @@ struct WhySimpleX: View { @Environment(\.dismiss) var dismiss: DismissAction @EnvironmentObject var m: ChatModel var onboarding: Bool + var titleColor: Color? = nil @Binding var createProfileNavLinkActive: Bool var body: some View { @@ -74,6 +75,7 @@ struct WhySimpleX: View { Text("You were born without an account") .font(.title) .bold() + .foregroundColor(titleColor) .padding(.top) Text("Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life.") Text("Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible.") diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index f9438fca32..bbbe242d70 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -192,6 +192,7 @@ class AppPreferences { val showHiddenProfilesNotice = mkBoolPreference(SHARED_PREFS_SHOW_HIDDEN_PROFILES_NOTICE, true) val oneHandUICardShown = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI_CARD_SHOWN, false) val addressCreationCardShown = mkBoolPreference(SHARED_PREFS_ADDRESS_CREATION_CARD_SHOWN, false) + val supporterBannerShown = mkBoolPreference(SHARED_PREFS_SUPPORTER_BANNER_SHOWN, false) val showMuteProfileAlert = mkBoolPreference(SHARED_PREFS_SHOW_MUTE_PROFILE_ALERT, true) val showReportsInSupportChatAlert = mkBoolPreference(SHARED_PREFS_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT, true) val appLanguage = mkStrPreference(SHARED_PREFS_APP_LANGUAGE, null) @@ -273,6 +274,7 @@ class AppPreferences { hintPref(laNoticeShown, false), hintPref(oneHandUICardShown, false), hintPref(addressCreationCardShown, false), + hintPref(supporterBannerShown, false), hintPref(liveMessageAlertShown, false), hintPref(signMessageAlertShown, false), hintPref(showHiddenProfilesNotice, true), @@ -464,6 +466,7 @@ class AppPreferences { private const val SHARED_PREFS_SHOW_HIDDEN_PROFILES_NOTICE = "ShowHiddenProfilesNotice" private const val SHARED_PREFS_ONE_HAND_UI_CARD_SHOWN = "OneHandUICardShown" private const val SHARED_PREFS_ADDRESS_CREATION_CARD_SHOWN = "AddressCreationCardShown" + private const val SHARED_PREFS_SUPPORTER_BANNER_SHOWN = "SupporterBannerShown" private const val SHARED_PREFS_SHOW_MUTE_PROFILE_ALERT = "ShowMuteProfileAlert" private const val SHARED_PREFS_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT = "ShowReportsInSupportChatAlert" private const val SHARED_PREFS_STORE_DB_PASSPHRASE = "StoreDBPassphrase" diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgeUserPreview.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgeUserPreview.kt new file mode 100644 index 0000000000..d33c048211 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgeUserPreview.kt @@ -0,0 +1,44 @@ +package chat.simplex.common.views.badges + +import androidx.compose.foundation.layout.* +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import dev.icerock.moko.resources.compose.stringResource +import chat.simplex.common.model.BadgeInfo +import chat.simplex.common.model.BadgeStatus +import chat.simplex.common.model.LocalBadge +import chat.simplex.common.platform.chatModel +import chat.simplex.common.views.helpers.NameWithBadge +import chat.simplex.common.views.helpers.ProfileImage +import chat.simplex.res.MR + +// User's avatar + name with a preview badge at the selected level, rendered via NameWithBadge +// so the preview matches how the badge appears on the name elsewhere. Callers can inject a +// picker affordance (e.g. a chevron) into the name row via `trailing`. +@Composable +fun BadgeUserPreview(level: BadgeLevel, modifier: Modifier = Modifier, trailing: @Composable () -> Unit = {}) { + val user = chatModel.currentUser.value + val displayName = user?.displayName ?: stringResource(MR.strings.badges_preview_my_nickname) + val previewBadge = LocalBadge( + badge = BadgeInfo(badgeType = level.badgeType), + status = BadgeStatus.Active + ) + 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() + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesHowItWorksView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesHowItWorksView.kt new file mode 100644 index 0000000000..f2aa964dd3 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesHowItWorksView.kt @@ -0,0 +1,35 @@ +package chat.simplex.common.views.badges + +import androidx.compose.foundation.layout.* +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import dev.icerock.moko.resources.compose.stringResource +import chat.simplex.common.platform.ColumnWithScrollBar +import chat.simplex.res.MR + +// Draft explanation of how private badges work. TODO [badges]: replace lorem ipsum with the real +// copy once the badge protocol and privacy properties are documented. +@Composable +fun BadgesHowItWorksView() { + ColumnWithScrollBar( + Modifier.padding(horizontal = 25.dp).padding(top = 8.dp), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + stringResource(MR.strings.badges_how_it_works_title), + style = MaterialTheme.typography.h1, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colors.primary, + modifier = Modifier.padding(bottom = 16.dp) + ) + Text(stringResource(MR.strings.badges_how_it_works_p1), style = MaterialTheme.typography.body1) + Text(stringResource(MR.strings.badges_how_it_works_p2), style = MaterialTheme.typography.body1) + Text(stringResource(MR.strings.badges_how_it_works_p3), style = MaterialTheme.typography.body1) + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesPayView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesPayView.kt new file mode 100644 index 0000000000..0a2de0ae7d --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesPayView.kt @@ -0,0 +1,136 @@ +package chat.simplex.common.views.badges + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +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.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import dev.icerock.moko.resources.StringResource +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.onboarding.OnboardingActionButton +import chat.simplex.res.MR + +// Draft billing periods used by the badges UI while the API/state machine is still being designed. +// TODO [badges]: replace with types produced by the badge purchase API when it lands. +enum class BadgePeriod { + OneMonth, + Subscribe; + + val icon: dev.icerock.moko.resources.ImageResource + get() = when (this) { + OneMonth -> MR.images.ic_calendar + Subscribe -> MR.images.ic_refresh + } + + val label: StringResource + get() = when (this) { + OneMonth -> MR.strings.badges_period_one_month + Subscribe -> MR.strings.badges_period_subscribe + } +} + +@Composable +fun BadgesPayView(level: BadgeLevel) { + var selectedPeriod by remember { mutableStateOf(BadgePeriod.Subscribe) } + + ColumnWithScrollBar( + Modifier.padding(horizontal = 25.dp).padding(top = 8.dp, bottom = 20.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + maxIntrinsicSize = true, + ) { + Text( + stringResource(level.title), + style = MaterialTheme.typography.h1, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colors.primary, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + BadgeUserPreview(level = level, modifier = Modifier.padding(top = 4.dp)) + + Text( + stringResource(level.tagline), + style = MaterialTheme.typography.body1, + color = MaterialTheme.colors.secondary, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth().padding(top = 4.dp) + ) + + Row( + Modifier.fillMaxWidth().padding(top = 12.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + PeriodCard(BadgePeriod.OneMonth, selectedPeriod, Modifier.weight(1f)) { selectedPeriod = it } + PeriodCard(BadgePeriod.Subscribe, selectedPeriod, Modifier.weight(1f)) { selectedPeriod = it } + } + + Spacer(Modifier.weight(1f).heightIn(min = 20.dp)) + + PayButton(level, selectedPeriod) + + Text( + stringResource(billingFooter(selectedPeriod)), + style = MaterialTheme.typography.body2, + color = MaterialTheme.colors.secondary, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth().padding(top = 4.dp) + ) + } +} + +@Composable +private fun PeriodCard(period: BadgePeriod, selectedPeriod: BadgePeriod, modifier: Modifier, onSelect: (BadgePeriod) -> Unit) { + val isSelected = period == selectedPeriod + val borderColor = if (isSelected) MaterialTheme.colors.primary else MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.92f) + val shape = RoundedCornerShape(16.dp) + Column( + modifier + .clip(shape) + .background(MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f), shape) + .border(2.dp, borderColor, shape) + .clickable { onSelect(period) } + .padding(vertical = 20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon( + painterResource(period.icon), + contentDescription = null, + tint = if (isSelected) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, + modifier = Modifier.size(32.dp) + ) + Text(stringResource(period.label), style = MaterialTheme.typography.h3, fontWeight = FontWeight.Bold) + } +} + +@Composable +private fun PayButton(level: BadgeLevel, selectedPeriod: BadgePeriod) { + OnboardingActionButton( + modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier, + labelId = if (selectedPeriod == BadgePeriod.Subscribe) level.payMonthlyLabel else level.payOnceLabel, + onboarding = null, + onclick = { + // TODO [badges] wire to purchase API when it lands. + } + ) +} + +// TODO [badges] source the actual renewal/end date from the purchase state machine when wired. +private fun billingFooter(period: BadgePeriod): StringResource = when (period) { + BadgePeriod.Subscribe -> MR.strings.badges_billing_footer_subscribe + BadgePeriod.OneMonth -> MR.strings.badges_billing_footer_one_month +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesRedeemCodeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesRedeemCodeView.kt new file mode 100644 index 0000000000..a290993657 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesRedeemCodeView.kt @@ -0,0 +1,30 @@ +package chat.simplex.common.views.badges + +import androidx.compose.foundation.layout.* +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import dev.icerock.moko.resources.compose.stringResource +import chat.simplex.common.platform.ColumnWithScrollBar +import chat.simplex.res.MR + +// Draft entry-point for redeeming an investor badge code. TODO [badges]: implement input field, +// server verification and success/failure states when the redeem API is defined. +@Composable +fun BadgesRedeemCodeView() { + ColumnWithScrollBar( + Modifier.padding(horizontal = 25.dp).padding(top = 8.dp), + horizontalAlignment = Alignment.Start + ) { + Text( + stringResource(MR.strings.badges_redeem_code_button), + style = MaterialTheme.typography.h1, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colors.primary + ) + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesSupportSimplexView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesSupportSimplexView.kt new file mode 100644 index 0000000000..21836f00d7 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesSupportSimplexView.kt @@ -0,0 +1,153 @@ +package chat.simplex.common.views.badges + +import androidx.compose.foundation.* +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.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import chat.simplex.common.BuildConfigCommon +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.darkStops +import chat.simplex.common.views.newchat.gradientPoints +import chat.simplex.common.views.newchat.lightStops +import chat.simplex.common.views.onboarding.HowItWorks +import chat.simplex.common.views.onboarding.OnboardingActionButton +import chat.simplex.res.MR + +// Entry point for badges management. Subsequent screens push via ModalManager; the enclosing +// modal (settings or the chat-list banner sheet) provides the sliding animation. +@Composable +fun BadgesSupportSimplexView() { + // TODO [badges] gate on user badge status (no badge → this view, active → "Manage your badge") + ColumnWithScrollBar( + Modifier.padding(horizontal = 25.dp).padding(top = 28.dp, bottom = 20.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + maxIntrinsicSize = true, + ) { + Text( + stringResource(MR.strings.badges_support_simplex_title), + style = MaterialTheme.typography.h1, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colors.primary, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + Text( + stringResource(MR.strings.badges_support_simplex_body), + style = MaterialTheme.typography.body1, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + val primary = MaterialTheme.colors.primary + TextButton({ + ModalManager.start.showModal { HowItWorks(user = chatModel.currentUser.value, onboardingStage = null, titleColor = primary) } + }) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) { + Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.primary) + Text(stringResource(MR.strings.badges_why_simplex_is_built), color = MaterialTheme.colors.primary, fontWeight = FontWeight.Medium) + } + } + + Spacer(Modifier.weight(1f)) + + PhoneSupporterHero(Modifier.fillMaxWidth(0.55f)) + + Spacer(Modifier.weight(1f)) + + ChooseLevelButton() + + RedeemCodeButton(Modifier.padding(top = 4.dp)) + } +} + +@Composable +private fun ChooseLevelButton() { + OnboardingActionButton( + modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier, + labelId = MR.strings.badges_choose_your_level, + onboarding = null, + onclick = { + ModalManager.start.showModal { BadgesYourLevelView() } + } + ) +} + +@Composable +private fun RedeemCodeButton(modifier: Modifier = Modifier) { + TextButton( + onClick = { ModalManager.start.showModal { BadgesRedeemCodeView() } }, + modifier = modifier + ) { + Text( + stringResource(MR.strings.badges_redeem_code_button), + color = MaterialTheme.colors.primary, + fontWeight = FontWeight.Medium + ) + } +} + +// Hero image reused across badges views and WhatsNewView v7.1. Fallback (no SIMPLEX_ASSETS) is a +// gradient card carrying the small supporter badge glyph. +@Composable +fun PhoneSupporterHero(modifier: Modifier = Modifier) { + val isDark = isInDarkTheme() + if (BuildConfigCommon.SIMPLEX_ASSETS) { + Image( + painterResource(if (isDark) MR.images.phone_supporter_light else MR.images.phone_supporter), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = modifier.fillMaxWidth() + ) + } else { + var size by remember { mutableStateOf(IntSize.Zero) } + val stops = if (isDark) darkStops else lightStops + val scale = if (isDark) 1.5f else 1.2f + val brush = remember(size, isDark) { + if (size.width > 0 && size.height > 0) { + val aspect = size.height.toFloat() / size.width.toFloat() + val gp = gradientPoints(aspect, scale) + Brush.linearGradient( + colorStops = stops, + start = Offset(gp.startX * size.width, gp.startY * size.height), + end = Offset(gp.endX * size.width, gp.endY * size.height) + ) + } else { + Brush.linearGradient(colorStops = stops) + } + } + Box( + modifier + .fillMaxWidth() + .aspectRatio(1f) + .clip(RoundedCornerShape(24.dp)) + .background(brush) + .onSizeChanged { size = it }, + contentAlignment = Alignment.Center + ) { + Image( + painterResource(MR.images.badge_supporter), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.size(96.dp) + ) + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesYourLevelView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesYourLevelView.kt new file mode 100644 index 0000000000..11a2749208 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/BadgesYourLevelView.kt @@ -0,0 +1,179 @@ +package chat.simplex.common.views.badges + +import androidx.compose.foundation.* +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.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import dev.icerock.moko.resources.StringResource +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import chat.simplex.common.model.BadgeType +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.onboarding.OnboardingActionButton +import chat.simplex.res.MR + +// Draft levels used by the badges UI while the API/state machine is still being designed. TODO [badges]: +// replace with types produced by the badge purchase API when it lands. +enum class BadgeLevel { + Supporter, + Legend; + + val title: StringResource + get() = when (this) { + Supporter -> MR.strings.badges_level_supporter + Legend -> MR.strings.badges_level_legend + } + + val filesDescription: StringResource + get() = when (this) { + Supporter -> MR.strings.badges_level_supporter_files + Legend -> MR.strings.badges_level_legend_files + } + + val monthlyPrice: StringResource + get() = when (this) { + Supporter -> MR.strings.badges_level_supporter_monthly + Legend -> MR.strings.badges_level_legend_monthly + } + + val oneMonthPrice: StringResource + get() = when (this) { + Supporter -> MR.strings.badges_level_supporter_one_month + Legend -> MR.strings.badges_level_legend_one_month + } + + val payMonthlyLabel: StringResource + get() = when (this) { + Supporter -> MR.strings.badges_pay_supporter_monthly + Legend -> MR.strings.badges_pay_legend_monthly + } + + val payOnceLabel: StringResource + get() = when (this) { + Supporter -> MR.strings.badges_pay_supporter_once + Legend -> MR.strings.badges_pay_legend_once + } + + val tagline: StringResource + get() = when (this) { + Supporter -> MR.strings.badges_level_supporter_tagline + Legend -> MR.strings.badges_level_legend_tagline + } + + val badgeAsset: dev.icerock.moko.resources.ImageResource + get() = when (this) { + Supporter -> MR.images.badge_supporter + Legend -> MR.images.badge_legend + } + + val badgeType: BadgeType + get() = when (this) { + Supporter -> BadgeType.Supporter + Legend -> BadgeType.Legend + } +} + +@Composable +fun BadgesYourLevelView() { + var selectedLevel by remember { mutableStateOf(BadgeLevel.Supporter) } + + ColumnWithScrollBar( + Modifier.padding(horizontal = 25.dp).padding(top = 8.dp, bottom = 20.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + maxIntrinsicSize = true, + ) { + Text( + stringResource(MR.strings.badges_your_level_title), + style = MaterialTheme.typography.h1, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colors.primary, + textAlign = TextAlign.Center, + 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 + ) + } + + Row( + Modifier.fillMaxWidth().padding(top = 8.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + LevelCard(BadgeLevel.Supporter, selectedLevel, Modifier.weight(1f)) { selectedLevel = it } + LevelCard(BadgeLevel.Legend, selectedLevel, Modifier.weight(1f)) { selectedLevel = it } + } + + Spacer(Modifier.weight(1f).heightIn(min = 20.dp)) + + ContinueButton(selectedLevel) + + HowItWorksButton(Modifier.padding(top = 4.dp)) + } +} + +@Composable +private fun LevelCard(level: BadgeLevel, selectedLevel: BadgeLevel, modifier: Modifier, onSelect: (BadgeLevel) -> Unit) { + val isSelected = level == selectedLevel + val borderColor = if (isSelected) MaterialTheme.colors.primary else MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.92f) + val shape = RoundedCornerShape(16.dp) + Column( + modifier + .clip(shape) + .background(MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f), shape) + .border(2.dp, borderColor, shape) + .clickable { onSelect(level) } + .padding(vertical = 20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Image( + painterResource(level.badgeAsset), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.size(60.dp) + ) + Text(stringResource(level.title), style = MaterialTheme.typography.h3, fontWeight = FontWeight.Bold) + Text(stringResource(level.filesDescription), style = MaterialTheme.typography.body2, color = MaterialTheme.colors.secondary) + Text(stringResource(level.monthlyPrice), style = MaterialTheme.typography.body1) + } +} + +@Composable +private fun ContinueButton(selectedLevel: BadgeLevel) { + OnboardingActionButton( + modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier, + labelId = MR.strings.badges_continue, + onboarding = null, + onclick = { + ModalManager.start.showModal { BadgesPayView(selectedLevel) } + } + ) +} + +@Composable +private fun HowItWorksButton(modifier: Modifier = Modifier) { + TextButton( + onClick = { ModalManager.start.showModal { BadgesHowItWorksView() } }, + modifier = modifier + ) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) { + Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.primary) + Text(stringResource(MR.strings.badges_how_it_works_button), color = MaterialTheme.colors.primary, fontWeight = FontWeight.Medium) + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/SupportSimpleXBanner.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/SupportSimpleXBanner.kt new file mode 100644 index 0000000000..86ba23de7b --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/badges/SupportSimpleXBanner.kt @@ -0,0 +1,153 @@ +package chat.simplex.common.views.badges + +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +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.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import dev.icerock.moko.resources.compose.painterResource +import chat.simplex.common.BuildConfigCommon +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.darkStops +import chat.simplex.common.views.newchat.lightStops +import chat.simplex.res.MR + +// Dumb chat-list card promoting the badges purchase flow. Both the show gate and the dismissal +// persistence live in ChatListView; this view just renders and reports taps. +@Composable +fun SupportSimpleXBanner(onTap: () -> Unit, onDismiss: () -> Unit) { + val cardCornerRadius = 16.dp + val cardHeight = 72.dp + // matches OneHandUICard's segment icon leading so the text aligns with it in the list + val cardLeadingPadding = 16.dp + val cardTrailingPadding = 8.dp + val heroWidth = 110.dp + // shorter than the natural drawn height so ContentScale.Crop slices the phone body at card bottom + val heroVisibleHeight = 108.dp + // matches the OneHandUICard-style X: outer end pad 4 + circle click area 32 = 36 reserved on right + val heroTrailingPadding = 36.dp + val textToHeroGap = 6.dp + + val isDark = isInDarkTheme() + var cardSize by remember { mutableStateOf(IntSize.Zero) } + val brush = remember(isDark, cardSize) { gradientBrush(isDark, cardSize) } + + // Root Box is sized to the card; hero and X are children so they can render outside the card's + // rounded-corner clip. Hero anchors bottom-end and its 108dp height extends 36dp above the card top. + Box(Modifier.fillMaxWidth().height(cardHeight)) { + Row( + Modifier + .fillMaxSize() + .clip(RoundedCornerShape(cardCornerRadius)) + .background(brush) + .clickable(onClick = onTap) + .onSizeChanged { cardSize = it } + .padding(start = cardLeadingPadding, end = cardTrailingPadding + heroWidth + heroTrailingPadding + textToHeroGap), + verticalAlignment = Alignment.CenterVertically + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + generalGetString(MR.strings.badges_banner_title), + style = MaterialTheme.typography.body1, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colors.primary + ) + Text( + generalGetString(MR.strings.badges_banner_subtitle), + style = MaterialTheme.typography.body2, + color = MaterialTheme.colors.onBackground + ) + } + } + + HeroThumbnail( + heroWidth = heroWidth, + heroVisibleHeight = heroVisibleHeight, + cardHeight = cardHeight, + trailingPadding = heroTrailingPadding, + modifier = Modifier.align(Alignment.BottomEnd) + ) + + // Same X pattern as OneHandUICard: circle-clipped clickable region with inner padding for hit area. + Icon( + painterResource(MR.images.ic_close), + contentDescription = generalGetString(MR.strings.icon_descr_close_button), + tint = MaterialTheme.colors.secondary, + modifier = Modifier + .align(Alignment.TopEnd) + .padding(end = 4.dp, top = 4.dp) + .clip(CircleShape) + .clickable { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.badges_banner_title), + text = generalGetString(MR.strings.badges_banner_dismiss_message), + onConfirm = onDismiss + ) + } + .padding(8.dp) + .size(16.dp) + ) + } +} + +@Composable +private fun HeroThumbnail(heroWidth: Dp, heroVisibleHeight: Dp, cardHeight: Dp, trailingPadding: Dp, modifier: Modifier) { + if (BuildConfigCommon.SIMPLEX_ASSETS) { + // draws at natural aspect, top-aligned in a shorter slot; ContentScale.Crop cuts the overflow at card bottom + Image( + painterResource(if (isInDarkTheme()) MR.images.phone_supporter_light else MR.images.phone_supporter), + contentDescription = null, + contentScale = ContentScale.Crop, + alignment = Alignment.TopCenter, + modifier = modifier.padding(end = trailingPadding).size(width = heroWidth, height = heroVisibleHeight) + ) + } else { + val badgeSize = 48.dp + Image( + painterResource(MR.images.badge_supporter), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = modifier + .padding(end = trailingPadding + 12.dp, top = (cardHeight - badgeSize) / 2, bottom = (cardHeight - badgeSize) / 2) + .size(badgeSize) + ) + } +} + +// light: 2-color end-to-end (lightStops' 0.0–0.5 blue plateau would read as all-blue here). +// dark: full 5-stop darkStops — the intermediate blue-to-navy transitions land nicely. +private fun gradientBrush(isDark: Boolean, size: IntSize): Brush { + if (size.width == 0 || size.height == 0) { + return if (isDark) Brush.linearGradient(colorStops = darkStops) + else Brush.linearGradient(colors = listOf(lightStops.first().second, lightStops.last().second)) + } + val w = size.width.toFloat() + val h = size.height.toFloat() + return if (isDark) { + Brush.linearGradient( + colorStops = darkStops, + start = Offset(0f, h), + end = Offset(w, 0f) + ) + } else { + Brush.linearGradient( + colors = listOf(lightStops.first().second, lightStops.last().second), + start = Offset(0f, h), + end = Offset(w, 0f) + ) + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index 77b4c40d7d..c11460bb2b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -41,6 +41,7 @@ import chat.simplex.common.platform.* import chat.simplex.common.views.call.Call import chat.simplex.common.views.chat.item.* import chat.simplex.common.views.chat.topPaddingToContent +import chat.simplex.common.views.badges.* import chat.simplex.common.views.newchat.* import chat.simplex.common.views.onboarding.* import chat.simplex.common.views.usersettings.* @@ -910,6 +911,7 @@ private fun BoxScope.ChatList(searchText: MutableState, listStat val oneHandUI = remember { appPrefs.oneHandUI.state } val oneHandUICardShown = remember { appPrefs.oneHandUICardShown.state } val addressCreationCardShown = remember { appPrefs.addressCreationCardShown.state } + val supporterBannerShown = remember { appPrefs.supporterBannerShown.state } val activeFilter = remember { chatModel.activeChatTagFilter } LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) { @@ -998,6 +1000,18 @@ private fun BoxScope.ChatList(searchText: MutableState, listStat ToggleChatListCard() } } + // +1 vs SimpleX Lock's threshold accounts for the always-present SimpleX Directory contact card, + // so this still needs 3 real conversations. + if (!supporterBannerShown.value && chatModel.chats.value.size > 3) { + item { + Box(Modifier.padding(16.dp).zIndex(1f)) { + SupportSimpleXBanner( + onTap = { ModalManager.start.showModal { BadgesSupportSimplexView() } }, + onDismiss = { appPrefs.supporterBannerShown.set(true) } + ) + } + } + } itemsIndexed(chats, key = { _, chat -> chat.remoteHostId to chat.id }) { index, chat -> val nextChatSelected = remember(chat.id, chats) { derivedStateOf { chatModel.chatId.value != null && chats.getOrNull(index + 1)?.id == chatModel.chatId.value diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt index 703d295523..8cfce9f90d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt @@ -7,6 +7,7 @@ import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalUriHandler import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.* @@ -22,12 +23,12 @@ import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource @Composable -fun HowItWorks(user: User?, onboardingStage: SharedPreference? = null) { +fun HowItWorks(user: User?, onboardingStage: SharedPreference? = null, titleColor: Color = Color.Unspecified) { Column(Modifier.fillMaxSize().padding(horizontal = if (appPlatform.isDesktop) DEFAULT_PADDING * 2 else DEFAULT_PADDING)) { Spacer(Modifier.statusBarsPadding().padding(top = AppBarHeight * fontSizeSqrtMultiplier)) val paraPadding = PaddingValues(bottom = if (appPlatform.isDesktop) 10.dp else 12.dp) Column(Modifier.weight(1f).padding(bottom = DEFAULT_PADDING).verticalScroll(rememberScrollState())) { - Text(stringResource(MR.strings.why_built_heading), style = MaterialTheme.typography.h1, modifier = Modifier.padding(bottom = DEFAULT_PADDING)) + Text(stringResource(MR.strings.why_built_heading), style = MaterialTheme.typography.h1, color = titleColor, modifier = Modifier.padding(bottom = DEFAULT_PADDING)) ReadableText(MR.strings.why_built_p1, padding = paraPadding) ReadableText(MR.strings.why_built_p2, padding = paraPadding) ReadableText(MR.strings.why_built_p3, padding = paraPadding) @@ -45,6 +46,9 @@ fun HowItWorks(user: User?, onboardingStage: SharedPreference? OnboardingActionButton(user, onboardingStage, onclick = { ModalManager.fullscreen.closeModal() }) TextButtonBelowOnboardingButton("", null) } + } else if (appPlatform.isAndroid) { + // no button below → add nav-bar-inset spacer so text doesn't run under system gesture bar + Spacer(Modifier.navigationBarsPadding()) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt index f95ffc1961..31f69d4410 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalUriHandler import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @@ -19,6 +20,7 @@ import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import chat.simplex.common.BuildConfigCommon import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel import chat.simplex.common.model.* @@ -934,8 +936,54 @@ private val versionDescriptions: List = listOf( ), ) ), + // TODO [badges] finalise copy + Read more link before v7.1 ships. + VersionDescription( + version = "v7.1", + post = null, + features = listOf( + VersionFeature.FeatureView( + icon = null, + titleId = MR.strings.whats_new_supporter_badge_short, + view = { SupporterBadgeWhatsNew() } + ) + ) + ), ) +@Composable +private fun SupporterBadgeWhatsNew() { + Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.Top) { + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(6.dp)) { + Text( + stringResource(MR.strings.whats_new_supporter_badge_title), + style = MaterialTheme.typography.h3, + fontWeight = FontWeight.Bold + ) + Text( + stringResource(MR.strings.whats_new_supporter_badge_body), + style = MaterialTheme.typography.body1, + maxLines = 10 + ) + } + val isDark = isInDarkTheme() + if (BuildConfigCommon.SIMPLEX_ASSETS) { + Image( + painterResource(if (isDark) MR.images.phone_supporter_light else MR.images.phone_supporter), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.size(110.dp) + ) + } else { + Image( + painterResource(MR.images.badge_supporter), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.size(72.dp) + ) + } + } +} + private val lastVersion = versionDescriptions.last().version fun setLastVersionDefault(m: ChatModel) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt index c8e040c592..68bf93a203 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt @@ -27,6 +27,7 @@ import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.badges.BadgesSupportSimplexView import chat.simplex.common.views.database.DatabaseView import chat.simplex.common.views.helpers.* import chat.simplex.common.views.migration.MigrateFromDeviceView @@ -102,6 +103,15 @@ fun SettingsLayout( } SectionDividerSpaced() + SectionView { + SectionItemView(click = showSettingsModal { BadgesSupportSimplexView() }) { + Image(painterResource(MR.images.badge_supporter), stringResource(MR.strings.supporter_perks), Modifier.size(24.dp)) + TextIconSpaced() + Text(stringResource(MR.strings.supporter_perks)) + } + } + SectionDividerSpaced() + SectionView(stringResource(MR.strings.advanced_settings)) { SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showCustomModal { _, close -> NetworkAndServersView(close) }, disabled = stopped) if (appPlatform == AppPlatform.ANDROID) { diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 7950e7cc1c..cd2bae21a4 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -3198,4 +3198,42 @@ This badge could not be verified and may not be genuine. Badge cannot be verified The badge is signed with a key that this version of the app does not recognize. Update the app to verify this badge. + Supporter + Legend + Send 2GB files + Send 5GB files + $7/month + $70/month + $7 + $70 + Pay $7/month + Pay $70/month + Pay $7 + Pay $70 + Optional profile badge\nand 2GB files + Optional profile badge\nand 5GB files + 1 month + Subscribe + Renews on July 22, 2026. Cancel anytime. + Ends on August 22, 2026. + Your level + Support SimpleX + SimpleX doesn\'t sell ads or data. It\'s funded by its users and by investors who share the mission. You can support the project and show a badge on your profile. + Why SimpleX is built. + Choose your level + Redeem badge code + Continue + How private badges work + How private badges work + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + My nickname + Support SimpleX + Get badge + files up to 5GB + You can support SimpleX later in Settings. + Supporter perks + Supporter badge ❤️ + Help keep the network running — send files up to 2 GB. + Supporter badge \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/phone_supporter.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/phone_supporter.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/phone_supporter.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/phone_supporter_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/phone_supporter_light.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/phone_supporter_light.svg @@ -0,0 +1,4 @@ + + + +