Skip to content

Commit daf4324

Browse files
committed
impl: introduce a factory of steps
in order to abstract even further some of the steps necessary for creating a step and navigating to it.
1 parent aae32ea commit daf4324

5 files changed

Lines changed: 129 additions & 67 deletions

File tree

src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt

Lines changed: 25 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import com.coder.toolbox.browser.browse
44
import com.coder.toolbox.cli.CoderCLIManager
55
import com.coder.toolbox.feed.IdeFeedManager
66
import com.coder.toolbox.oauth.OAuth2Client
7-
import com.coder.toolbox.oauth.OAuthTokenResponse
87
import com.coder.toolbox.plugin.PluginManager
98
import com.coder.toolbox.sdk.CoderRestClient
109
import com.coder.toolbox.sdk.ex.APIResponseException
@@ -22,14 +21,15 @@ import com.coder.toolbox.util.url
2221
import com.coder.toolbox.util.validateStrictWebUrl
2322
import com.coder.toolbox.util.withPath
2423
import com.coder.toolbox.views.Action
25-
import com.coder.toolbox.views.CoderCliSetupWizardPage
2624
import com.coder.toolbox.views.CoderDelimiter
2725
import com.coder.toolbox.views.CoderSettingsPage
26+
import com.coder.toolbox.views.CoderSetupWizardPage
2827
import com.coder.toolbox.views.NewEnvironmentPage
2928
import com.coder.toolbox.views.SuspendBiConsumer
3029
import com.coder.toolbox.views.state.CoderOAuthSessionContext
30+
import com.coder.toolbox.views.state.Credentials
3131
import com.coder.toolbox.views.state.PageRouter
32-
import com.coder.toolbox.views.state.WizardStep
32+
import com.coder.toolbox.views.state.toSessionContext
3333
import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon
3434
import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon.IconType
3535
import com.jetbrains.toolbox.api.core.util.LoadableState
@@ -374,20 +374,18 @@ class CoderRemoteProvider(
374374
// Different URL - we need a new connection. Tear down any
375375
// in-flight wizard, install a fresh one on the router, and let
376376
// showPluginEnvironmentsPage() pull it through getOverrideUiPage.
377-
router.activeWizard?.dispose()
378-
val wizard = CoderCliSetupWizardPage(
377+
val credentials = newToken?.let { Credentials.Token(it) } ?: Credentials.MTls
378+
val wizard = CoderSetupWizardPage.connectStep(
379379
context, settingsPage, visibilityState,
380-
initialAutoSetup = true,
380+
url = newUrl,
381+
credentials = credentials,
381382
jumpToMainPageOnError = true,
382383
onConnect = onConnect.andThen(deferredLinkHandler(params, newUrl))
383384
.andThen { _, _ ->
384385
coderHeaderPage.isBusy.update { false }
385386
},
386-
onTokenRefreshed = ::onTokenRefreshed
387+
onTokenRefreshed = ::onTokenRefreshed,
387388
)
388-
wizard.model.url = newUrl
389-
wizard.model.token = newToken
390-
wizard.model.goTo(WizardStep.CONNECT)
391389
router.replaceWith(wizard)
392390
context.envPageManager.showPluginEnvironmentsPage()
393391
}
@@ -454,14 +452,13 @@ class CoderRemoteProvider(
454452
private suspend fun exchangeOAuthCodeForToken(
455453
code: String,
456454
oauthSessionContext: CoderOAuthSessionContext,
457-
wizard: CoderCliSetupWizardPage,
455+
wizard: CoderSetupWizardPage,
458456
) {
459457
try {
460458
context.logger.info("Handling OAuth callback...")
461459

462460
val tokenResponse = OAuth2Client(context).exchangeCode(oauthSessionContext, code)
463-
wizard.model.oauthSession = oauthSessionContext.copy(tokenResponse = tokenResponse)
464-
wizard.model.goTo(WizardStep.CONNECT)
461+
wizard.advanceToConnectWithOAuth(oauthSessionContext.copy(tokenResponse = tokenResponse))
465462
} catch (e: Exception) {
466463
context.logAndShowError("OAuth Error", "Exception during token exchange: ${e.message}", e)
467464
}
@@ -529,47 +526,31 @@ class CoderRemoteProvider(
529526
*/
530527
override fun getOverrideUiPage(): UiPage? {
531528
if (client != null) return null
532-
return router.requireWizard { buildSetupWizard() }
529+
return router.getOrCreate { buildSetupWizard() }
533530
}
534531

535532
/**
536533
* Build the wizard for the current state. Called once per provider lifetime
537534
* (until [close] clears the router); subsequent visibility cycles reuse the
538535
* same instance, preserving any in-flight connect job.
539536
*/
540-
private fun buildSetupWizard(): CoderCliSetupWizardPage {
537+
private fun buildSetupWizard(): CoderSetupWizardPage {
541538
// When coming back to the application, initializeSession immediately.
542539
if (shouldDoAutoSetup()) {
543540
try {
544-
val storedOAuthSession = context.secrets.oauthSessionFor(context.deploymentUrl.toString())
545-
val wizard = CoderCliSetupWizardPage(
541+
val url = context.deploymentUrl
542+
val credentials = context.secrets.oauthSessionFor(url.toString())?.let {
543+
Credentials.OAuth(it.toSessionContext())
544+
} ?: context.secrets.apiTokenFor(url)?.let {
545+
Credentials.Token(it)
546+
} ?: Credentials.MTls
547+
return CoderSetupWizardPage.connectStep(
546548
context, settingsPage, visibilityState,
547-
initialAutoSetup = true,
548-
jumpToMainPageOnError = false,
549+
url = url,
550+
credentials = credentials,
549551
onConnect = onConnect,
550-
onTokenRefreshed = ::onTokenRefreshed
552+
onTokenRefreshed = ::onTokenRefreshed,
551553
)
552-
wizard.model.url = context.deploymentUrl
553-
wizard.model.token = context.secrets.apiTokenFor(context.deploymentUrl)
554-
if (storedOAuthSession != null) {
555-
wizard.model.oauthSession = CoderOAuthSessionContext(
556-
clientId = storedOAuthSession.clientId,
557-
clientSecret = storedOAuthSession.clientSecret,
558-
tokenCodeVerifier = "",
559-
state = "",
560-
tokenEndpoint = storedOAuthSession.tokenEndpoint,
561-
tokenAuthMethod = storedOAuthSession.tokenAuthMethod,
562-
tokenResponse = OAuthTokenResponse(
563-
accessToken = "",
564-
tokenType = "",
565-
expiresIn = null,
566-
refreshToken = storedOAuthSession.refreshToken,
567-
scope = null
568-
)
569-
)
570-
}
571-
wizard.model.goTo(WizardStep.CONNECT)
572-
return wizard
573554
} catch (ex: Exception) {
574555
errorBuffer.add(ex)
575556
} finally {
@@ -578,14 +559,11 @@ class CoderRemoteProvider(
578559
}
579560

580561
// Login flow.
581-
val setupWizardPage = CoderCliSetupWizardPage(
582-
context,
583-
settingsPage,
584-
visibilityState,
562+
val setupWizardPage = CoderSetupWizardPage.deploymentUrlStep(
563+
context, settingsPage, visibilityState,
585564
onConnect = onConnect,
586-
onTokenRefreshed = ::onTokenRefreshed
565+
onTokenRefreshed = ::onTokenRefreshed,
587566
)
588-
setupWizardPage.model.goToFirst()
589567
// We might have navigated here due to a polling error.
590568
errorBuffer.forEach {
591569
setupWizardPage.notify("Error encountered", it)

src/main/kotlin/com/coder/toolbox/views/CoderCliSetupWizardPage.kt renamed to src/main/kotlin/com/coder/toolbox/views/CoderSetupWizardPage.kt

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.coder.toolbox.CoderToolboxContext
44
import com.coder.toolbox.cli.CoderCLIManager
55
import com.coder.toolbox.sdk.CoderRestClient
66
import com.coder.toolbox.views.state.CoderOAuthSessionContext
7+
import com.coder.toolbox.views.state.Credentials
78
import com.coder.toolbox.views.state.WizardModel
89
import com.coder.toolbox.views.state.WizardStep
910
import com.jetbrains.toolbox.api.remoteDev.ProviderVisibilityState
@@ -16,7 +17,7 @@ import kotlinx.coroutines.flow.update
1617
import kotlinx.coroutines.launch
1718
import java.net.URL
1819

19-
class CoderCliSetupWizardPage(
20+
class CoderSetupWizardPage private constructor(
2021
private val context: CoderToolboxContext,
2122
private val settingsPage: CoderSettingsPage,
2223
visibilityState: StateFlow<ProviderVisibilityState>,
@@ -25,6 +26,63 @@ class CoderCliSetupWizardPage(
2526
onConnect: SuspendBiConsumer<CoderRestClient, CoderCLIManager>,
2627
onTokenRefreshed: (suspend (url: URL, oauthSessionCtx: CoderOAuthSessionContext) -> Unit)? = null
2728
) : CoderPage(MutableStateFlow(context.i18n.ptrl("Setting up Coder")), false) {
29+
30+
companion object {
31+
fun deploymentUrlStep(
32+
context: CoderToolboxContext,
33+
settingsPage: CoderSettingsPage,
34+
visibilityState: StateFlow<ProviderVisibilityState>,
35+
onConnect: SuspendBiConsumer<CoderRestClient, CoderCLIManager>,
36+
onTokenRefreshed: (suspend (url: URL, oauthSessionCtx: CoderOAuthSessionContext) -> Unit)? = null,
37+
): CoderSetupWizardPage = CoderSetupWizardPage(
38+
context, settingsPage, visibilityState,
39+
onConnect = onConnect,
40+
onTokenRefreshed = onTokenRefreshed,
41+
).apply { model.goToFirst() }
42+
43+
fun tokenStep(
44+
context: CoderToolboxContext,
45+
settingsPage: CoderSettingsPage,
46+
visibilityState: StateFlow<ProviderVisibilityState>,
47+
url: URL,
48+
onConnect: SuspendBiConsumer<CoderRestClient, CoderCLIManager>,
49+
onTokenRefreshed: (suspend (url: URL, oauthSessionCtx: CoderOAuthSessionContext) -> Unit)? = null,
50+
): CoderSetupWizardPage = CoderSetupWizardPage(
51+
context, settingsPage, visibilityState,
52+
onConnect = onConnect,
53+
onTokenRefreshed = onTokenRefreshed,
54+
).apply {
55+
model.url = url
56+
model.goTo(WizardStep.TOKEN_REQUEST)
57+
}
58+
59+
fun connectStep(
60+
context: CoderToolboxContext,
61+
settingsPage: CoderSettingsPage,
62+
visibilityState: StateFlow<ProviderVisibilityState>,
63+
url: URL,
64+
credentials: Credentials,
65+
initialAutoSetup: Boolean = true,
66+
jumpToMainPageOnError: Boolean = false,
67+
onConnect: SuspendBiConsumer<CoderRestClient, CoderCLIManager>,
68+
onTokenRefreshed: (suspend (url: URL, oauthSessionCtx: CoderOAuthSessionContext) -> Unit)? = null,
69+
): CoderSetupWizardPage = CoderSetupWizardPage(
70+
context, settingsPage, visibilityState,
71+
initialAutoSetup = initialAutoSetup,
72+
jumpToMainPageOnError = jumpToMainPageOnError,
73+
onConnect = onConnect,
74+
onTokenRefreshed = onTokenRefreshed,
75+
).apply {
76+
model.url = url
77+
when (credentials) {
78+
is Credentials.MTls -> Unit
79+
is Credentials.Token -> model.token = credentials.value
80+
is Credentials.OAuth -> model.oauthSession = credentials.session
81+
}
82+
model.goTo(WizardStep.CONNECT)
83+
}
84+
}
85+
2886
val model: WizardModel = WizardModel()
2987

3088
private val shouldAutoSetup = MutableStateFlow(initialAutoSetup)
@@ -125,6 +183,11 @@ class CoderCliSetupWizardPage(
125183
}
126184
}
127185

186+
fun advanceToConnectWithOAuth(oauthSession: CoderOAuthSessionContext) {
187+
model.oauthSession = oauthSession
188+
model.goTo(WizardStep.CONNECT)
189+
}
190+
128191
/**
129192
* Cancels any in-flight work owned by this wizard. Called by the page router
130193
* when the wizard is being replaced (e.g. by a deep link to a different

src/main/kotlin/com/coder/toolbox/views/state/OAuthSessionContext.kt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,20 @@ data class StoredOAuthSession(
2121
val tokenEndpoint: String
2222
)
2323

24-
fun CoderOAuthSessionContext?.hasRefreshToken(): Boolean = this?.tokenResponse?.refreshToken != null
24+
fun CoderOAuthSessionContext?.hasRefreshToken(): Boolean = this?.tokenResponse?.refreshToken != null
25+
26+
fun StoredOAuthSession.toSessionContext(): CoderOAuthSessionContext = CoderOAuthSessionContext(
27+
clientId = clientId,
28+
clientSecret = clientSecret,
29+
tokenCodeVerifier = "",
30+
state = "",
31+
tokenEndpoint = tokenEndpoint,
32+
tokenAuthMethod = tokenAuthMethod,
33+
tokenResponse = OAuthTokenResponse(
34+
accessToken = "",
35+
tokenType = "",
36+
expiresIn = null,
37+
refreshToken = refreshToken,
38+
scope = null
39+
)
40+
)
Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,47 @@
11
package com.coder.toolbox.views.state
22

3-
import com.coder.toolbox.views.CoderCliSetupWizardPage
3+
import com.coder.toolbox.views.CoderSetupWizardPage
44
import kotlinx.coroutines.flow.MutableStateFlow
55

66
/**
77
* The page that should currently be rendered in place of the environment list.
88
*/
9-
sealed interface OverrideRoute {
10-
object None : OverrideRoute
11-
data class Wizard(val page: CoderCliSetupWizardPage) : OverrideRoute
9+
sealed interface PageRoute {
10+
object None : PageRoute
11+
data class Wizard(val page: CoderSetupWizardPage) : PageRoute
1212
}
1313

1414
/**
15-
* Holds the active [OverrideRoute]. The same page instance is returned across
15+
* Holds the active [PageRoute]. The same page instance is returned across
1616
* Toolbox visibility cycles so in-flight work (e.g. an ongoing connect) is
1717
* preserved instead of being thrown away every time the window reopens.
1818
*/
1919
class PageRouter {
20-
private val route = MutableStateFlow<OverrideRoute>(OverrideRoute.None)
20+
private val route = MutableStateFlow<PageRoute>(PageRoute.None)
2121

22-
val activeWizard: CoderCliSetupWizardPage?
23-
get() = (route.value as? OverrideRoute.Wizard)?.page
22+
val activeWizard: CoderSetupWizardPage?
23+
get() = (route.value as? PageRoute.Wizard)?.page
2424

2525
/**
26-
* Returns the wizard already on this route, or builds a new one and
26+
* Returns the page already on this route, or builds a new one and
2727
* registers it.
2828
*/
29-
fun requireWizard(build: () -> CoderCliSetupWizardPage): CoderCliSetupWizardPage {
30-
(route.value as? OverrideRoute.Wizard)?.let { return it.page }
31-
val page = build()
32-
route.value = OverrideRoute.Wizard(page)
33-
return page
29+
fun getOrCreate(build: () -> CoderSetupWizardPage): CoderSetupWizardPage {
30+
(route.value as? PageRoute.Wizard)?.let { return it.page }
31+
return build().also { route.value = PageRoute.Wizard(it) }
3432
}
3533

3634
/**
3735
* Replaces any active page with [page]. Used when an external trigger
3836
* (e.g. a deep link to a different deployment) needs to forcibly install
3937
* a new wizard.
4038
*/
41-
fun replaceWith(page: CoderCliSetupWizardPage) {
42-
route.value = OverrideRoute.Wizard(page)
39+
fun replaceWith(page: CoderSetupWizardPage) {
40+
activeWizard?.dispose()
41+
route.value = PageRoute.Wizard(page)
4342
}
4443

4544
fun clear() {
46-
route.value = OverrideRoute.None
45+
route.value = PageRoute.None
4746
}
4847
}

src/main/kotlin/com/coder/toolbox/views/state/WizardModel.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import java.net.URL
77
/**
88
* Per-wizard mutable state: current step, form values, and OAuth session.
99
*
10-
* Owned by a single [com.coder.toolbox.views.CoderCliSetupWizardPage] instance
10+
* Owned by a single [com.coder.toolbox.views.CoderSetupWizardPage] instance
1111
* and lives as long as that wizard does, so it survives Toolbox visibility
1212
* cycles without leaking across wizard recreations.
1313
*/
@@ -58,4 +58,10 @@ class WizardModel {
5858

5959
enum class WizardStep {
6060
URL_REQUEST, TOKEN_REQUEST, CONNECT;
61+
}
62+
63+
sealed interface Credentials {
64+
object MTls : Credentials
65+
data class Token(val value: String) : Credentials
66+
data class OAuth(val session: CoderOAuthSessionContext) : Credentials
6167
}

0 commit comments

Comments
 (0)