Skip to content

Commit

Permalink
Merge pull request #8983 from element-hq/feature/bma/sunsetApplication
Browse files Browse the repository at this point in the history
Sunset application - first step
  • Loading branch information
bmarty authored Jan 13, 2025
2 parents 037958f + 0e4c39b commit e42075a
Show file tree
Hide file tree
Showing 22 changed files with 482 additions and 18 deletions.
6 changes: 6 additions & 0 deletions library/ui-strings/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3641,4 +3641,10 @@
<string name="pill_message_in_room">Message in %s</string>
<string name="pill_message_in_unknown_room">Message in room</string>
<string name="pill_message_unknown_room_or_space">Room/Space</string>

<string name="error_mas_not_supported_title">You can no longer create an account with %1$s using this app</string>
<string name="error_mas_not_supported_subtitle">Download %1$s to use %2$s for your account or choose a different homeserver.</string>
<string name="view_download_replacement_app_title">Download %1$s</string>
<string name="view_download_replacement_app_subtitle">Faster, more secure, and packed with powerful collaboration tools.</string>

</resources>
21 changes: 21 additions & 0 deletions library/ui-styles/src/main/res/values/colors.xml
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,25 @@
<color name="vctr_rich_text_editor_menu_button_background_light">#EEF8F4</color>
<color name="vctr_rich_text_editor_menu_button_background_dark">#1D292A</color>

<!-- Other colors -->
<attr name="vctr_bg_critical_subtle" format="color" />
<color name="vctr_bg_critical_subtle_light">#FFF7F6</color>
<color name="vctr_bg_critical_subtle_dark">#3E0000</color>

<attr name="vctr_border_critical_subtle" format="color" />
<color name="vctr_border_critical_subtle_light">#FFC5BC</color>
<color name="vctr_border_critical_subtle_dark">#710000</color>

<attr name= "vctr_icon_critical_primary" format="color" />
<color name="vctr_icon_critical_primary_light">#D51928</color>
<color name="vctr_icon_critical_primary_dark">#FD3E3C</color>

<attr name= "vctr_text_critical_primary" format="color" />
<color name="vctr_text_critical_primary_light">#D51928</color>
<color name="vctr_text_critical_primary_dark">#FD3E3C</color>

<attr name= "vctr_text_primary" format="color" />
<color name="vctr_text_primary_light">#1B1D22</color>
<color name="vctr_text_primary_dark">#EBEEF2</color>

</resources>
7 changes: 7 additions & 0 deletions library/ui-styles/src/main/res/values/theme_dark.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@
<item name="vctr_list_separator_on_surface">?vctr_system</item>
<item name="vctr_unread_background">?vctr_notice_secondary</item>

<!-- Other colors -->
<item name="vctr_bg_critical_subtle" >@color/vctr_bg_critical_subtle_dark</item>
<item name="vctr_border_critical_subtle" >@color/vctr_border_critical_subtle_dark</item>
<item name= "vctr_icon_critical_primary" >@color/vctr_icon_critical_primary_dark</item>
<item name= "vctr_text_critical_primary" >@color/vctr_text_critical_primary_dark</item>
<item name= "vctr_text_primary" >@color/vctr_text_primary_dark</item>

<!-- Material color -->
<item name="colorPrimary">@color/element_accent_dark</item>
<item name="colorPrimaryVariant">@color/element_accent_dark</item>
Expand Down
7 changes: 7 additions & 0 deletions library/ui-styles/src/main/res/values/theme_light.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@
<item name="vctr_list_separator_on_surface">?vctr_system</item>
<item name="vctr_unread_background">?vctr_notice_secondary</item>

<!-- Other colors -->
<item name="vctr_bg_critical_subtle" >@color/vctr_bg_critical_subtle_light</item>
<item name="vctr_border_critical_subtle" >@color/vctr_border_critical_subtle_light</item>
<item name= "vctr_icon_critical_primary" >@color/vctr_icon_critical_primary_light</item>
<item name= "vctr_text_critical_primary" >@color/vctr_text_critical_primary_light</item>
<item name= "vctr_text_primary" >@color/vctr_text_primary_light</item>

<!-- Material color -->
<item name="colorPrimary">@color/element_accent_light</item>
<item name="colorPrimaryVariant">@color/element_accent_light</item>
Expand Down
11 changes: 11 additions & 0 deletions vector-config/src/main/java/im/vector/app/config/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,15 @@ 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(
learnMoreLink = "https://element.io/app-for-productivity",
replacementApplicationName = "Element X",
replacementApplicationId = "io.element.android.x",
)
}
46 changes: 46 additions & 0 deletions vector-config/src/main/java/im/vector/app/config/SunsetConfig.kt
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")))
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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")
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
// Navigate to the screen to create an account, it will show the error
setState { copy(isLoading = false) }
_viewEvents.post(OnboardingViewEvents.OpenCombinedRegister)
} else {
internalRegisterAction(RegisterAction.StartRegistration)
}
}
OnboardingFlow.SignInSignUp,
null -> {
Expand All @@ -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)
Expand Down Expand Up @@ -924,7 +940,10 @@ private fun LoginMode.supportsSignModeScreen(): Boolean {
return when (this) {
LoginMode.Password,
is LoginMode.SsoAndPassword -> true
is LoginMode.Sso,
is LoginMode.Sso -> {
// In this case, an error will be displayed in the next screen
hasOidcCompatibilityFlow
}
LoginMode.Unknown,
LoginMode.Unsupported -> false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.autofill.HintConstants
import androidx.core.text.isDigitsOnly
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.withState
import com.google.android.material.dialog.MaterialAlertDialogBuilder
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.clearErrorOnChange
import im.vector.app.core.extensions.content
import im.vector.app.core.extensions.editText
Expand All @@ -31,6 +35,9 @@ import im.vector.app.core.extensions.realignPercentagesToParent
import im.vector.app.core.extensions.setOnFocusLostListener
import im.vector.app.core.extensions.setOnImeDoneListener
import im.vector.app.core.extensions.toReducedUrl
import im.vector.app.core.resources.BuildMeta
import im.vector.app.core.utils.openApplicationStore
import im.vector.app.core.utils.openUrlInChromeCustomTab
import im.vector.app.databinding.FragmentFtueCombinedRegisterBinding
import im.vector.app.features.login.LoginMode
import im.vector.app.features.login.SSORedirectRouterActivity
Expand All @@ -52,12 +59,14 @@ import org.matrix.android.sdk.api.failure.isRegistrationDisabled
import org.matrix.android.sdk.api.failure.isUsernameInUse
import org.matrix.android.sdk.api.failure.isWeakPassword
import reactivecircus.flowbinding.android.widget.textChanges
import javax.inject.Inject

private const val MINIMUM_PASSWORD_LENGTH = 8

@AndroidEntryPoint
class FtueAuthCombinedRegisterFragment :
AbstractSSOFtueAuthFragment<FragmentFtueCombinedRegisterBinding>() {
@Inject lateinit var buildMeta: BuildMeta

override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueCombinedRegisterBinding {
return FragmentFtueCombinedRegisterBinding.inflate(inflater, container, false)
Expand Down Expand Up @@ -181,7 +190,8 @@ class FtueAuthCombinedRegisterFragment :
}

private fun setupUi(state: OnboardingViewState) {
views.selectedServerName.text = state.selectedHomeserver.userFacingUrl.toReducedUrl()
val serverName = state.selectedHomeserver.userFacingUrl.toReducedUrl()
views.selectedServerName.text = serverName

if (state.isLoading) {
// Ensure password is hidden
Expand All @@ -201,6 +211,47 @@ class FtueAuthCombinedRegisterFragment :
is LoginMode.SsoAndPassword -> renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode)
else -> hideSsoProviders()
}

(Config.sunsetConfig as? SunsetConfig.Enabled)?.let { config ->
val isMasSupportRequired = state.selectedHomeserver.hasOidcCompatibilityFlow
views.serverSelectionSpacing.isVisible = !isMasSupportRequired
views.serverSelectionDivider.isVisible = !isMasSupportRequired
views.chooseServerCardErrorMas.isVisible = isMasSupportRequired
views.chooseServerCardDownloadReplacementApp.isVisible = isMasSupportRequired

if (isMasSupportRequired) {
views.chooseServerCardErrorMas.findViewById<TextView>(R.id.view_card_error_title).text =
getString(CommonStrings.error_mas_not_supported_title, serverName)
views.chooseServerCardErrorMas.findViewById<TextView>(R.id.view_card_error_subtitle).text =
getString(
CommonStrings.error_mas_not_supported_subtitle,
config.replacementApplicationName,
serverName,
)
views.chooseServerCardDownloadReplacementApp.findViewById<TextView>(R.id.view_download_replacement_app_title).text =
getString(CommonStrings.view_download_replacement_app_title, config.replacementApplicationName)

views.chooseServerCardDownloadReplacementApp.debouncedClicks {
openApplicationStore(
activity = requireActivity(),
buildMeta = buildMeta,
appId = config.replacementApplicationId,
)
}
views.chooseServerCardDownloadReplacementApp.findViewById<View>(R.id.view_download_replacement_app_learn_more)?.debouncedClicks {
openUrlInChromeCustomTab(
context = requireContext(),
session = null,
url = config.learnMoreLink,
)
}

// Disable form
views.createAccountInput.isEnabled = false
views.createAccountPasswordInput.isEnabled = false
views.createAccountSubmit.isEnabled = false
}
}
}

private fun renderSsoProviders(deviceId: String?, loginMode: LoginMode) {
Expand Down
Loading

0 comments on commit e42075a

Please sign in to comment.