diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index a5daeddc7a..54d783fc85 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -3641,4 +3641,10 @@
Message in %s
Message in room
Room/Space
+
+ You can no longer create an account with %1$s using this app
+ Download %1$s to use %2$s for your account or choose a different homeserver.
+ Download %1$s
+ Faster, more secure, and packed with powerful collaboration tools.
+
diff --git a/library/ui-styles/src/main/res/values/colors.xml b/library/ui-styles/src/main/res/values/colors.xml
index 9d8645a707..5e9bf9062c 100644
--- a/library/ui-styles/src/main/res/values/colors.xml
+++ b/library/ui-styles/src/main/res/values/colors.xml
@@ -158,4 +158,25 @@
#EEF8F4
#1D292A
+
+
+ #FFF7F6
+ #3E0000
+
+
+ #FFC5BC
+ #710000
+
+
+ #D51928
+ #FD3E3C
+
+
+ #D51928
+ #FD3E3C
+
+
+ #1B1D22
+ #EBEEF2
+
diff --git a/library/ui-styles/src/main/res/values/theme_dark.xml b/library/ui-styles/src/main/res/values/theme_dark.xml
index 9afa14caed..39ec830abd 100644
--- a/library/ui-styles/src/main/res/values/theme_dark.xml
+++ b/library/ui-styles/src/main/res/values/theme_dark.xml
@@ -55,6 +55,13 @@
- ?vctr_system
- ?vctr_notice_secondary
+
+ - @color/vctr_bg_critical_subtle_dark
+ - @color/vctr_border_critical_subtle_dark
+ - @color/vctr_icon_critical_primary_dark
+ - @color/vctr_text_critical_primary_dark
+ - @color/vctr_text_primary_dark
+
- @color/element_accent_dark
- @color/element_accent_dark
diff --git a/library/ui-styles/src/main/res/values/theme_light.xml b/library/ui-styles/src/main/res/values/theme_light.xml
index 23782ee34b..bd397d1bc6 100644
--- a/library/ui-styles/src/main/res/values/theme_light.xml
+++ b/library/ui-styles/src/main/res/values/theme_light.xml
@@ -55,6 +55,13 @@
- ?vctr_system
- ?vctr_notice_secondary
+
+ - @color/vctr_bg_critical_subtle_light
+ - @color/vctr_border_critical_subtle_light
+ - @color/vctr_icon_critical_primary_light
+ - @color/vctr_text_critical_primary_light
+ - @color/vctr_text_primary_light
+
- @color/element_accent_light
- @color/element_accent_light
diff --git a/vector-config/src/main/java/im/vector/app/config/Config.kt b/vector-config/src/main/java/im/vector/app/config/Config.kt
index 1bd21dd08e..d70dd109f4 100644
--- a/vector-config/src/main/java/im/vector/app/config/Config.kt
+++ b/vector-config/src/main/java/im/vector/app/config/Config.kt
@@ -97,4 +97,16 @@ object Config {
val ER_DEBUG_ANALYTICS_CONFIG = DEBUG_ANALYTICS_CONFIG.copy(sentryEnvironment = "element-r")
val SHOW_UNVERIFIED_SESSIONS_ALERT_AFTER_MILLIS = 7.days.inWholeMilliseconds // 1 Week
+
+ /**
+ * Sunsetting the application.
+ * Fork maintainers can use this to inform users about their new application if any. Note that you probably also want
+ * to replace the resource `replacement_app_icon` too.
+ */
+ val sunsetConfig: SunsetConfig = SunsetConfig.Enabled(
+ // TODO: update this URL.
+ learnMoreLink = "https://element.io/",
+ replacementApplicationName = "Element X",
+ replacementApplicationId = "io.element.android.x",
+ )
}
diff --git a/vector-config/src/main/java/im/vector/app/config/SunsetConfig.kt b/vector-config/src/main/java/im/vector/app/config/SunsetConfig.kt
new file mode 100644
index 0000000000..f012ac32a9
--- /dev/null
+++ b/vector-config/src/main/java/im/vector/app/config/SunsetConfig.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2025 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.config
+
+sealed interface SunsetConfig {
+ /**
+ * Sunsetting the application is disabled.
+ */
+ data object Disabled : SunsetConfig
+
+ /**
+ * Sunsetting the application is enabled and can be configured by implementing this class.
+ */
+ data class Enabled(
+ /**
+ * The URL target to learn more.
+ */
+ val learnMoreLink: String,
+
+ /**
+ * The replacement application ID.
+ * Example: for Element application, the replacement application ID is the id of Element X: "Element X".
+ */
+ val replacementApplicationName: String,
+
+ /**
+ * The replacement application ID.
+ * Example: for Element App, the replacement application ID is the id of Element X: "io.element.android.x".
+ */
+ val replacementApplicationId: String,
+ ) : SunsetConfig
+}
diff --git a/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt b/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt
index 82271ca23c..4db340a723 100644
--- a/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt
+++ b/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt
@@ -32,6 +32,7 @@ import androidx.core.app.ShareCompat
import androidx.core.content.FileProvider
import androidx.core.content.getSystemService
import im.vector.app.R
+import im.vector.app.core.resources.BuildMeta
import im.vector.app.features.notifications.NotificationUtils
import im.vector.app.features.themes.ThemeUtils
import im.vector.lib.strings.CommonStrings
@@ -367,13 +368,21 @@ private fun addToGallery(savedFile: File, mediaMimeType: String?, context: Conte
}
/**
- * Open the play store to the provided application Id, default to this app.
+ * Open the play store or the F-Droid to the provided application Id, default to this app.
*/
-fun openPlayStore(activity: Activity, appId: String) {
+fun openApplicationStore(
+ activity: Activity,
+ buildMeta: BuildMeta,
+ appId: String = buildMeta.applicationId,
+) {
try {
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$appId")))
} catch (activityNotFoundException: ActivityNotFoundException) {
- activity.safeStartActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=$appId")))
+ if (buildMeta.flavorDescription == "FDroid") {
+ activity.safeStartActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://f-droid.org/packages/$appId")))
+ } else {
+ activity.safeStartActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=$appId")))
+ }
}
}
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/MasSupportRequiredException.kt b/vector/src/main/java/im/vector/app/features/onboarding/MasSupportRequiredException.kt
new file mode 100644
index 0000000000..ca6090bd05
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/onboarding/MasSupportRequiredException.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2025 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.onboarding
+
+class MasSupportRequiredException : Exception("Please use replacement app")
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
index 27f1727641..54b2c83376 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
@@ -12,6 +12,8 @@ import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
+import im.vector.app.config.Config
+import im.vector.app.config.SunsetConfig
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
@@ -761,7 +763,13 @@ class OnboardingViewModel @AssistedInject constructor(
}
OnboardingFlow.SignUp -> {
updateSignMode(SignMode.SignUp)
- internalRegisterAction(RegisterAction.StartRegistration)
+ if (authResult.selectedHomeserver.hasOidcCompatibilityFlow && Config.sunsetConfig is SunsetConfig.Enabled) {
+ // An error is displayed now
+ setState { copy(isLoading = false) }
+ _viewEvents.post(OnboardingViewEvents.Failure(MasSupportRequiredException()))
+ } else {
+ internalRegisterAction(RegisterAction.StartRegistration)
+ }
}
OnboardingFlow.SignInSignUp,
null -> {
@@ -775,9 +783,17 @@ class OnboardingViewModel @AssistedInject constructor(
private suspend fun onHomeServerEdited(config: HomeServerConnectionConfig, serverTypeOverride: ServerType?, authResult: StartAuthenticationResult) {
when (awaitState().onboardingFlow) {
- OnboardingFlow.SignUp -> internalRegisterAction(RegisterAction.StartRegistration) {
- updateServerSelection(config, serverTypeOverride, authResult)
- _viewEvents.post(OnboardingViewEvents.OnHomeserverEdited)
+ OnboardingFlow.SignUp -> {
+ if (authResult.selectedHomeserver.hasOidcCompatibilityFlow && Config.sunsetConfig is SunsetConfig.Enabled) {
+ // An error is displayed now
+ setState { copy(isLoading = false) }
+ _viewEvents.post(OnboardingViewEvents.Failure(MasSupportRequiredException()))
+ } else {
+ internalRegisterAction(RegisterAction.StartRegistration) {
+ updateServerSelection(config, serverTypeOverride, authResult)
+ _viewEvents.post(OnboardingViewEvents.OnHomeserverEdited)
+ }
+ }
}
OnboardingFlow.SignIn -> {
updateServerSelection(config, serverTypeOverride, authResult)
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedServerSelectionFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedServerSelectionFragment.kt
index 3f34c6ea68..e43de86c13 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedServerSelectionFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedServerSelectionFragment.kt
@@ -11,7 +11,12 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.widget.TextView
+import androidx.core.view.isVisible
import dagger.hilt.android.AndroidEntryPoint
+import im.vector.app.R
+import im.vector.app.config.Config
+import im.vector.app.config.SunsetConfig
import im.vector.app.core.extensions.associateContentStateWith
import im.vector.app.core.extensions.clearErrorOnChange
import im.vector.app.core.extensions.content
@@ -20,20 +25,26 @@ import im.vector.app.core.extensions.realignPercentagesToParent
import im.vector.app.core.extensions.setOnImeDoneListener
import im.vector.app.core.extensions.showKeyboard
import im.vector.app.core.extensions.toReducedUrl
+import im.vector.app.core.resources.BuildMeta
import im.vector.app.core.utils.ensureProtocol
import im.vector.app.core.utils.ensureTrailingSlash
+import im.vector.app.core.utils.openApplicationStore
+import im.vector.app.core.utils.openUrlInChromeCustomTab
import im.vector.app.core.utils.openUrlInExternalBrowser
import im.vector.app.databinding.FragmentFtueServerSelectionCombinedBinding
+import im.vector.app.features.onboarding.MasSupportRequiredException
import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.onboarding.OnboardingFlow
import im.vector.app.features.onboarding.OnboardingViewEvents
import im.vector.app.features.onboarding.OnboardingViewState
import im.vector.lib.strings.CommonStrings
import org.matrix.android.sdk.api.failure.isHomeserverUnavailable
+import javax.inject.Inject
@AndroidEntryPoint
class FtueAuthCombinedServerSelectionFragment :
AbstractFtueAuthFragment() {
+ @Inject lateinit var buildMeta: BuildMeta
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueServerSelectionCombinedBinding {
return FragmentFtueServerSelectionCombinedBinding.inflate(inflater, container, false)
@@ -57,6 +68,22 @@ class FtueAuthCombinedServerSelectionFragment :
}
views.chooseServerGetInTouch.debouncedClicks { openUrlInExternalBrowser(requireContext(), getString(im.vector.app.config.R.string.ftue_ems_url)) }
views.chooseServerSubmit.debouncedClicks { updateServerUrl() }
+ (Config.sunsetConfig as? SunsetConfig.Enabled)?.let { config ->
+ views.chooseServerCardDownloadReplacementApp.debouncedClicks {
+ openApplicationStore(
+ activity = requireActivity(),
+ buildMeta = buildMeta,
+ appId = config.replacementApplicationId,
+ )
+ }
+ views.chooseServerCardDownloadReplacementApp.findViewById(R.id.view_download_replacement_app_learn_more)?.debouncedClicks {
+ openUrlInChromeCustomTab(
+ context = requireContext(),
+ session = null,
+ url = config.learnMoreLink,
+ )
+ }
+ }
views.chooseServerInput.clearErrorOnChange(viewLifecycleOwner)
}
@@ -89,10 +116,30 @@ class FtueAuthCombinedServerSelectionFragment :
}
override fun onError(throwable: Throwable) {
+ val isMasSupportRequiredException = throwable is MasSupportRequiredException
views.chooseServerInput.error = when {
throwable.isHomeserverUnavailable() -> getString(CommonStrings.login_error_homeserver_not_found)
+ isMasSupportRequiredException -> " "
else -> errorFormatter.toHumanReadable(throwable)
}
+ views.chooseServerCardErrorMas.isVisible = isMasSupportRequiredException
+ views.chooseServerCardDownloadReplacementApp.isVisible = isMasSupportRequiredException
+ if (isMasSupportRequiredException) {
+ views.chooseServerSubmit.isEnabled = false
+ }
+ val config = Config.sunsetConfig
+ if (throwable is MasSupportRequiredException && config is SunsetConfig.Enabled) {
+ views.chooseServerCardErrorMas.findViewById(R.id.view_card_error_title).text =
+ getString(CommonStrings.error_mas_not_supported_title, views.chooseServerInput.content())
+ views.chooseServerCardErrorMas.findViewById(R.id.view_card_error_subtitle).text =
+ getString(
+ CommonStrings.error_mas_not_supported_subtitle,
+ config.replacementApplicationName,
+ views.chooseServerInput.content(),
+ )
+ views.chooseServerCardDownloadReplacementApp.findViewById(R.id.view_download_replacement_app_title).text =
+ getString(CommonStrings.view_download_replacement_app_title, config.replacementApplicationName)
+ }
}
private fun String.toReducedUrlKeepingSchemaIfInsecure() = toReducedUrl(keepSchema = this.startsWith("http://"))
diff --git a/vector/src/main/res/drawable-mdpi/replacement_app_icon.png b/vector/src/main/res/drawable-mdpi/replacement_app_icon.png
new file mode 100644
index 0000000000..db4e68bbc2
Binary files /dev/null and b/vector/src/main/res/drawable-mdpi/replacement_app_icon.png differ
diff --git a/vector/src/main/res/drawable-xhdpi/replacement_app_icon.png b/vector/src/main/res/drawable-xhdpi/replacement_app_icon.png
new file mode 100644
index 0000000000..d8e0864606
Binary files /dev/null and b/vector/src/main/res/drawable-xhdpi/replacement_app_icon.png differ
diff --git a/vector/src/main/res/drawable-xxhdpi/replacement_app_icon.png b/vector/src/main/res/drawable-xxhdpi/replacement_app_icon.png
new file mode 100644
index 0000000000..589de41ed3
Binary files /dev/null and b/vector/src/main/res/drawable-xxhdpi/replacement_app_icon.png differ
diff --git a/vector/src/main/res/drawable/card_background.xml b/vector/src/main/res/drawable/card_background.xml
new file mode 100644
index 0000000000..61cefc68ed
--- /dev/null
+++ b/vector/src/main/res/drawable/card_background.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/drawable/card_background_error.xml b/vector/src/main/res/drawable/card_background_error.xml
new file mode 100644
index 0000000000..ee43a08339
--- /dev/null
+++ b/vector/src/main/res/drawable/card_background_error.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/drawable/ic_error.xml b/vector/src/main/res/drawable/ic_error.xml
new file mode 100644
index 0000000000..7ff2b52908
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_error.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/vector/src/main/res/layout/fragment_ftue_combined_login.xml b/vector/src/main/res/layout/fragment_ftue_combined_login.xml
index a589ec6f5a..b7a5fb459d 100644
--- a/vector/src/main/res/layout/fragment_ftue_combined_login.xml
+++ b/vector/src/main/res/layout/fragment_ftue_combined_login.xml
@@ -83,7 +83,8 @@
app:layout_constraintBottom_toTopOf="@id/selectedServerDescription"
app:layout_constraintEnd_toStartOf="@id/editServerButton"
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
- app:layout_constraintTop_toBottomOf="@id/chooseYourServerHeader" />
+ app:layout_constraintTop_toBottomOf="@id/chooseYourServerHeader"
+ tools:text="server.org" />
@@ -112,13 +112,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ app:layout_constraintTop_toBottomOf="@+id/chooseServerCardDownloadReplacementApp" />