-
Notifications
You must be signed in to change notification settings - Fork 136
Store creation using a webview #7602
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 13 commits
430f4e5
fdcabaf
a558a5b
48cb8ea
6040d21
43eb48c
2f4a98b
1b61062
fd11b02
6f9f490
28d47e2
a1d5f6c
61f6895
eda1ab1
71c6a95
6627d58
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,14 +6,18 @@ import android.view.View | |
| import android.webkit.WebChromeClient | ||
| import android.webkit.WebView | ||
| import androidx.core.view.isVisible | ||
| import androidx.lifecycle.lifecycleScope | ||
| 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 | ||
|
|
@@ -28,11 +32,15 @@ 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" | ||
| } | ||
|
|
||
| private val webViewClient by lazy { WPComWebViewClient(this) } | ||
| private val navArgs: WPComWebViewFragmentArgs by navArgs() | ||
| private var siteUrls = ArrayList<String>() | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
|
|
||
| @Inject lateinit var wpcomWebViewAuthenticator: WPComWebViewAuthenticator | ||
|
|
||
|
|
@@ -52,20 +60,48 @@ 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) | ||
|
|
||
| lifecycleScope.launchWhenResumed { | ||
|
||
| if (isAdded && navArgs.urlToTriggerExit?.matchesUrl(url) == true) { | ||
| 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) | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
|
@@ -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() | ||
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
|
@@ -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 { | ||
|
|
@@ -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) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| 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)) | ||
|
|
@@ -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, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 { | ||
|
|
@@ -29,6 +30,7 @@ enum class FeatureFlag { | |
| MORE_MENU_INBOX, | ||
| WC_SHIPPING_BANNER, | ||
| STORE_CREATION_FLOW -> PackageUtils.isDebugBuild() | ||
| STORE_CREATION_WEBVIEW_FLOW -> PackageUtils.isDebugBuild() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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:onStoreCreatedcan be very simplified, basically the same implementation asonSiteAddressReceived.There was a problem hiding this comment.
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
WCWebViewin 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?
There was a problem hiding this comment.
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.