Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ enum class AnalyticsEvent(val siteless: Boolean = false) {
SITE_PICKER_SITE_DISCOVERY(siteless = true),
SITE_PICKER_JETPACK_TIMEOUT_ERROR_SHOWN(siteless = true),
SITE_PICKER_JETPACK_TIMEOUT_CONTACT_SUPPORT_CLICKED(siteless = true),
SITE_PICKER_CREATE_SITE_TAPPED(siteless = true),
LOGIN_WOOCOMMERCE_SITE_CREATED(siteless = true),

// -- Dashboard
DASHBOARD_PULLED_TO_REFRESH,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ import androidx.navigation.fragment.navArgs
import com.woocommerce.android.R
import com.woocommerce.android.databinding.FragmentWpcomWebviewBinding
import com.woocommerce.android.extensions.navigateBackWithNotice
import com.woocommerce.android.extensions.navigateBackWithResult
import com.woocommerce.android.ui.base.BaseFragment
import com.woocommerce.android.ui.common.wpcomwebview.WPComWebViewFragment.UrlComparisonMode.EQUALITY
import com.woocommerce.android.ui.common.wpcomwebview.WPComWebViewFragment.UrlComparisonMode.PARTIAL
import com.woocommerce.android.ui.main.MainActivity.Companion.BackPressListener
import com.woocommerce.android.util.WooLog
import com.woocommerce.android.util.WooLog.T
import dagger.hilt.android.AndroidEntryPoint
import org.wordpress.android.fluxc.network.UserAgent
import javax.inject.Inject
Expand All @@ -28,11 +31,16 @@ import javax.inject.Inject
class WPComWebViewFragment : BaseFragment(R.layout.fragment_wpcom_webview), UrlInterceptor, BackPressListener {
companion object {
const val WEBVIEW_RESULT = "webview-result"
const val WEBVIEW_RESULT_WITH_URL = "webview-result-with-url"
const val WEBVIEW_DISMISSED = "webview-dismissed"
const val WEBVIEW_STORE_CHECKOUT_STRING = "checkout/thank-you/"
const val WEBVIEW_STORE_URL_KEY = "store-url-key"
Comment on lines +36 to +37
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before starting a full review for the PR @0nko, I wanted to raise the point below 🙂, if you feel like what I'm raising below doesn't make sense, or if the time doesn't allow it, please let me know, and I'll review the PR as is.

I feel like these constants and the logic below to extract the site URL don't belong in this class, this fragment was supposed to be a reusable screen for the authenticated WPCom logic, with the sole responsibility of loading a URL and then informing the parent screens when a specific URL was loaded as a termination of a specific flow, but with the current logic, it's responsible for more than this, and it has logic that's not reusable or part of other flows.
If the siteUrl extraction logic needed only the last URL of the flow, then I would've suggested to update the logic of the screen to return the last URL, and make the parsing of this URL a responsibility of the calling Fragment.
But given how it has to be done, I suggest we create a separate screen for this, I believe we can use the Composable component WCWebView, it supports WPCom authentication, and it's flexible enough, so that we can have the URL parsing and extraction easily separated. This will have some other advantages:

  1. Remove the responsibility of waiting from the site picker, it already handles too much, and by being implemented in the new screen, we can use a better ui/text for the loading if needed (something other than "verifying your site" which sounds weird for a site that we were responsible for creating 😅).
  2. Then in the site picker, the onStoreCreated can be very simplified, basically the same implementation as onSiteAddressReceived.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the great suggestions, @hichamboushaba! I've been so focused on making the store creation flow load the new site automatically that I didn't stop and think about how to properly separate the new functionality from the existing components, thinking this is all just a temporary solution anyway...

As you suggested, we can use the WCWebView in its own Composable screen, with a ViewModel, which can then be used as a base for Iteration 2. I started working on it but I can already see it will be a bit more work.

Considering the circumstances, could we go ahead with the current PR (since it's already providing the desired functionality), and in the meantime, I'll refactor the code, as I mentioned above?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good @0nko 🙂, thanks for this.
For the review, I added a comment about a potential issue that we want to fix before releasing this, and for the rest, I will review the code and functionality today or tomorrow morning.

}

private val webViewClient by lazy { WPComWebViewClient(this) }
private val navArgs: WPComWebViewFragmentArgs by navArgs()
private var siteUrls = ArrayList<String>()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are multiple address aliases associated with a new site. That's why a list of addresses is returned so that the site can be loaded and selected.

private var canNavigate = true

@Inject lateinit var wpcomWebViewAuthenticator: WPComWebViewAuthenticator

Expand All @@ -52,20 +60,47 @@ class WPComWebViewFragment : BaseFragment(R.layout.fragment_wpcom_webview), UrlI
}
}
this.settings.javaScriptEnabled = true
this.settings.domStorageEnabled = true
settings.userAgentString = userAgent.userAgent
}

siteUrls = savedInstanceState?.getStringArrayList(WEBVIEW_STORE_URL_KEY) ?: siteUrls

wpcomWebViewAuthenticator.authenticateAndLoadUrl(binding.webView, navArgs.urlToLoad)
}

override fun onSaveInstanceState(outState: Bundle) {
outState.putStringArrayList(WEBVIEW_STORE_URL_KEY, siteUrls)
super.onSaveInstanceState(outState)
}

override fun onLoadUrl(url: String) {
fun String.matchesUrl(url: String) = when (navArgs.urlComparisonMode) {
PARTIAL -> url.contains(this, ignoreCase = true)
EQUALITY -> equals(url, ignoreCase = true)
}

if (isAdded && navArgs.urlToTriggerExit?.matchesUrl(url) == true) {
navigateBackWithNotice(WEBVIEW_RESULT)
extractSiteUrl(url)

if (isAdded && navArgs.urlToTriggerExit?.matchesUrl(url) == true && canNavigate) {
canNavigate = false
if (siteUrls.isEmpty()) {
navigateBackWithNotice(WEBVIEW_RESULT)
} else {
navigateBackWithResult(WEBVIEW_RESULT_WITH_URL, siteUrls)
WooLog.d(T.LOGIN, "Site creation URLs: ${siteUrls.joinToString()}")
}
}
}

private fun extractSiteUrl(url: String) {
"$WEBVIEW_STORE_CHECKOUT_STRING.+/".toRegex().find(url)?.range?.let { range ->
val start = range.first + WEBVIEW_STORE_CHECKOUT_STRING.length
val end = range.last
val siteUrl = url.substring(start, end)
if (!siteUrls.contains(siteUrl)) {
siteUrls.add(siteUrl)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import com.woocommerce.android.ui.sitepicker.SitePickerViewModel.SitePickerState
import com.woocommerce.android.ui.sitepicker.SitePickerViewModel.SitePickerState.WooNotFoundState
import com.woocommerce.android.ui.sitepicker.sitediscovery.SitePickerSiteDiscoveryFragment
import com.woocommerce.android.util.ChromeCustomTabUtils
import com.woocommerce.android.util.FeatureFlag
import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.Exit
import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.Logout
import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.ShowDialog
Expand Down Expand Up @@ -92,6 +93,11 @@ class SitePickerFragment : BaseFragment(R.layout.fragment_site_picker), LoginEma
binding.loginEpilogueButtonBar.buttonSecondary.setOnClickListener {
viewModel.onTryAnotherAccountButtonClick()
}

binding.loginEpilogueButtonBar.buttonTertiary.isVisible = FeatureFlag.STORE_CREATION_WEBVIEW_FLOW.isEnabled()
binding.loginEpilogueButtonBar.buttonTertiary.setOnClickListener {
viewModel.onCreateSiteButtonClick()
}
}

@Suppress("LongMethod", "ComplexMethod")
Expand Down Expand Up @@ -176,6 +182,13 @@ class SitePickerFragment : BaseFragment(R.layout.fragment_site_picker), LoginEma
AnalyticsTracker.track(AnalyticsEvent.LOGIN_WOOCOMMERCE_SETUP_COMPLETED)
viewModel.onWooInstalled()
}
handleResult<List<String>>(WPComWebViewFragment.WEBVIEW_RESULT_WITH_URL) { urls ->
AnalyticsTracker.track(
AnalyticsEvent.LOGIN_WOOCOMMERCE_SITE_CREATED,
mapOf(AnalyticsTracker.KEY_URL to urls.joinToString())
)
viewModel.onSiteCreated(urls)
}
handleNotice(WPComWebViewFragment.WEBVIEW_DISMISSED) {
AnalyticsTracker.track(AnalyticsEvent.LOGIN_WOOCOMMERCE_SETUP_DISMISSED)
}
Expand Down Expand Up @@ -275,7 +288,8 @@ class SitePickerFragment : BaseFragment(R.layout.fragment_site_picker), LoginEma
findNavController().navigate(
NavGraphMainDirections.actionGlobalWPComWebViewFragment(
urlToLoad = event.url,
urlToTriggerExit = event.validationUrl
urlToTriggerExit = event.validationUrl,
title = event.title
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.woocommerce.android.ui.sitepicker.SitePickerViewModel.SitesListItem.N
import com.woocommerce.android.ui.sitepicker.SitePickerViewModel.SitesListItem.WooSiteUiModel
import com.woocommerce.android.util.FeatureFlag
import com.woocommerce.android.util.WooLog
import com.woocommerce.android.util.WooLog.T
import com.woocommerce.android.viewmodel.LiveDataDelegate
import com.woocommerce.android.viewmodel.MultiLiveEvent
import com.woocommerce.android.viewmodel.MultiLiveEvent.Event.Exit
Expand All @@ -38,8 +39,10 @@ import com.woocommerce.android.viewmodel.ResourceProvider
import com.woocommerce.android.viewmodel.ScopedViewModel
import com.woocommerce.android.viewmodel.navArgs
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooErrorType
Expand Down Expand Up @@ -67,6 +70,9 @@ class SitePickerViewModel @Inject constructor(
companion object {
private const val WOOCOMMERCE_INSTALLATION_URL = "https://wordpress.com/plugins/woocommerce/"
private const val WOOCOMMERCE_INSTALLATION_DONE_URL = "marketplace/thank-you/woocommerce"
private const val WOOCOMMERCE_STORE_CREATION_URL = "https://woocommerce.com/start"
private const val WOOCOMMERCE_STORE_CREATION_DONE_URL = "calypso/images/wpcom-ecommerce"
private const val WOOCOMMERCE_STORE_CREATION_RETRY_INTERVAL = 2000L
}

private val navArgs: SitePickerFragmentArgs by savedState.navArgs()
Expand All @@ -78,7 +84,7 @@ class SitePickerViewModel @Inject constructor(
val sites: LiveData<List<SitesListItem>> = _sites

private var loginSiteAddress: String?
get() = savedState.get("key") ?: appPrefsWrapper.getLoginSiteAddress()
get() = savedState["key"] ?: appPrefsWrapper.getLoginSiteAddress()
set(value) = savedState.set("key", value)

val shouldShowToolbar: Boolean
Expand Down Expand Up @@ -112,11 +118,12 @@ class SitePickerViewModel @Inject constructor(
}
}

private suspend fun fetchSitesFromApi(showSkeleton: Boolean) {
private suspend fun fetchSitesFromApi(showSkeleton: Boolean, delayTime: Long = 0) {
sitePickerViewState = sitePickerViewState.copy(
isSkeletonViewVisible = showSkeleton
)

delay(delayTime)
val startTime = System.currentTimeMillis()
val result = repository.fetchWooCommerceSites()
val duration = System.currentTimeMillis() - startTime
Expand Down Expand Up @@ -406,6 +413,17 @@ class SitePickerViewModel @Inject constructor(
triggerEvent(SitePickerEvent.NavigateToSiteAddressEvent)
}

fun onCreateSiteButtonClick() {
analyticsTrackerWrapper.track(AnalyticsEvent.SITE_PICKER_CREATE_SITE_TAPPED)
triggerEvent(
NavigateToWPComWebView(
url = WOOCOMMERCE_STORE_CREATION_URL,
validationUrl = WOOCOMMERCE_STORE_CREATION_DONE_URL,
title = resourceProvider.getString(string.create_new_store)
)
)
}

fun onTryAnotherAccountButtonClick() {
trackLoginEvent(clickEvent = UnifiedLoginTracker.Click.TRY_ANOTHER_ACCOUNT)
launch {
Expand Down Expand Up @@ -530,6 +548,42 @@ class SitePickerViewModel @Inject constructor(
}
}

fun onSiteCreated(urls: List<String>) {
launch {
sitePickerViewState = sitePickerViewState.copy(
isSkeletonViewVisible = false, isProgressDiaLogVisible = true
)

var newSite: SiteModel? = null
while (newSite?.hasWooCommerce != true) {
newSite = urls.firstNotNullOfOrNull { url -> repository.getSiteBySiteUrl(url) }
if (newSite != null && newSite.hasWooCommerce) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a new site is first fetched, the Woo plugin is still not ready and the store can't be selected. So we need to wait for the hasWooCommerce to be true.

onSiteSelected(newSite)
onContinueButtonClick(true)
WooLog.d(T.LOGIN, "Found new site: ${newSite.url}")
} else {
WooLog.d(T.LOGIN, "New site not found, retrying...")
val result = fetchSitesAfterCreation()
if (result.isFailure) {
triggerEvent(ShowSnackbar(string.site_picker_error))
break
} else {
displaySites(result.getOrDefault(emptyList()))
}
}
}
}
}

private suspend fun fetchSitesAfterCreation(): Result<List<SiteModel>> {
val result = withContext(Dispatchers.Default) {
delay(WOOCOMMERCE_STORE_CREATION_RETRY_INTERVAL)
repository.fetchWooCommerceSites()
}
return if (result.isError) Result.failure(Exception(result.error.message))
else Result.success(result.model ?: emptyList())
}

fun onWooInstalled() {
suspend fun fetchSite(site: SiteModel, retries: Int = 0): Result<SiteModel> {
delay(retries * TimeUnit.SECONDS.toMillis(2))
Expand Down Expand Up @@ -660,7 +714,11 @@ class SitePickerViewModel @Inject constructor(
object NavigateToNewToWooEvent : SitePickerEvent()
object NavigateToSiteAddressEvent : SitePickerEvent()
data class NavigateToHelpFragmentEvent(val origin: HelpActivity.Origin) : SitePickerEvent()
data class NavigateToWPComWebView(val url: String, val validationUrl: String) : SitePickerEvent()
data class NavigateToWPComWebView(
val url: String,
val validationUrl: String,
val title: String? = null
) : SitePickerEvent()
data class NavigateToAccountMismatchScreen(
val primaryButton: AccountMismatchPrimaryButton,
val siteUrl: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ enum class FeatureFlag {
WC_SHIPPING_BANNER,
UNIFIED_ORDER_EDITING,
ORDER_CREATION_CUSTOMER_SEARCH,
STORE_CREATION_WEBVIEW_FLOW,
STORE_CREATION_FLOW;

fun isEnabled(context: Context? = null): Boolean {
Expand All @@ -29,6 +30,7 @@ enum class FeatureFlag {
MORE_MENU_INBOX,
WC_SHIPPING_BANNER,
STORE_CREATION_FLOW -> PackageUtils.isDebugBuild()
STORE_CREATION_WEBVIEW_FLOW -> PackageUtils.isDebugBuild()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

np, I think these two feature flags might be confusing, especially with how we are going to interact with them for the simplified login project.
I think we should updateSTORE_CREATION to be called ACCOUNT_CREATION, as this is what's really controls right now.

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,16 @@
android:text="@string/login_view_connected_stores"
android:textAllCaps="false"
tools:visibility="visible"/>

<com.google.android.material.button.MaterialButton
android:id="@+id/button_tertiary"
style="@style/Woo.Button.Outlined"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="@dimen/minor_100"
android:layout_marginEnd="@dimen/minor_00"
android:text="@string/create_new_store"
android:textAllCaps="false"
tools:visibility="visible"/>
</LinearLayout>
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,13 @@
android:text="@string/login_view_connected_stores"
android:textAllCaps="false"
tools:visibility="visible"/>

<com.google.android.material.button.MaterialButton
android:id="@+id/button_tertiary"
style="@style/Woo.Button.Outlined"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/create_new_store"
android:textAllCaps="false"
tools:visibility="visible"/>
</LinearLayout>
3 changes: 3 additions & 0 deletions WooCommerce/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2647,4 +2647,7 @@
<string name="stats_widget_last_updated_message">As of %1$s</string>
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>

<!-- Store creation -->
<string name="create_new_store">Create a new store</string>
</resources>