Skip to content

Commit 86cb425

Browse files
committed
Auto-mint application password before showing the My Site card
Before the "Authenticate using Application Password" card is shown on My Site, attempt headless creation via the new `SiteStore.createApplicationPassword(site)` (which routes through the existing Jetpack tunnel and persists credentials onto the SiteModel so `siteHasBadCredentials` flips). On Created/Existing we hide the card; on NotSupported/Failure we fall back to today's discovery + Custom Tab prompt. This makes the Atomic guard relaxation actually user-visible — Atomic users no longer see the card or have to authorize through a browser.
1 parent 147fc9c commit 86cb425

3 files changed

Lines changed: 127 additions & 0 deletions

File tree

WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/applicationpassword/ApplicationPasswordViewModelSlice.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,21 @@ class ApplicationPasswordViewModelSlice @Inject constructor(
184184
return@launch
185185
}
186186

187+
// Headless mint short-circuits the Custom Tab flow for sites the WP.com tunnel can
188+
// mint against (Atomic, Jetpack-WPCom-REST). Other sites return NotSupported and fall
189+
// through to discovery + card below.
190+
val siteForMint = storedSite ?: site
191+
val createResult = siteStore.createApplicationPassword(siteForMint)
192+
if (!createResult.isError && createResult.credentials != null) {
193+
uiModelMutable.postValue(null)
194+
appLogWrapper.d(AppLog.T.MAIN, "A_P: Hiding card for ${site.url} - headless mint succeeded")
195+
return@launch
196+
}
197+
appLogWrapper.d(
198+
AppLog.T.MAIN,
199+
"A_P: Headless mint failed for ${site.url} (notSupported=${createResult.error?.notSupported})"
200+
)
201+
187202
val authorizationUrlComplete = applicationPasswordLoginHelper.getAuthorizationUrlComplete(site.url)
188203
if (authorizationUrlComplete.isEmpty()) {
189204
uiModelMutable.postValue(null)

WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/applicationpassword/ApplicationPasswordViewModelSliceTest.kt

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,14 @@ import org.mockito.kotlin.mock
2020
import org.wordpress.android.fluxc.Dispatcher
2121
import org.wordpress.android.fluxc.model.SiteModel
2222
import org.wordpress.android.fluxc.model.SitesModel
23+
import org.wordpress.android.fluxc.network.BaseRequest.BaseNetworkError
24+
import org.wordpress.android.fluxc.network.BaseRequest.GenericErrorType
2325
import org.wordpress.android.fluxc.network.discovery.SelfHostedEndpointFinder
2426
import org.wordpress.android.fluxc.network.xmlrpc.site.SiteXMLRPCClient
27+
import org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords.ApplicationPasswordCredentials
2528
import org.wordpress.android.fluxc.network.rest.wpapi.rs.WpApiClientProvider
2629
import org.wordpress.android.fluxc.store.SiteStore
30+
import org.wordpress.android.fluxc.store.SiteStore.OnApplicationPasswordCreated
2731
import org.wordpress.android.fluxc.utils.AppLogWrapper
2832
import org.wordpress.android.ui.accounts.login.ApplicationPasswordLoginHelper
2933
import org.wordpress.android.ui.mysite.MySiteCardAndItem
@@ -98,8 +102,28 @@ class ApplicationPasswordViewModelSliceTest : BaseUnitTest() {
98102
}
99103
}
100104

105+
private suspend fun stubMintFailure(notSupported: Boolean = false) {
106+
whenever(siteStore.createApplicationPassword(any())).thenReturn(
107+
OnApplicationPasswordCreated(
108+
siteTest,
109+
BaseNetworkError(GenericErrorType.UNKNOWN, "fail"),
110+
notSupported = notSupported,
111+
)
112+
)
113+
}
114+
115+
private suspend fun stubMintSuccess() {
116+
whenever(siteStore.createApplicationPassword(any())).thenReturn(
117+
OnApplicationPasswordCreated(
118+
siteTest,
119+
ApplicationPasswordCredentials("user", "pass", uuid = "u")
120+
)
121+
)
122+
}
123+
101124
@Test
102125
fun `given proper site, when api discovery is success, then add the application password card`() = runTest {
126+
stubMintFailure()
103127
whenever(applicationPasswordLoginHelper.getAuthorizationUrlComplete(eq(TEST_URL)))
104128
.thenReturn("$TEST_URL_AUTH$TEST_URL_AUTH_SUFFIX")
105129

@@ -111,6 +135,7 @@ class ApplicationPasswordViewModelSliceTest : BaseUnitTest() {
111135

112136
@Test
113137
fun `given login scenario, when api discovery is empty, then show no card`() = runTest {
138+
stubMintFailure()
114139
whenever(applicationPasswordLoginHelper.getAuthorizationUrlComplete(eq(TEST_URL)))
115140
.thenReturn("")
116141

@@ -120,6 +145,30 @@ class ApplicationPasswordViewModelSliceTest : BaseUnitTest() {
120145
verify(applicationPasswordLoginHelper).getAuthorizationUrlComplete(eq(TEST_URL))
121146
}
122147

148+
@Test
149+
fun `given headless mint succeeds, then hide card and skip discovery`() = runTest {
150+
stubMintSuccess()
151+
152+
applicationPasswordViewModelSlice.buildCard(siteTest)
153+
154+
assertNull(applicationPasswordCard)
155+
verify(siteStore).createApplicationPassword(any())
156+
verify(applicationPasswordLoginHelper, never()).getAuthorizationUrlComplete(any())
157+
}
158+
159+
@Test
160+
fun `given headless mint returns NotSupported, then fall back to discovery`() = runTest {
161+
stubMintFailure(notSupported = true)
162+
whenever(applicationPasswordLoginHelper.getAuthorizationUrlComplete(eq(TEST_URL)))
163+
.thenReturn("$TEST_URL_AUTH$TEST_URL_AUTH_SUFFIX")
164+
165+
applicationPasswordViewModelSlice.buildCard(siteTest)
166+
167+
assertNotNull(applicationPasswordCard)
168+
verify(siteStore).createApplicationPassword(any())
169+
verify(applicationPasswordLoginHelper).getAuthorizationUrlComplete(eq(TEST_URL))
170+
}
171+
123172
@Test
124173
fun `given site already authenticated, when calling api discovery, then show no card`() = runTest {
125174
whenever(siteStore.sites)

libs/fluxc/src/main/java/org/wordpress/android/fluxc/store/SiteStore.kt

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ import org.wordpress.android.fluxc.model.jetpacksocial.JetpackSocialMapper
8585
import org.wordpress.android.fluxc.network.BaseRequest.BaseNetworkError
8686
import org.wordpress.android.fluxc.network.rest.wpapi.WPAPINetworkError
8787
import org.wordpress.android.fluxc.network.rest.wpapi.WPAPIResponse
88+
import org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords.ApplicationPasswordCreationResult
89+
import org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords.ApplicationPasswordCredentials
8890
import org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords.ApplicationPasswordDeletionResult
8991
import org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords.ApplicationPasswordsManager
9092
import org.wordpress.android.fluxc.network.rest.wpapi.site.SiteWPAPIRestClient
@@ -798,6 +800,15 @@ open class SiteStore @Inject constructor(
798800
}
799801
}
800802

803+
data class OnApplicationPasswordCreated(
804+
val site: SiteModel,
805+
val credentials: ApplicationPasswordCredentials? = null,
806+
) : OnChanged<OnApplicationPasswordCreateError>() {
807+
constructor(site: SiteModel, error: BaseNetworkError, notSupported: Boolean): this(site) {
808+
this.error = OnApplicationPasswordCreateError(error, notSupported)
809+
}
810+
}
811+
801812
class OnSiteLaunched() : OnChanged<LaunchSiteError>() {
802813
constructor(error: LaunchSiteError) : this() {
803814
this.error = error
@@ -829,6 +840,23 @@ open class SiteStore @Inject constructor(
829840
}
830841
}
831842

843+
class OnApplicationPasswordCreateError(
844+
error: BaseNetworkError,
845+
val notSupported: Boolean,
846+
) : OnChangedError {
847+
var errorCode: String? = null
848+
var message: String
849+
850+
init {
851+
if (error is WPAPINetworkError) {
852+
errorCode = error.errorCode
853+
} else if (error is WPComGsonNetworkError) {
854+
errorCode = error.apiError
855+
}
856+
message = error.message
857+
}
858+
}
859+
832860
class PlansError
833861
@JvmOverloads constructor(
834862
@JvmField val type: PlansErrorType,
@@ -2372,6 +2400,41 @@ open class SiteStore @Inject constructor(
23722400
}
23732401
}
23742402

2403+
suspend fun createApplicationPassword(site: SiteModel): OnApplicationPasswordCreated =
2404+
coroutineEngine.withDefaultContext(T.API, this, "Create Application Password") {
2405+
when (val result = applicationPasswordsManagerProvider.get().getApplicationCredentials(site)) {
2406+
is ApplicationPasswordCreationResult.Created -> {
2407+
persistApplicationPasswordCredentials(site, result.credentials)
2408+
OnApplicationPasswordCreated(site, result.credentials)
2409+
}
2410+
is ApplicationPasswordCreationResult.Existing -> {
2411+
if (site.apiRestUsernamePlain.isNullOrEmpty() || site.apiRestPasswordPlain.isNullOrEmpty()) {
2412+
persistApplicationPasswordCredentials(site, result.credentials)
2413+
}
2414+
OnApplicationPasswordCreated(site, result.credentials)
2415+
}
2416+
is ApplicationPasswordCreationResult.NotSupported ->
2417+
OnApplicationPasswordCreated(site, result.originalError, notSupported = true)
2418+
is ApplicationPasswordCreationResult.Failure ->
2419+
OnApplicationPasswordCreated(site, result.error, notSupported = false)
2420+
}
2421+
}
2422+
2423+
private fun persistApplicationPasswordCredentials(
2424+
site: SiteModel,
2425+
credentials: ApplicationPasswordCredentials,
2426+
) {
2427+
site.apply {
2428+
apiRestUsernamePlain = credentials.userName
2429+
apiRestPasswordPlain = credentials.password
2430+
apiRestUsernameEncrypted = ""
2431+
apiRestPasswordEncrypted = ""
2432+
apiRestUsernameIV = ""
2433+
apiRestPasswordIV = ""
2434+
}
2435+
emitChange(updateApplicationPassword(site))
2436+
}
2437+
23752438
suspend fun fetchSitePlans(siteModel: SiteModel): FetchedPlansPayload {
23762439
return if (siteModel.isUsingWpComRestApi) {
23772440
coroutineEngine.withDefaultContext(T.API, this, "Fetch site plans") {

0 commit comments

Comments
 (0)