diff --git a/apps/multiplatform/.idea/codeStyles/Project.xml b/apps/multiplatform/.idea/codeStyles/Project.xml
index a847d12689..c02a1b3923 100644
--- a/apps/multiplatform/.idea/codeStyles/Project.xml
+++ b/apps/multiplatform/.idea/codeStyles/Project.xml
@@ -3,6 +3,41 @@
+
+
+
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt
index 70e0067260..52cde3abca 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt
@@ -139,7 +139,7 @@ fun MainScreen() {
when {
onboarding == OnboardingStage.Step1_SimpleXInfo && chatModel.migrationState.value != null -> {
// In migration process. Nothing should interrupt it, that's why it's the first branch in when()
- SimpleXInfo(chatModel, onboarding = true)
+ IntroCarouselView(chatModel)
if (appPlatform.isDesktop) {
ModalManager.fullscreen.showInView()
}
@@ -184,7 +184,7 @@ fun MainScreen() {
when (state) {
OnboardingStage.OnboardingComplete -> { /* handled out of AnimatedContent block */}
OnboardingStage.Step1_SimpleXInfo -> {
- SimpleXInfo(chatModel, onboarding = true)
+ IntroCarouselView(chatModel)
if (appPlatform.isDesktop) {
ModalManager.fullscreen.showInView()
}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt
index 6ec124048c..091c87dc2e 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt
@@ -17,10 +17,12 @@ import androidx.compose.ui.graphics.SolidColor
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.*
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import chat.simplex.common.BuildConfigCommon
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel.controller
@@ -30,6 +32,12 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.*
import chat.simplex.common.views.usersettings.SettingsActionItem
import chat.simplex.res.MR
+import androidx.compose.foundation.Image
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.ime
+import androidx.compose.foundation.layout.windowInsetsBottomHeight
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
@@ -143,20 +151,68 @@ fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) {
val displayName = rememberSaveable { mutableStateOf("") }
val focusRequester = remember { FocusRequester() }
Column(if (appPlatform.isAndroid) Modifier.fillMaxSize().padding(start = DEFAULT_ONBOARDING_HORIZONTAL_PADDING * 2, end = DEFAULT_ONBOARDING_HORIZONTAL_PADDING * 2, bottom = DEFAULT_PADDING) else Modifier.widthIn(max = 600.dp).fillMaxHeight().padding(horizontal = DEFAULT_PADDING).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) {
- Box(Modifier.align(Alignment.CenterHorizontally)) {
- AppBarTitle(stringResource(MR.strings.create_your_profile), bottomPadding = DEFAULT_PADDING, withPadding = false)
+ // Isometric illustration (conditional on USE_BRANDED_IMAGES)
+ if (BuildConfigCommon.USE_BRANDED_IMAGES) {
+ Spacer(Modifier.height(DEFAULT_PADDING * 2))
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = DEFAULT_PADDING),
+ contentAlignment = Alignment.Center
+ ) {
+ Image(
+ painter = painterResource(MR.images.intro_2),
+ contentDescription = null,
+ modifier = Modifier.size(200.dp),
+ contentScale = ContentScale.Fit
+ )
+ }
+ Spacer(Modifier.height(DEFAULT_PADDING))
}
- ReadableText(MR.strings.your_profile_is_stored_on_your_device, TextAlign.Center, padding = PaddingValues(), style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary))
+
+ // Title: "Create profile"
+ Text(
+ text = stringResource(MR.strings.create_profile),
+ style = MaterialTheme.typography.h1,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colors.onBackground,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+
Spacer(Modifier.height(DEFAULT_PADDING))
- ReadableText(MR.strings.profile_is_only_shared_with_your_contacts, TextAlign.Center, style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary))
- Spacer(Modifier.height(DEFAULT_PADDING))
- ProfileNameField(displayName, stringResource(MR.strings.display_name), { it.trim() == mkValidName(it) }, focusRequester)
+
+ // Subtitle: "Your profile is stored on your device and only shared with your contacts."
+ Text(
+ text = stringResource(MR.strings.create_profile_subtitle),
+ style = MaterialTheme.typography.body1,
+ color = MaterialTheme.colors.secondary,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Spacer(Modifier.height(DEFAULT_PADDING * 2))
+
+ // Name input field with placeholder: "Enter your name..."
+ ProfileNameField(displayName, stringResource(MR.strings.enter_your_name_placeholder), { it.trim() == mkValidName(it) }, focusRequester)
}
+
Spacer(Modifier.fillMaxHeight().weight(1f))
- Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) {
+
+ // Calculate bottom padding - ColumnWithScrollBar already applies imePadding automatically
+ val imePadding = WindowInsets.ime.asPaddingValues().calculateBottomPadding()
+ val isKeyboardOpen = appPlatform.isAndroid && keyboardState == KeyboardState.Opened && imePadding > 0.dp
+
+ Column(
+ Modifier
+ .widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp)
+ .align(Alignment.CenterHorizontally)
+ .padding(bottom = DEFAULT_PADDING * 2),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
OnboardingActionButton(
if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING).fillMaxWidth() else Modifier.widthIn(min = 300.dp),
- labelId = MR.strings.create_profile_button,
+ labelId = MR.strings.create_profile,
onboarding = null,
enabled = canCreateProfile(displayName.value),
onclick = { createProfileOnboarding(chat.simplex.common.platform.chatModel, displayName.value, close) }
@@ -164,6 +220,14 @@ fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) {
// Reserve space
TextButtonBelowOnboardingButton("", null)
}
+
+ // Add spacer at bottom when keyboard is open to ensure button can be scrolled above keyboard
+ // This provides extra scrollable space so the button remains visible
+ if (isKeyboardOpen) {
+ Spacer(Modifier.height(imePadding + DEFAULT_PADDING))
+ } else {
+ Spacer(Modifier.height(DEFAULT_PADDING))
+ }
LaunchedEffect(Unit) {
delay(300)
@@ -173,13 +237,15 @@ fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) {
LaunchedEffect(Unit) {
setLastVersionDefault(chatModel)
}
- if (savedKeyboardState != keyboardState) {
- LaunchedEffect(keyboardState) {
+ // Auto-scroll when keyboard opens to ensure button is visible
+ LaunchedEffect(keyboardState) {
+ if (appPlatform.isAndroid && keyboardState == KeyboardState.Opened) {
scope.launch {
- savedKeyboardState = keyboardState
+ delay(100) // Small delay to ensure layout is updated
scrollState.animateScrollTo(scrollState.maxValue)
}
}
+ savedKeyboardState = keyboardState
}
}
}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt
index 9c6c0fa635..abc55f15c7 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt
@@ -18,6 +18,7 @@ import androidx.compose.ui.text.TextStyle
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.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
@@ -27,6 +28,13 @@ import chat.simplex.common.views.usersettings.networkAndServers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
+import androidx.compose.foundation.Image
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.material.Icon
+import chat.simplex.common.views.helpers.BoltFilled
+import chat.simplex.common.views.onboarding.SetNotificationsMode
@Composable
fun ModalData.OnboardingConditionsView() {
@@ -43,30 +51,83 @@ fun ModalData.OnboardingConditionsView() {
.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer),
maxIntrinsicSize = true
) {
- Box(Modifier.align(Alignment.CenterHorizontally)) {
- AppBarTitle(stringResource(MR.strings.operator_conditions_of_use), bottomPadding = DEFAULT_PADDING)
+ Column(
+ (if (appPlatform.isDesktop) Modifier.width(450.dp).align(Alignment.CenterHorizontally) else Modifier)
+ .fillMaxWidth()
+ .padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ // Isometric illustration (conditional on USE_BRANDED_IMAGES)
+ if (BuildConfigCommon.USE_BRANDED_IMAGES) {
+ Spacer(Modifier.height(DEFAULT_PADDING * 2))
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = DEFAULT_PADDING),
+ contentAlignment = Alignment.Center
+ ) {
+ Image(
+ painter = painterResource(MR.images.intro_2),
+ contentDescription = null,
+ modifier = Modifier.size(200.dp),
+ contentScale = ContentScale.Fit
+ )
+ }
+ Spacer(Modifier.height(DEFAULT_PADDING))
+ }
+
+ // Title: "Conditions of use" (centered, bold)
+ Text(
+ text = stringResource(MR.strings.conditions_of_use_title),
+ style = MaterialTheme.typography.h1,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colors.onBackground,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Spacer(Modifier.height(DEFAULT_PADDING * 2))
}
Spacer(Modifier.weight(1f))
+
Column(
(if (appPlatform.isDesktop) Modifier.width(450.dp).align(Alignment.CenterHorizontally) else Modifier)
.fillMaxWidth()
.padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING),
horizontalAlignment = Alignment.Start
) {
+ // Body text (left-aligned)
Text(
stringResource(MR.strings.onboarding_conditions_private_chats_not_accessible),
- style = TextStyle(fontSize = 17.sp, lineHeight = 23.sp)
+ style = TextStyle(fontSize = 17.sp, lineHeight = 23.sp),
+ color = MaterialTheme.colors.onBackground
)
Spacer(Modifier.height(DEFAULT_PADDING))
Text(
- stringResource(MR.strings.onboarding_conditions_by_using_you_agree),
- style = TextStyle(fontSize = 17.sp, lineHeight = 23.sp)
+ "By using SimpleX Chat you agree to:",
+ style = TextStyle(fontSize = 17.sp, lineHeight = 23.sp),
+ color = MaterialTheme.colors.onBackground
+ )
+ Spacer(Modifier.height(DEFAULT_PADDING_HALF))
+ Text(
+ "• send only legal content in public groups.",
+ style = TextStyle(fontSize = 17.sp, lineHeight = 23.sp),
+ color = MaterialTheme.colors.onBackground,
+ modifier = Modifier.padding(start = DEFAULT_PADDING)
+ )
+ Spacer(Modifier.height(DEFAULT_PADDING_HALF))
+ Text(
+ "• respect other users – no spam.",
+ style = TextStyle(fontSize = 17.sp, lineHeight = 23.sp),
+ color = MaterialTheme.colors.onBackground,
+ modifier = Modifier.padding(start = DEFAULT_PADDING)
)
Spacer(Modifier.height(DEFAULT_PADDING))
+ // Privacy policy link (blue, underlined)
Text(
stringResource(MR.strings.onboarding_conditions_privacy_policy_and_conditions_of_use),
- style = TextStyle(fontSize = 17.sp),
+ style = TextStyle(fontSize = 17.sp, textDecoration = TextDecoration.Underline),
color = MaterialTheme.colors.primary,
modifier = Modifier
.clickable(
@@ -83,10 +144,65 @@ fun ModalData.OnboardingConditionsView() {
Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) {
AcceptConditionsButton(enabled = selectedOperatorIds.value.isNotEmpty(), selectedOperatorIds)
- TextButtonBelowOnboardingButton(stringResource(MR.strings.onboarding_conditions_configure_server_operators)) {
- ModalManager.fullscreen.showModalCloseable { close ->
- ChooseServerOperators(serverOperators, selectedOperatorIds, close)
- }
+
+ // Configure server operators link with icon
+ Row(
+ modifier = Modifier
+ .clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = null
+ ) {
+ ModalManager.fullscreen.showModalCloseable { close ->
+ ChooseServerOperators(serverOperators, selectedOperatorIds, close)
+ }
+ }
+ .padding(vertical = DEFAULT_PADDING_HALF),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Text(
+ text = stringResource(MR.strings.onboarding_conditions_configure_server_operators),
+ color = MaterialTheme.colors.primary,
+ fontSize = 16.sp
+ )
+ Spacer(Modifier.width(8.dp))
+ // Server operator icon - using network icon if available, otherwise a simple circle
+ Icon(
+ painterResource(MR.images.ic_info),
+ contentDescription = null,
+ tint = MaterialTheme.colors.secondary,
+ modifier = Modifier.size(16.dp)
+ )
+ }
+
+ // Configure notifications link with icon
+ Row(
+ modifier = Modifier
+ .clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = null
+ ) {
+ ModalManager.fullscreen.showModalCloseable { close ->
+ SetNotificationsMode(chatModel)
+ }
+ }
+ .padding(vertical = DEFAULT_PADDING_HALF),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Text(
+ text = stringResource(MR.strings.configure_notifications),
+ color = MaterialTheme.colors.primary,
+ fontSize = 16.sp
+ )
+ Spacer(Modifier.width(8.dp))
+ // Notification icon - using bolt/lightning icon
+ Icon(
+ BoltFilled,
+ contentDescription = null,
+ tint = MaterialTheme.colors.primary,
+ modifier = Modifier.size(16.dp)
+ )
}
}
}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/IntroCarouselView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/IntroCarouselView.kt
new file mode 100644
index 0000000000..ed315d843c
--- /dev/null
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/IntroCarouselView.kt
@@ -0,0 +1,125 @@
+package chat.simplex.common.views.onboarding
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import chat.simplex.common.BuildConfigCommon
+import chat.simplex.common.model.ChatModel
+import chat.simplex.common.platform.appPlatform
+import chat.simplex.common.ui.theme.*
+import chat.simplex.common.views.helpers.*
+import chat.simplex.common.views.migration.MigrateToDeviceView
+import chat.simplex.common.views.migration.MigrationToState
+import chat.simplex.res.MR
+import dev.icerock.moko.resources.compose.painterResource
+import dev.icerock.moko.resources.compose.stringResource
+
+@Composable
+fun IntroCarouselView(chatModel: ChatModel) {
+ CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) {
+ ModalView({}, showClose = false, showAppBar = false) {
+ IntroCarouselContent(chatModel)
+ }
+ }
+ LaunchedEffect(Unit) {
+ if (chatModel.migrationState.value != null && !ModalManager.fullscreen.hasModalsOpen()) {
+ ModalManager.fullscreen.showCustomModal(animated = false) { close -> MigrateToDeviceView(close) }
+ }
+ }
+}
+
+@Composable
+private fun IntroCarouselContent(chatModel: ChatModel) {
+ val pagerState = rememberPagerState(initialPage = 0, initialPageOffsetFraction = 0f) { 3 }
+
+ Column(Modifier.fillMaxSize()) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = DEFAULT_PADDING + 8.dp),
+ contentAlignment = Alignment.Center,
+ ) {
+ SimpleXLogo(modifier = Modifier.widthIn(max = if (appPlatform.isAndroid) 250.dp else 500.dp))
+ }
+
+ Spacer(Modifier.height(24.dp))
+
+
+ HorizontalPager(
+ state = pagerState,
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxWidth(),
+ pageNestedScrollConnection = LocalAppBarHandler.current!!.connection,
+ verticalAlignment = Alignment.Top,
+ userScrollEnabled = appPlatform.isAndroid,
+ ) { page ->
+ val headline = when (page) {
+ 0 -> stringResource(MR.strings.intro_headline_1)
+ 1 -> stringResource(MR.strings.intro_headline_2)
+ else -> stringResource(MR.strings.intro_headline_3)
+ }
+ val subtitle = when (page) {
+ 0 -> stringResource(MR.strings.intro_subtitle_1)
+ 1 -> stringResource(MR.strings.intro_subtitle_2)
+ else -> stringResource(MR.strings.intro_subtitle_3)
+ }
+ val showButtons = page == 2
+ val introImage = when (page) {
+ 0 -> MR.images.intro_2
+ 1 -> MR.images.intro_2
+ else -> MR.images.intro_2
+ }
+ IntroPage(
+ headline = headline,
+ subtitle = subtitle,
+ centralContent = {
+ if (BuildConfigCommon.USE_BRANDED_IMAGES) {
+ Icon(
+ painter = painterResource(introImage),
+ contentDescription = null,
+ modifier = Modifier.size(200.dp),
+ )
+ }
+ },
+ showButtons = showButtons,
+ onCreateProfile = if (showButtons) {
+ {}
+ } else null,
+ onMigrate = if (showButtons) {
+ {
+ chatModel.migrationState.value = MigrationToState.PasteOrScanLink
+ ModalManager.fullscreen.showCustomModal { close -> MigrateToDeviceView(close) }
+ }
+ } else null,
+ )
+ }
+
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = DEFAULT_PADDING),
+ contentAlignment = Alignment.Center,
+ ) {
+ PageIndicator(pageCount = 3, currentPage = pagerState.currentPage)
+ }
+ }
+}
+
+@Composable
+private fun TextFallback() {
+ Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ Icon(
+ painter = painterResource(MR.images.ic_chat),
+ contentDescription = null,
+ modifier = Modifier.size(120.dp),
+ tint = MaterialTheme.colors.primary.copy(alpha = 0.6f),
+ )
+ }
+}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/IntroPage.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/IntroPage.kt
new file mode 100644
index 0000000000..5cda021321
--- /dev/null
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/IntroPage.kt
@@ -0,0 +1,85 @@
+package chat.simplex.common.views.onboarding
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.*
+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.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import chat.simplex.common.platform.appPlatform
+import chat.simplex.common.ui.theme.*
+import chat.simplex.res.MR
+import dev.icerock.moko.resources.compose.stringResource
+
+@Composable
+fun IntroPage(
+ headline: String,
+ subtitle: String,
+ centralContent: @Composable BoxScope.() -> Unit,
+ showButtons: Boolean = false,
+ onCreateProfile: (() -> Unit)? = null,
+ onMigrate: (() -> Unit)? = null,
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = DEFAULT_ONBOARDING_HORIZONTAL_PADDING),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Top,
+ ) {
+
+ Box(
+ modifier = Modifier
+ .weight(0.5f)
+ .fillMaxWidth(),
+ contentAlignment = Alignment.Center
+ ) {
+ centralContent()
+ }
+
+ Spacer(Modifier.height(32.dp))
+
+ Text(
+ text = headline,
+ style = MaterialTheme.typography.h1,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colors.onBackground,
+ textAlign = TextAlign.Center,
+ lineHeight = 38.sp,
+ )
+ Spacer(Modifier.height(16.dp))
+ Text(
+ text = subtitle,
+ style = MaterialTheme.typography.body1,
+ color = MaterialTheme.colors.secondary,
+ textAlign = TextAlign.Center,
+ lineHeight = 24.sp,
+ )
+
+ if (showButtons && (onCreateProfile != null || onMigrate != null)) {
+ Spacer(Modifier.height(DEFAULT_PADDING * 2))
+ Column(
+ Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ if (onCreateProfile != null) {
+ OnboardingActionButton(
+ modifier = Modifier.fillMaxWidth(),
+ labelId = MR.strings.create_your_profile,
+ onboarding = OnboardingStage.Step2_CreateProfile,
+ onclick = onCreateProfile,
+ )
+ }
+ if (onMigrate != null) {
+ TextButtonBelowOnboardingButton(
+ text = stringResource(MR.strings.migrate_from_another_device),
+ onClick = onMigrate,
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/PageIndicator.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/PageIndicator.kt
new file mode 100644
index 0000000000..e8a84c02ed
--- /dev/null
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/PageIndicator.kt
@@ -0,0 +1,44 @@
+package chat.simplex.common.views.onboarding
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import chat.simplex.common.ui.theme.DEFAULT_PADDING
+import chat.simplex.common.ui.theme.isInDarkTheme
+
+@Composable
+fun PageIndicator(
+ pageCount: Int,
+ currentPage: Int,
+ modifier: Modifier = Modifier,
+ dotSize: Dp = 8.dp,
+ spacing: Dp = 8.dp,
+) {
+ val activeColor = MaterialTheme.colors.primary
+ val inactiveColor = if (isInDarkTheme()) {
+ MaterialTheme.colors.secondary.copy(alpha = 0.5f)
+ } else {
+ MaterialTheme.colors.secondary.copy(alpha = 0.4f)
+ }
+
+ Row(
+ modifier = modifier.padding(vertical = DEFAULT_PADDING),
+ horizontalArrangement = Arrangement.spacedBy(spacing, Alignment.CenterHorizontally),
+ ) {
+ repeat(pageCount) { index ->
+ Box(
+ modifier = Modifier
+ .size(dotSize)
+ .clip(CircleShape)
+ .background(if (index == currentPage) activeColor else inactiveColor)
+ )
+ }
+ }
+}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt
index e5d00fddd1..2e446f207c 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt
@@ -95,12 +95,12 @@ fun SimpleXInfoLayout(
}
@Composable
-fun SimpleXLogo() {
+fun SimpleXLogo(modifier: Modifier = Modifier) {
Image(
painter = painterResource(if (isInDarkTheme()) MR.images.logo_light else MR.images.logo),
contentDescription = stringResource(MR.strings.image_descr_simplex_logo),
contentScale = ContentScale.FillWidth,
- modifier = Modifier
+ modifier = modifier
.padding(vertical = DEFAULT_PADDING)
.fillMaxWidth()
)
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/intro_2.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/intro_2.svg
new file mode 100644
index 0000000000..66f365ed92
--- /dev/null
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/intro_2.svg
@@ -0,0 +1,3 @@
+
diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties
index 393d2a1782..38d8a9f876 100644
--- a/apps/multiplatform/gradle.properties
+++ b/apps/multiplatform/gradle.properties
@@ -40,4 +40,4 @@ compose.version=1.8.2
database.backend=sqlite
# Set to true for branded builds (includes proprietary illustrations). Default: false (AGPL-compliant build).
-# USE_BRANDED_IMAGES=true
+ USE_BRANDED_IMAGES=true
diff --git a/docs/rfcs/2025-01-31-onboarding-ui-agpl-restructure.md b/docs/rfcs/2025-01-31-onboarding-ui-agpl-restructure.md
index fc4bc5c846..0765e3d437 100644
--- a/docs/rfcs/2025-01-31-onboarding-ui-agpl-restructure.md
+++ b/docs/rfcs/2025-01-31-onboarding-ui-agpl-restructure.md
@@ -624,11 +624,25 @@ If critical issues arise post-release:
## Open Questions
1. **Asset repository structure** — Final decision on sibling folder vs separate repo?
+
+Let's do folder with readme that it's proprietory and not licensed.
+
2. **Intro content** — Final copy for headlines and subtitles?
+
+Yes, it's on me, start from invitations flow - it's more interesting and ready.
+
3. **Animated fallback design** — Specific animation/text layout for AGPL build?
+
+Just show opened card I think in the first iteration, we'll see how it looks
+
4. **Migration flow entry** — Should "Migrate from another device" also appear elsewhere?
+
+Don't understand the question.
+
5. **Analytics** — Any onboarding completion tracking requirements?
+We don't do it yet
+
---
## References