4
4
*/
5
5
6
6
import * as vscode from 'vscode'
7
- import { CodeCatalystClient } from '../shared/clients/codecatalystClient'
7
+ import { CodeCatalystClient , createClient } from '../shared/clients/codecatalystClient'
8
8
import { getIdeProperties } from '../shared/extensionUtilities'
9
9
import {
10
10
Auth ,
@@ -19,8 +19,9 @@ import {
19
19
import { getSecondaryAuth } from '../credentials/secondaryAuth'
20
20
import { getLogger } from '../shared/logger'
21
21
import * as localizedText from '../shared/localizedText'
22
- import { ToolkitError } from '../shared/errors'
22
+ import { ToolkitError , isAwsError } from '../shared/errors'
23
23
import { MetricName , MetricShapes , telemetry } from '../shared/telemetry/telemetry'
24
+ import { openUrl } from '../shared/utilities/vsCodeUtils'
24
25
25
26
// Secrets stored on the macOS keychain appear as individual entries for each key
26
27
// This is fine so long as the user has only a few accounts. Otherwise this should
@@ -37,6 +38,8 @@ export class CodeCatalystAuthStorage {
37
38
}
38
39
}
39
40
41
+ export const onboardingUrl = vscode . Uri . parse ( 'https://codecatalyst.aws/onboarding/view' )
42
+
40
43
const defaultScopes = [ ...ssoAccountAccessScopes , ...codecatalystScopes ]
41
44
export const isValidCodeCatalystConnection = ( conn : Connection ) : conn is SsoConnection =>
42
45
isBuilderIdConnection ( conn ) && hasScopes ( conn , codecatalystScopes )
@@ -104,6 +107,19 @@ export class CodeCatalystAuthenticationProvider {
104
107
await this . secondaryAuth . restoreConnection ( )
105
108
}
106
109
110
+ public async promptOnboarding ( ) : Promise < void > {
111
+ const message = `Using CodeCatalyst requires onboarding with a Space. Sign up with CodeCatalyst to get started.`
112
+ const openBrowser = 'Open Browser'
113
+ const resp = await vscode . window . showInformationMessage ( message , { modal : true } , openBrowser )
114
+ if ( resp === openBrowser ) {
115
+ await openUrl ( onboardingUrl )
116
+ }
117
+
118
+ // Mark the current execution as cancelled regardless of the user response. We could poll here instead, waiting
119
+ // for the user to onboard. But that might take a while.
120
+ throw new ToolkitError ( 'Not onboarded with CodeCatalyst' , { code : 'NotOnboarded' , cancelled : true } )
121
+ }
122
+
107
123
public async promptNotConnected ( ) : Promise < SsoConnection > {
108
124
type ConnectionFlowEvent = Partial < MetricShapes [ MetricName ] > & {
109
125
readonly codecatalyst_connectionFlow : 'Create' | 'Switch' | 'Upgrade' // eslint-disable-line @typescript-eslint/naming-convention
@@ -114,8 +130,9 @@ export class CodeCatalystAuthenticationProvider {
114
130
const cancelItem : vscode . MessageItem = { title : localizedText . cancel , isCloseAffordance : true }
115
131
116
132
if ( conn === undefined ) {
117
- // TODO: change to `satisfies` on TS 4.9
118
- telemetry . record ( { codecatalyst_connectionFlow : 'Create' } as ConnectionFlowEvent )
133
+ telemetry . record ( {
134
+ codecatalyst_connectionFlow : 'Create' ,
135
+ } satisfies ConnectionFlowEvent as MetricShapes [ MetricName ] )
119
136
120
137
const message = `The ${
121
138
getIdeProperties ( ) . company
@@ -135,15 +152,17 @@ export class CodeCatalystAuthenticationProvider {
135
152
}
136
153
137
154
const upgrade = async ( ) => {
138
- // TODO: change to `satisfies` on TS 4.9
139
- telemetry . record ( { codecatalyst_connectionFlow : 'Upgrade' } as ConnectionFlowEvent )
155
+ telemetry . record ( {
156
+ codecatalyst_connectionFlow : 'Upgrade' ,
157
+ } satisfies ConnectionFlowEvent as MetricShapes [ MetricName ] )
140
158
141
159
return this . secondaryAuth . addScopes ( conn , defaultScopes )
142
160
}
143
161
144
162
if ( isBuilderIdConnection ( conn ) && this . auth . activeConnection ?. id !== conn . id ) {
145
- // TODO: change to `satisfies` on TS 4.9
146
- telemetry . record ( { codecatalyst_connectionFlow : 'Switch' } as ConnectionFlowEvent )
163
+ telemetry . record ( {
164
+ codecatalyst_connectionFlow : 'Switch' ,
165
+ } satisfies ConnectionFlowEvent as MetricShapes [ MetricName ] )
147
166
148
167
const resp = await vscode . window . showInformationMessage (
149
168
'CodeCatalyst requires an AWS Builder ID connection.\n\n Switch to it now?' ,
@@ -171,6 +190,41 @@ export class CodeCatalystAuthenticationProvider {
171
190
throw new ToolkitError ( 'Not connected to CodeCatalyst' , { code : 'NoConnectionBadState' } )
172
191
}
173
192
193
+ public async isConnectionOnboarded ( conn : SsoConnection , recheck = false ) {
194
+ const mementoKey = 'codecatalyst.connections'
195
+ const getState = ( ) => this . memento . get ( mementoKey , { } as Record < string , { onboarded : boolean } > )
196
+ const updateState = ( state : { onboarded : boolean } ) =>
197
+ this . memento . update ( mementoKey , {
198
+ ...getState ( ) ,
199
+ [ conn . id ] : state ,
200
+ } )
201
+
202
+ const state = getState ( ) [ conn . id ]
203
+ if ( state !== undefined && ! recheck ) {
204
+ return state . onboarded
205
+ }
206
+
207
+ try {
208
+ await createClient ( conn )
209
+ await updateState ( { onboarded : true } )
210
+
211
+ return true
212
+ } catch ( e ) {
213
+ if ( isOnboardingException ( e ) && this . auth . getConnectionState ( conn ) === 'valid' ) {
214
+ await updateState ( { onboarded : false } )
215
+
216
+ return false
217
+ }
218
+
219
+ throw e
220
+ }
221
+
222
+ function isOnboardingException ( e : unknown ) {
223
+ // `GetUserDetails` returns `AccessDeniedException` if the user has not onboarded
224
+ return isAwsError ( e ) && e . code === 'AccessDeniedException' && e . message . includes ( 'GetUserDetails' )
225
+ }
226
+ }
227
+
174
228
private static instance : CodeCatalystAuthenticationProvider
175
229
176
230
public static fromContext ( ctx : Pick < vscode . ExtensionContext , 'secrets' | 'globalState' > ) {
0 commit comments