From f580ae73a70ccb84be84c0b89b55b5fa3e9cbafa Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Wed, 22 Apr 2026 12:43:19 -0600 Subject: [PATCH] Add per-site GutenbergKit opt-in and announcement bottom sheet Surfaces the new GutenbergKit editor to users ahead of its default rollout. Adds a one-time announcement bottom sheet on app start and a per-site toggle in Site Settings to opt in or out. Gated on the remote feature flag and skipped for sites where the block editor is disabled. --- .../android/ui/main/WPMainActivity.java | 6 ++ .../android/ui/posts/EditorLauncher.kt | 4 +- ...nbergKitAnnouncementBottomSheetFragment.kt | 98 +++++++++++++++++++ .../ui/posts/GutenbergKitFeatureChecker.kt | 40 ++++---- .../wordpress/android/ui/prefs/AppPrefs.java | 48 +++++++++ .../android/ui/prefs/AppPrefsWrapper.kt | 10 ++ .../ui/prefs/SiteSettingsFragment.java | 19 +++- .../viewmodel/main/WPMainActivityViewModel.kt | 17 ++++ ...utenberg_kit_announcement_bottom_sheet.xml | 75 ++++++++++++++ WordPress/src/main/res/values/key_strings.xml | 1 + WordPress/src/main/res/values/strings.xml | 10 ++ WordPress/src/main/res/xml/site_settings.xml | 6 ++ .../posts/GutenbergKitFeatureCheckerTest.kt | 33 ++++++- .../main/WPMainActivityViewModelTest.kt | 4 + 14 files changed, 350 insertions(+), 21 deletions(-) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitAnnouncementBottomSheetFragment.kt create mode 100644 WordPress/src/main/res/layout/gutenberg_kit_announcement_bottom_sheet.xml diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java index 0354b98d9aac..7dcb0f4dbfa6 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java @@ -107,6 +107,7 @@ import org.wordpress.android.ui.photopicker.MediaPickerLauncher; import org.wordpress.android.ui.posts.EditorConstants; import org.wordpress.android.ui.posts.EditorLauncher; +import org.wordpress.android.ui.posts.GutenbergKitAnnouncementBottomSheetFragment; import org.wordpress.android.ui.posts.PostUtils.EntryPoint; import org.wordpress.android.ui.prefs.AppPrefs; import org.wordpress.android.ui.prefs.AppSettingsActivity; @@ -672,6 +673,11 @@ private void initViewModel() { .show(getSupportFragmentManager(), FeatureAnnouncementDialogFragment.TAG); }); + mViewModel.getOnGutenbergKitAnnouncementRequested().observe(this, siteUrl -> { + GutenbergKitAnnouncementBottomSheetFragment.newInstance(siteUrl) + .show(getSupportFragmentManager(), GutenbergKitAnnouncementBottomSheetFragment.TAG); + }); + mFloatingActionButton.setOnClickListener(v -> { PageType selectedPage = getSelectedPage(); if (selectedPage != null) mViewModel.onFabClicked(getSelectedSite(), selectedPage); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditorLauncher.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditorLauncher.kt index 3206849776e2..560569ee026e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditorLauncher.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditorLauncher.kt @@ -89,10 +89,10 @@ class EditorLauncher @Inject constructor( * Determines if GutenbergKit editor should be used based on feature flags and post content. */ private fun shouldUseGutenbergKitEditor(params: EditorLauncherParams): Boolean { - val featureState = gutenbergKitFeatureChecker.getFeatureState() + val site = params.siteSource.getSite(siteStore) + val featureState = gutenbergKitFeatureChecker.getFeatureState(site) val isGutenbergFeatureEnabled = featureState.isGutenbergKitEnabled - val site = params.siteSource.getSite(siteStore) return when { !isGutenbergFeatureEnabled -> { logFeatureDisabledReason(featureState) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitAnnouncementBottomSheetFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitAnnouncementBottomSheetFragment.kt new file mode 100644 index 000000000000..5f8c631e4e03 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitAnnouncementBottomSheetFragment.kt @@ -0,0 +1,98 @@ +package org.wordpress.android.ui.posts + +import android.os.Bundle +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.TextPaint +import android.text.method.LinkMovementMethod +import android.text.style.ClickableSpan +import android.text.style.ForegroundColorSpan +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.google.android.material.button.MaterialButton +import com.google.android.material.color.MaterialColors +import org.wordpress.android.R +import org.wordpress.android.ui.WPWebViewActivity +import org.wordpress.android.ui.prefs.AppPrefs + +/** + * One-time announcement bottom sheet for the upcoming GutenbergKit editor. + * Opts the user in for the currently selected site, or dismisses. Provides a + * "Learn more" link that opens a web page. + */ +class GutenbergKitAnnouncementBottomSheetFragment : BottomSheetDialogFragment() { + private var siteUrl: String? = null + var onOptIn: (() -> Unit)? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = inflater.inflate(R.layout.gutenberg_kit_announcement_bottom_sheet, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // Prevent Material's BottomSheetDialog from applying status-bar insets as top padding. + ViewCompat.setOnApplyWindowInsetsListener(view) { v, _ -> + v.setPadding(v.paddingLeft, 0, v.paddingRight, v.paddingBottom) + WindowInsetsCompat.CONSUMED + } + + siteUrl = arguments?.getString(ARG_SITE_URL) + + bindBodyWithLearnMore(view.findViewById(R.id.body_text)) + + view.findViewById(R.id.try_now_button).setOnClickListener { + siteUrl?.let { AppPrefs.setGutenbergKitEnabledForSite(it, true) } + onOptIn?.invoke() + dismiss() + } + + view.findViewById(R.id.maybe_later_button).setOnClickListener { + dismiss() + } + } + + private fun bindBodyWithLearnMore(textView: TextView) { + val body = getString(R.string.gutenberg_kit_announcement_body) + val learnMore = getString(R.string.gutenberg_kit_announcement_learn_more) + val combined = SpannableStringBuilder(body).append(' ').append(learnMore) + val start = combined.length - learnMore.length + val end = combined.length + val color = MaterialColors.getColor(textView, androidx.appcompat.R.attr.colorPrimary) + combined.setSpan(object : ClickableSpan() { + override fun onClick(widget: View) { + WPWebViewActivity.openURL( + requireContext(), + getString(R.string.gutenberg_kit_learn_more_url) + ) + } + + override fun updateDrawState(ds: TextPaint) { + super.updateDrawState(ds) + ds.isUnderlineText = false + } + }, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + combined.setSpan(ForegroundColorSpan(color), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + textView.text = combined + textView.movementMethod = LinkMovementMethod.getInstance() + } + + companion object { + const val TAG = "GutenbergKitAnnouncementBottomSheetFragment" + private const val ARG_SITE_URL = "site_url" + + @JvmStatic + fun newInstance(siteUrl: String?): GutenbergKitAnnouncementBottomSheetFragment { + return GutenbergKitAnnouncementBottomSheetFragment().apply { + arguments = Bundle().apply { putString(ARG_SITE_URL, siteUrl) } + } + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitFeatureChecker.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitFeatureChecker.kt index f3b007d5d2db..74f7b8b8b4cd 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitFeatureChecker.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitFeatureChecker.kt @@ -1,5 +1,7 @@ package org.wordpress.android.ui.posts +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.ui.prefs.AppPrefsWrapper import org.wordpress.android.ui.prefs.experimentalfeatures.ExperimentalFeatures import org.wordpress.android.ui.prefs.experimentalfeatures.ExperimentalFeatures.Feature import org.wordpress.android.util.config.GutenbergKitFeature @@ -13,7 +15,8 @@ import javax.inject.Singleton @Singleton class GutenbergKitFeatureChecker @Inject constructor( private val experimentalFeatures: ExperimentalFeatures, - private val gutenbergKitFeature: GutenbergKitFeature + private val gutenbergKitFeature: GutenbergKitFeature, + private val appPrefsWrapper: AppPrefsWrapper ) { /** * Data class containing the state of all GutenbergKit-related feature flags. @@ -21,41 +24,44 @@ class GutenbergKitFeatureChecker @Inject constructor( data class FeatureState( val isExperimentalBlockEditorEnabled: Boolean, val isGutenbergKitFeatureEnabled: Boolean, - val isDisableExperimentalBlockEditorEnabled: Boolean + val isDisableExperimentalBlockEditorEnabled: Boolean, + val isEnabledForSite: Boolean = false ) { /** * Determines if GutenbergKit should be enabled based on the feature states. */ val isGutenbergKitEnabled: Boolean - get() = (isExperimentalBlockEditorEnabled || isGutenbergKitFeatureEnabled) && + get() = (isExperimentalBlockEditorEnabled || + isGutenbergKitFeatureEnabled || + isEnabledForSite) && !isDisableExperimentalBlockEditorEnabled } /** - * Gets the current state of all GutenbergKit-related feature flags. - * - * @return FeatureState containing all flag states and the computed enabled state + * Gets the current state of all GutenbergKit-related feature flags for the given site (if any). */ - fun getFeatureState(): FeatureState { + @JvmOverloads + fun getFeatureState(site: SiteModel? = null): FeatureState { return FeatureState( isExperimentalBlockEditorEnabled = experimentalFeatures.isEnabled(Feature.EXPERIMENTAL_BLOCK_EDITOR), isGutenbergKitFeatureEnabled = gutenbergKitFeature.isEnabled(), isDisableExperimentalBlockEditorEnabled = experimentalFeatures.isEnabled( Feature.DISABLE_EXPERIMENTAL_BLOCK_EDITOR - ) + ), + isEnabledForSite = site?.url?.let { appPrefsWrapper.isGutenbergKitEnabledForSite(it) } ?: false ) } /** - * Determines if GutenbergKit is enabled based on feature flags. - * - * The feature is enabled if: - * - Either the experimental block editor is enabled OR the GutenbergKit feature flag is on - * - AND the disable experimental block editor flag is NOT enabled - * - * @return true if GutenbergKit should be enabled, false otherwise + * Determines if GutenbergKit is enabled based on feature flags (and optional per-site opt-in). */ - fun isGutenbergKitEnabled(): Boolean { - return getFeatureState().isGutenbergKitEnabled + @JvmOverloads + fun isGutenbergKitEnabled(site: SiteModel? = null): Boolean { + return getFeatureState(site).isGutenbergKitEnabled } + + /** + * Whether the user-facing remote feature flag is on (controls opt-in surfaces). + */ + fun isGutenbergKitRemoteFeatureEnabled(): Boolean = gutenbergKitFeature.isEnabled() } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java index d25c677ce6ab..f094f1c86259 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java @@ -126,6 +126,7 @@ public enum DeletablePrefKey implements PrefKey { SHOULD_AUTO_ENABLE_GUTENBERG_FOR_THE_NEW_POSTS_PHASE_2, GUTENBERG_OPT_IN_DIALOG_SHOWN, GUTENBERG_FOCAL_POINT_PICKER_TOOLTIP_SHOWN, + GUTENBERG_KIT_OPT_IN_SITES, POST_LIST_AUTHOR_FILTER, POST_LIST_VIEW_LAYOUT_TYPE, @@ -318,6 +319,8 @@ public enum UndeletablePrefKey implements PrefKey { // These preferences persist across logout/login cycles. IS_TRACK_NETWORK_REQUESTS_ENABLED, TRACK_NETWORK_REQUESTS_RETENTION_PERIOD, + + GUTENBERG_KIT_ANNOUNCEMENT_SHOWN, } static SharedPreferences prefs() { @@ -827,6 +830,51 @@ public static boolean isGutenbergInfoPopupDisplayed(String siteURL) { return urls != null && urls.contains(siteURL); } + public static boolean isGutenbergKitEnabledForSite(String siteURL) { + if (TextUtils.isEmpty(siteURL)) { + return false; + } + Set urls; + try { + urls = prefs().getStringSet(DeletablePrefKey.GUTENBERG_KIT_OPT_IN_SITES.name(), null); + } catch (ClassCastException exp) { + return false; + } + return urls != null && urls.contains(siteURL); + } + + public static void setGutenbergKitEnabledForSite(String siteURL, boolean enabled) { + if (TextUtils.isEmpty(siteURL)) { + return; + } + Set urls; + try { + urls = prefs().getStringSet(DeletablePrefKey.GUTENBERG_KIT_OPT_IN_SITES.name(), null); + } catch (ClassCastException exp) { + return; + } + + Set newUrls = new HashSet<>(); + if (urls != null) { + newUrls.addAll(urls); + } + if (enabled) { + newUrls.add(siteURL); + } else { + newUrls.remove(siteURL); + } + + prefs().edit().putStringSet(DeletablePrefKey.GUTENBERG_KIT_OPT_IN_SITES.name(), newUrls).apply(); + } + + public static boolean wasGutenbergKitAnnouncementShown() { + return prefs().getBoolean(UndeletablePrefKey.GUTENBERG_KIT_ANNOUNCEMENT_SHOWN.name(), false); + } + + public static void setGutenbergKitAnnouncementShown(boolean shown) { + prefs().edit().putBoolean(UndeletablePrefKey.GUTENBERG_KIT_ANNOUNCEMENT_SHOWN.name(), shown).apply(); + } + public static void setGutenbergInfoPopupDisplayed(String siteURL, boolean isDisplayed) { if (isGutenbergInfoPopupDisplayed(siteURL)) { return; diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt index a90cddd978b2..895209b5707a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt @@ -553,6 +553,16 @@ class AppPrefsWrapper @Inject constructor(val buildConfigWrapper: BuildConfigWra fun setStatsNewStatsSuggestionLastDismissedAt(timestamp: Long) = AppPrefs.setStatsNewStatsSuggestionLastDismissedAt(timestamp) + fun isGutenbergKitEnabledForSite(siteUrl: String?): Boolean = + AppPrefs.isGutenbergKitEnabledForSite(siteUrl) + + fun setGutenbergKitEnabledForSite(siteUrl: String?, enabled: Boolean) = + AppPrefs.setGutenbergKitEnabledForSite(siteUrl, enabled) + + var wasGutenbergKitAnnouncementShown: Boolean + get() = AppPrefs.wasGutenbergKitAnnouncementShown() + set(value) = AppPrefs.setGutenbergKitAnnouncementShown(value) + companion object { private const val LIGHT_MODE_ID = 0 private const val DARK_MODE_ID = 1 diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsFragment.java index 5657f3a850b6..ebf3a53c9ff8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsFragment.java @@ -228,6 +228,7 @@ public class SiteSettingsFragment extends PreferenceFragment // Writing settings private WPSwitchPreference mGutenbergDefaultForNewPosts; private WPSwitchPreference mUseThemeStylesPref; + private WPSwitchPreference mGutenbergKitPref; private DetailListPreference mCategoryPref; private DetailListPreference mFormatPref; private WPPreference mDateFormatPref; @@ -848,6 +849,8 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { AnalyticsUtils.refreshMetadata(mAccountStore, mSiteStore); } else if (preference == mUseThemeStylesPref) { mSiteSettings.setUseThemeStyles((Boolean) newValue); + } else if (preference == mGutenbergKitPref) { + AppPrefs.setGutenbergKitEnabledForSite(mSite.getUrl(), (Boolean) newValue); } else if (preference == mBloggingPromptsPref) { final boolean isEnabled = (boolean) newValue; mPromptsSettingsHelper.updatePromptsCardEnabledBlocking(mSite.getId(), isEnabled); @@ -1037,6 +1040,10 @@ public void initPreferences() { (WPSwitchPreference) getChangePref(R.string.pref_key_use_theme_styles); mUseThemeStylesPref.setChecked(mSiteSettings.getUseThemeStyles()); + mGutenbergKitPref = + (WPSwitchPreference) getChangePref(R.string.pref_key_gutenberg_kit_enabled); + mGutenbergKitPref.setChecked(AppPrefs.isGutenbergKitEnabledForSite(mSite.getUrl())); + mSiteAcceleratorSettings = (PreferenceScreen) getClickPref(R.string.pref_key_site_accelerator_settings); mSiteAcceleratorSettingsNested = (PreferenceScreen) getClickPref(R.string.pref_key_site_accelerator_settings_nested); @@ -1083,6 +1090,12 @@ public void initPreferences() { WPPrefUtils.removePreference(this, R.string.pref_key_site_editor, R.string.pref_key_use_theme_styles); } + // hide the GutenbergKit opt-in switch unless the remote feature flag is on + if (!mGutenbergKitFeatureChecker.isGutenbergKitRemoteFeatureEnabled()) { + WPPrefUtils.removePreference(this, R.string.pref_key_site_editor, + R.string.pref_key_gutenberg_kit_enabled); + } + // hide Admin options depending of capabilities on this site if ((!isAccessedViaWPComRest && !mSite.isSelfHostedAdmin()) || (isAccessedViaWPComRest && !mSite.getHasCapabilityManageOptions())) { @@ -1207,7 +1220,8 @@ public void setEditingEnabled(boolean enabled) { mDateFormatPref, mTimeFormatPref, mTimezonePref, mBloggingRemindersPref, mPostsPerPagePref, mAmpPref, mDeleteSitePref, mJpMonitorActivePref, mJpMonitorEmailNotesPref, mJpSsoPref, mJpMonitorWpNotesPref, mJpBruteForcePref, mJpAllowlistPref, mJpMatchEmailPref, mJpUseTwoFactorPref, - mGutenbergDefaultForNewPosts, mUseThemeStylesPref, mHomepagePref, mBloggingPromptsPref + mGutenbergDefaultForNewPosts, mUseThemeStylesPref, mGutenbergKitPref, mHomepagePref, + mBloggingPromptsPref }; for (Preference preference : editablePreference) { @@ -1552,6 +1566,9 @@ public void setPreferencesFromSiteSettings() { mWeekStartPref.setSummary(mWeekStartPref.getEntry()); mGutenbergDefaultForNewPosts.setChecked(SiteUtils.isBlockEditorDefaultForNewPost(mSite)); mUseThemeStylesPref.setChecked(mSiteSettings.getUseThemeStyles()); + if (mGutenbergKitPref != null) { + mGutenbergKitPref.setChecked(AppPrefs.isGutenbergKitEnabledForSite(mSite.getUrl())); + } setAdFreeHostingChecked(mSiteSettings.isAdFreeHostingEnabled()); boolean checked = mSiteSettings.isImprovedSearchEnabled() || mSiteSettings.getJetpackSearchEnabled(); mImprovedSearch.setChecked(checked); diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModel.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModel.kt index 411a4e08cc92..eed5d71eb7cc 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModel.kt @@ -29,12 +29,14 @@ import org.wordpress.android.ui.main.analytics.MainCreateSheetTracker import org.wordpress.android.ui.main.utils.MainCreateSheetHelper import org.wordpress.android.ui.mysite.SelectedSiteRepository import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptAttribution +import org.wordpress.android.ui.posts.GutenbergKitFeatureChecker import org.wordpress.android.ui.prefs.AppPrefsWrapper import org.wordpress.android.ui.prefs.privacy.banner.domain.ShouldAskPrivacyConsent import org.wordpress.android.ui.utils.UiString.UiStringText import org.wordpress.android.ui.whatsnew.FeatureAnnouncementProvider import org.wordpress.android.util.BuildConfigWrapper import org.wordpress.android.util.FluxCUtils +import org.wordpress.android.util.SiteUtils import org.wordpress.android.util.SiteUtils.hasFullAccessToContent import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper import org.wordpress.android.viewmodel.Event @@ -58,6 +60,7 @@ class WPMainActivityViewModel @Inject constructor( private val shouldAskPrivacyConsent: ShouldAskPrivacyConsent, private val mainCreateSheetHelper: MainCreateSheetHelper, private val mainCreateSheetTracker: MainCreateSheetTracker, + private val gutenbergKitFeatureChecker: GutenbergKitFeatureChecker, @Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher, ) : ScopedViewModel(mainDispatcher) { private var isStarted = false @@ -79,6 +82,9 @@ class WPMainActivityViewModel @Inject constructor( private val _onFeatureAnnouncementRequested = SingleLiveEvent() val onFeatureAnnouncementRequested: LiveData = _onFeatureAnnouncementRequested + private val _onGutenbergKitAnnouncementRequested = SingleLiveEvent() + val onGutenbergKitAnnouncementRequested: LiveData = _onGutenbergKitAnnouncementRequested + private val _createPostWithBloggingPrompt = SingleLiveEvent() val createPostWithBloggingPrompt: LiveData = _createPostWithBloggingPrompt @@ -253,6 +259,17 @@ class WPMainActivityViewModel @Inject constructor( setMainFabUiState(showFab, site, page) checkAndShowFeatureAnnouncement() + checkAndShowGutenbergKitAnnouncement(site) + } + + private fun checkAndShowGutenbergKitAnnouncement(site: SiteModel?) { + if (!gutenbergKitFeatureChecker.isGutenbergKitRemoteFeatureEnabled()) return + if (appPrefsWrapper.wasGutenbergKitAnnouncementShown) return + if (site == null) return + if (!SiteUtils.isBlockEditorDefaultForNewPost(site)) return + + appPrefsWrapper.wasGutenbergKitAnnouncementShown = true + _onGutenbergKitAnnouncementRequested.postValue(site.url) } private fun checkAndShowFeatureAnnouncement() { diff --git a/WordPress/src/main/res/layout/gutenberg_kit_announcement_bottom_sheet.xml b/WordPress/src/main/res/layout/gutenberg_kit_announcement_bottom_sheet.xml new file mode 100644 index 000000000000..98388cb033b5 --- /dev/null +++ b/WordPress/src/main/res/layout/gutenberg_kit_announcement_bottom_sheet.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WordPress/src/main/res/values/key_strings.xml b/WordPress/src/main/res/values/key_strings.xml index efee36015b10..516cf1a01f01 100644 --- a/WordPress/src/main/res/values/key_strings.xml +++ b/WordPress/src/main/res/values/key_strings.xml @@ -56,6 +56,7 @@ wp_pref_key_optimize_video wp_pref_key_gutenberg_default_for_new_posts wp_pref_key_use_theme_styles + wp_pref_key_gutenberg_kit_enabled wp_pref_site_default_video_width wp_pref_site_default_encoder_bitrate wp_pref_site_discussion diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index a05deea3b4fb..2cab7ecfce3b 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -700,6 +700,16 @@ Edit new posts and pages with the block editor Use Theme Styles Make the block editor look like your theme + Try the new editor + Opt in to the next-generation block editor for this site + + + A better block editor is coming + The next generation block editor is coming – and it brings all of the long-missing WordPress Core blocks. You can try it now on this site and switch back anytime from Site Settings.\n\nStarting in May, it will become the default editor for everyone. + Try it now + Maybe later + Learn more + https://wordpress.com/support/editors/ Password updated To reconnect the app to your self-hosted site, enter the site\'s new password here. Homepage Settings diff --git a/WordPress/src/main/res/xml/site_settings.xml b/WordPress/src/main/res/xml/site_settings.xml index fc885caed522..1b0074851272 100644 --- a/WordPress/src/main/res/xml/site_settings.xml +++ b/WordPress/src/main/res/xml/site_settings.xml @@ -138,6 +138,12 @@ android:summary="@string/site_settings_use_theme_styles_summary" android:title="@string/site_settings_use_theme_styles" /> + + diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/GutenbergKitFeatureCheckerTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/GutenbergKitFeatureCheckerTest.kt index 35b22ab7a8d2..b0fb55eddf36 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/posts/GutenbergKitFeatureCheckerTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/GutenbergKitFeatureCheckerTest.kt @@ -7,6 +7,8 @@ import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.junit.MockitoJUnitRunner import org.mockito.kotlin.whenever +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.ui.prefs.AppPrefsWrapper import org.wordpress.android.ui.prefs.experimentalfeatures.ExperimentalFeatures import org.wordpress.android.ui.prefs.experimentalfeatures.ExperimentalFeatures.Feature import org.wordpress.android.util.config.GutenbergKitFeature @@ -19,11 +21,14 @@ class GutenbergKitFeatureCheckerTest { @Mock private lateinit var gutenbergKitFeature: GutenbergKitFeature + @Mock + private lateinit var appPrefsWrapper: AppPrefsWrapper + private lateinit var featureChecker: GutenbergKitFeatureChecker @Before fun setUp() { - featureChecker = GutenbergKitFeatureChecker(experimentalFeatures, gutenbergKitFeature) + featureChecker = GutenbergKitFeatureChecker(experimentalFeatures, gutenbergKitFeature, appPrefsWrapper) } // Helper method to setup mock behavior @@ -231,6 +236,32 @@ class GutenbergKitFeatureCheckerTest { } } + @Test + fun `isGutenbergKitEnabled returns true when only per-site opt-in is set`() { + setupFeatureFlags( + experimentalBlockEditor = false, + gutenbergKitEnabled = false, + disableExperimentalBlockEditor = false + ) + val site = SiteModel().apply { url = "https://example.com" } + whenever(appPrefsWrapper.isGutenbergKitEnabledForSite("https://example.com")).thenReturn(true) + + assertThat(featureChecker.isGutenbergKitEnabled(site)).isTrue() + } + + @Test + fun `disable flag overrides per-site opt-in`() { + setupFeatureFlags( + experimentalBlockEditor = false, + gutenbergKitEnabled = false, + disableExperimentalBlockEditor = true + ) + val site = SiteModel().apply { url = "https://example.com" } + whenever(appPrefsWrapper.isGutenbergKitEnabledForSite("https://example.com")).thenReturn(true) + + assertThat(featureChecker.isGutenbergKitEnabled(site)).isFalse() + } + @Test fun `feature is enabled when at least one enabling flag is true and disable flag is false`() { val enabledTestCases = listOf( diff --git a/WordPress/src/test/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModelTest.kt index 3254941410d0..0423ebfbc2da 100644 --- a/WordPress/src/test/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModelTest.kt @@ -98,6 +98,9 @@ class WPMainActivityViewModelTest : BaseUnitTest() { @Mock private lateinit var mainCreateSheetTracker: MainCreateSheetTracker + @Mock + private lateinit var gutenbergKitFeatureChecker: org.wordpress.android.ui.posts.GutenbergKitFeatureChecker + private val featureAnnouncement = FeatureAnnouncement( "14.7", 2, @@ -150,6 +153,7 @@ class WPMainActivityViewModelTest : BaseUnitTest() { shouldAskPrivacyConsent, mainCreateSheetHelper, mainCreateSheetTracker, + gutenbergKitFeatureChecker, NoDelayCoroutineDispatcher(), ) viewModel.onFeatureAnnouncementRequested.observeForever(