Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
9789e81
Refactor handling of Application Passwords configuration
hichamboushaba Aug 30, 2025
6161d74
Rename property
hichamboushaba Aug 30, 2025
cb1cf6e
Add new property to control whether feature is enabled or not for WPC…
hichamboushaba Aug 30, 2025
225395d
Copy `WooExperimentalNetwork` logic to `WooNetwork`
hichamboushaba Aug 30, 2025
856bbe1
Remove WooExperimentalNetwork
hichamboushaba Aug 30, 2025
3444de8
Force using Jetpack tunnel when feature is disabled
hichamboushaba Aug 30, 2025
aeb3d6b
Remove unused argument
hichamboushaba Aug 30, 2025
7df9071
Remove unused remote config key
hichamboushaba Aug 30, 2025
65d2f19
Remove old code related to the initial experiment
hichamboushaba Aug 30, 2025
edc6d45
Improve log message for failed requests
hichamboushaba Aug 30, 2025
90d18fb
Add a feature flag for the feature
hichamboushaba Aug 30, 2025
a442412
Minor refactoring for the configuration class
hichamboushaba Sep 1, 2025
60312f7
Make `isEnabledForDirectAccess` enabled by default
hichamboushaba Sep 1, 2025
043d3dc
Update unit tests
hichamboushaba Sep 1, 2025
de41eb5
Fix detekt issues
hichamboushaba Sep 1, 2025
c7f9c85
Fix wear app build issue
hichamboushaba Sep 1, 2025
8ea3d7f
Fix issue for Jetpack CP sites
hichamboushaba Sep 2, 2025
bd77057
Add preference key for the experimental toggle
hichamboushaba Sep 3, 2025
44044c9
Add a preference toggle to the experimental features screen
hichamboushaba Sep 3, 2025
2ef0b44
Rename settings entry to "Experimental features"
hichamboushaba Sep 3, 2025
1700937
Merge pull request #14559 from woocommerce/issue/WOOMOB-1183-jp-app-p…
irfano Sep 5, 2025
8ee5125
Remove outdated comment section
hichamboushaba Sep 5, 2025
3e845a0
Merge branch 'trunk' into issue/WOOMOB-1126-app-passwords-network
hichamboushaba Sep 5, 2025
586a357
Remove old way for checking for app passwords support
hichamboushaba Sep 5, 2025
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
@@ -0,0 +1,24 @@
package com.woocommerce.android.wear.di

import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords.ApplicationPasswordsConfiguration
import javax.inject.Inject

@Module
@InstallIn(SingletonComponent::class)
interface ApplicationPasswordsModule {
@Binds
fun bindApplicationPasswordsConfiguration(
configuration: WooWearApplicationPasswordsConfiguration
): ApplicationPasswordsConfiguration
}

class WooWearApplicationPasswordsConfiguration @Inject constructor() : ApplicationPasswordsConfiguration {
override val applicationName: String = ""

override fun isEnabledForDirectAccess(): Boolean = false
override suspend fun isEnabledForJetpackAccess(): Boolean = false
Comment on lines +22 to +23
Copy link
Member Author

@hichamboushaba hichamboushaba Sep 1, 2025

Choose a reason for hiding this comment

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

The wear app didn't support Application Passwords before #12124, and we are keeping the same behavior.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.woocommerce.android.applicationpasswords

import com.woocommerce.android.BuildConfig
import com.woocommerce.android.util.DeviceInfo
import com.woocommerce.android.util.FeatureFlag.APP_PASSWORDS_FOR_JETPACK_SITES
import jakarta.inject.Inject
import org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords.ApplicationPasswordsConfiguration

class WooApplicationPasswordsConfiguration @Inject constructor() : ApplicationPasswordsConfiguration {
override val applicationName: String =
"${BuildConfig.APPLICATION_ID}.app-client.${DeviceInfo.name.replace(' ', '-')}"

override suspend fun isEnabledForJetpackAccess(): Boolean = APP_PASSWORDS_FOR_JETPACK_SITES.isEnabled()
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ class FirebaseRemoteConfigRepository @Inject constructor(
companion object {
private const val DEBUG_INTERVAL = 10L
private const val RELEASE_INTERVAL = 31200L

const val KEY_ENABLE_JETPACK_APP_PASSWORDS_EXPERIMENT = "wcandroid_enable_jetpack_app_passwords_experiment_2"
}

private val minimumFetchIntervalInSeconds =
Expand All @@ -37,11 +35,7 @@ class FirebaseRemoteConfigRepository @Inject constructor(
private val _fetchStatus = MutableStateFlow(RemoteConfigFetchStatus.Pending)
override val fetchStatus: Flow<RemoteConfigFetchStatus> = _fetchStatus.asStateFlow()

private val defaultValues by lazy {
mapOf(
KEY_ENABLE_JETPACK_APP_PASSWORDS_EXPERIMENT to false.toString()
)
}
private val defaultValues = emptyMap<String, String>()

init {
remoteConfig.apply {
Expand Down Expand Up @@ -73,8 +67,4 @@ class FirebaseRemoteConfigRepository @Inject constructor(
@VisibleForTesting
fun observeStringRemoteValue(key: String) = changesTrigger
.map { remoteConfig.getString(key) }

override fun isJetpackAppPasswordsExperimentEnabled(): Boolean {
return remoteConfig.getBoolean(KEY_ENABLE_JETPACK_APP_PASSWORDS_EXPERIMENT)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import kotlinx.coroutines.flow.Flow
interface RemoteConfigRepository {
val fetchStatus: Flow<RemoteConfigFetchStatus>
fun fetchRemoteConfig()

fun isJetpackAppPasswordsExperimentEnabled(): Boolean
}

enum class RemoteConfigFetchStatus {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package com.woocommerce.android.di

import com.woocommerce.android.BuildConfig
import com.woocommerce.android.applicationpasswords.ApplicationPasswordsNotifier
import com.woocommerce.android.util.DeviceInfo
import com.woocommerce.android.applicationpasswords.WooApplicationPasswordsConfiguration
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.wordpress.android.fluxc.module.ApplicationPasswordsClientId
import org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords.ApplicationPasswordsConfiguration
import org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords.ApplicationPasswordsListener

@Module
Expand All @@ -19,10 +17,8 @@ interface ApplicationPasswordsModule {
notifier: ApplicationPasswordsNotifier
): ApplicationPasswordsListener

companion object {
@Provides
@ApplicationPasswordsClientId
fun providesApplicationPasswordClientId() =
"${BuildConfig.APPLICATION_ID}.app-client.${DeviceInfo.name.replace(' ', '-')}"
}
@Binds
fun bindApplicationPasswordsConfiguration(
configuration: WooApplicationPasswordsConfiguration
): ApplicationPasswordsConfiguration
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.module.ApplicationPasswordsClientId
import org.wordpress.android.fluxc.network.rest.wpapi.Nonce.CookieNonceErrorType.INVALID_CREDENTIALS
import org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords.ApplicationPasswordsConfiguration
import org.wordpress.android.fluxc.store.SiteStore.SiteError
import org.wordpress.android.login.LoginAnalyticsListener
import org.wordpress.android.util.UrlUtils
Expand All @@ -55,7 +55,7 @@ class LoginSiteCredentialsViewModel @Inject constructor(
private val analyticsTracker: AnalyticsTrackerWrapper,
private val appPrefs: AppPrefsWrapper,
private val resourceProvider: ResourceProvider,
@ApplicationPasswordsClientId private val applicationPasswordsClientId: String
private val applicationPasswordsConfiguration: ApplicationPasswordsConfiguration
) : ScopedViewModel(savedStateHandle) {
companion object {
const val SITE_ADDRESS_KEY = "site-address"
Expand All @@ -82,7 +82,9 @@ class LoginSiteCredentialsViewModel @Inject constructor(

private val SiteModel?.fullAuthorizationUrl: String?
get() = this?.applicationPasswordsAuthorizeUrl
?.let { url -> "$url?app_name=$applicationPasswordsClientId&success_url=$REDIRECTION_URL" }
?.let { url ->
"$url?app_name=${applicationPasswordsConfiguration.applicationName}&success_url=$REDIRECTION_URL"
}

val viewState = combine(
flowOf(siteAddress.removeSchemeAndSuffix()),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.woocommerce.android.ui.orders.list

import com.woocommerce.android.R
import com.woocommerce.android.config.RemoteConfigRepository
import com.woocommerce.android.extensions.getBillingName
import com.woocommerce.android.model.TimeGroup
import com.woocommerce.android.model.TimeGroup.GROUP_FUTURE
Expand Down Expand Up @@ -45,8 +44,7 @@ class OrderListItemDataSource @Inject constructor(
private val networkStatus: NetworkStatus,
private val fetcher: FetchOrdersRepository,
private val resourceProvider: ResourceProvider,
private val dateUtils: DateUtils,
private val remoteConfigRepository: RemoteConfigRepository
private val dateUtils: DateUtils
) : ListItemDataSourceInterface<WCOrderListDescriptor, OrderListItemIdentifier, OrderListItemUIType> {
override fun getItemsAndFetchIfNecessary(
listDescriptor: WCOrderListDescriptor,
Expand Down Expand Up @@ -195,8 +193,7 @@ class OrderListItemDataSource @Inject constructor(
override fun fetchList(listDescriptor: WCOrderListDescriptor, offset: Long) {
val fetchOrderListPayload = FetchOrderListPayload(
listDescriptor = listDescriptor,
offset = offset,
useAppPasswordsForJetpackSites = remoteConfigRepository.isJetpackAppPasswordsExperimentEnabled()
offset = offset
)
dispatcher.dispatch(WCOrderActionBuilder.newFetchOrderListAction(fetchOrderListPayload))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ enum class FeatureFlag {
WOO_POS_HISTORICAL_ORDERS_M1,
WOO_POS_LOCAL_CATALOG_M1,
HIDE_SITES_FROM_SITE_PICKER,
AI_PRODUCT_IMAGE_BACKGROUND_REMOVAL;
AI_PRODUCT_IMAGE_BACKGROUND_REMOVAL,
APP_PASSWORDS_FOR_JETPACK_SITES;

fun isEnabled(context: Context? = null): Boolean {
return when (this) {
Expand All @@ -29,7 +30,8 @@ enum class FeatureFlag {
BETTER_CUSTOMER_SEARCH_M2,
ORDER_CREATION_AUTO_TAX_RATE,
AI_PRODUCT_IMAGE_BACKGROUND_REMOVAL,
WOO_POS_LOCAL_CATALOG_M1 -> PackageUtils.isDebugBuild()
WOO_POS_LOCAL_CATALOG_M1,
APP_PASSWORDS_FOR_JETPACK_SITES -> PackageUtils.isDebugBuild()

NEW_SHIPPING_SUPPORT,
BULK_UPDATE_ORDERS_STATUS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import org.wordpress.android.fluxc.network.BaseRequest.BaseNetworkError
import org.wordpress.android.fluxc.network.BaseRequest.GenericErrorType
import org.wordpress.android.fluxc.network.rest.wpapi.Nonce
import org.wordpress.android.fluxc.network.rest.wpapi.WPAPINetworkError
import org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords.ApplicationPasswordsConfiguration
import org.wordpress.android.login.LoginAnalyticsListener

@ExperimentalCoroutinesApi
Expand Down Expand Up @@ -95,7 +96,10 @@ class LoginSiteCredentialsViewModelTest : BaseUnitTest() {
applicationPasswordsNotifier = applicationPasswordsNotifier,
analyticsTracker = analyticsTracker,
appPrefs = appPrefs,
applicationPasswordsClientId = clientId,
applicationPasswordsConfiguration = object : ApplicationPasswordsConfiguration {
override val applicationName: String = clientId
override suspend fun isEnabledForJetpackAccess(): Boolean = true
},
resourceProvider = resourceProvider
)
}
Expand Down Expand Up @@ -292,22 +296,23 @@ class LoginSiteCredentialsViewModelTest : BaseUnitTest() {
}

@Test
fun `given application pwd disabled and wp-login-php accessible, when submitting login, then show error screen`() = testBlocking {
setup {
whenever(wpApiSiteRepository.checkIfUserIsEligible(testSite)).thenReturn(Result.failure(Exception()))
}

viewModel.viewState.observeForTesting {
viewModel.onUsernameChanged(testUsername)
viewModel.onPasswordChanged(testPassword)
viewModel.onContinueClick()
applicationPasswordsUnavailableEvents.tryEmit(mock())
fun `given application pwd disabled and wp-login-php accessible, when submitting login, then show error screen`() =
testBlocking {
setup {
whenever(wpApiSiteRepository.checkIfUserIsEligible(testSite)).thenReturn(Result.failure(Exception()))
}

viewModel.viewState.observeForTesting {
viewModel.onUsernameChanged(testUsername)
viewModel.onPasswordChanged(testPassword)
viewModel.onContinueClick()
applicationPasswordsUnavailableEvents.tryEmit(mock())
}

assertThat(viewModel.event.value)
.isEqualTo(ShowApplicationPasswordsUnavailableScreen(siteAddress, isJetpackConnected))
}

assertThat(viewModel.event.value)
.isEqualTo(ShowApplicationPasswordsUnavailableScreen(siteAddress, isJetpackConnected))
}

@Test
fun `give user role fetch fails, when submitting login, then show a snackbar`() = testBlocking {
setup {
Expand Down Expand Up @@ -353,23 +358,24 @@ class LoginSiteCredentialsViewModelTest : BaseUnitTest() {
}

@Test
fun `given application passwords enabled and login fails for an unknown reason, when user attempts to sign-in, then show WebView login flow`() = testBlocking {
setup {
whenever(wpApiSiteRepository.login(siteAddress, testUsername, testPassword))
.thenReturn(Result.failure(Exception()))
whenever(wpApiSiteRepository.fetchSite(siteAddress))
.thenReturn(Result.success(testSite.apply { applicationPasswordsAuthorizeUrl = urlAuthBase }))
fun `given application passwords enabled and login fails for an unknown reason, when user attempts to sign-in, then show WebView login flow`() =
testBlocking {
setup {
whenever(wpApiSiteRepository.login(siteAddress, testUsername, testPassword))
.thenReturn(Result.failure(Exception()))
whenever(wpApiSiteRepository.fetchSite(siteAddress))
.thenReturn(Result.success(testSite.apply { applicationPasswordsAuthorizeUrl = urlAuthBase }))
}

val event = viewModel.event.runAndCaptureValues {
viewModel.onUsernameChanged(testUsername)
viewModel.onPasswordChanged(testPassword)
viewModel.viewState.getOrAwaitValue()
viewModel.onContinueClick()
}.last()

assertThat(event).isInstanceOf(LoginSiteCredentialsViewModel.ShowApplicationPasswordTutorialScreen::class.java)
assertThat((event as LoginSiteCredentialsViewModel.ShowApplicationPasswordTutorialScreen).url)
.isEqualTo(urlAuthFull)
}

val event = viewModel.event.runAndCaptureValues {
viewModel.onUsernameChanged(testUsername)
viewModel.onPasswordChanged(testPassword)
viewModel.viewState.getOrAwaitValue()
viewModel.onContinueClick()
}.last()

assertThat(event).isInstanceOf(LoginSiteCredentialsViewModel.ShowApplicationPasswordTutorialScreen::class.java)
assertThat((event as LoginSiteCredentialsViewModel.ShowApplicationPasswordTutorialScreen).url)
.isEqualTo(urlAuthFull)
}
}
Loading
Loading