Skip to content

Commit 8e09e8c

Browse files
authored
feat: integrate multistep auth flows into desktop app (#18103)
1 parent 84e62fc commit 8e09e8c

18 files changed

Lines changed: 151 additions & 16 deletions

File tree

packages/app/src/components/dialog-connect-provider.tsx

Lines changed: 134 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,13 @@ import { Link } from "@/components/link"
1515
import { useLanguage } from "@/context/language"
1616
import { useGlobalSDK } from "@/context/global-sdk"
1717
import { useGlobalSync } from "@/context/global-sync"
18-
import { usePlatform } from "@/context/platform"
1918
import { DialogSelectModel } from "./dialog-select-model"
2019
import { DialogSelectProvider } from "./dialog-select-provider"
2120

2221
export function DialogConnectProvider(props: { provider: string }) {
2322
const dialog = useDialog()
2423
const globalSync = useGlobalSync()
2524
const globalSDK = useGlobalSDK()
26-
const platform = usePlatform()
2725
const language = useLanguage()
2826

2927
const alive = { value: true }
@@ -49,13 +47,14 @@ export function DialogConnectProvider(props: { provider: string }) {
4947
const [store, setStore] = createStore({
5048
methodIndex: undefined as undefined | number,
5149
authorization: undefined as undefined | ProviderAuthAuthorization,
52-
state: "pending" as undefined | "pending" | "complete" | "error",
50+
state: "pending" as undefined | "pending" | "complete" | "error" | "prompt",
5351
error: undefined as string | undefined,
5452
})
5553

5654
type Action =
5755
| { type: "method.select"; index: number }
5856
| { type: "method.reset" }
57+
| { type: "auth.prompt" }
5958
| { type: "auth.pending" }
6059
| { type: "auth.complete"; authorization: ProviderAuthAuthorization }
6160
| { type: "auth.error"; error: string }
@@ -77,6 +76,11 @@ export function DialogConnectProvider(props: { provider: string }) {
7776
draft.error = undefined
7877
return
7978
}
79+
if (action.type === "auth.prompt") {
80+
draft.state = "prompt"
81+
draft.error = undefined
82+
return
83+
}
8084
if (action.type === "auth.pending") {
8185
draft.state = "pending"
8286
draft.error = undefined
@@ -120,7 +124,7 @@ export function DialogConnectProvider(props: { provider: string }) {
120124
return fallback
121125
}
122126

123-
async function selectMethod(index: number) {
127+
async function selectMethod(index: number, inputs?: Record<string, string>) {
124128
if (timer.current !== undefined) {
125129
clearTimeout(timer.current)
126130
timer.current = undefined
@@ -130,13 +134,18 @@ export function DialogConnectProvider(props: { provider: string }) {
130134
dispatch({ type: "method.select", index })
131135

132136
if (method.type === "oauth") {
137+
if (method.prompts?.length && !inputs) {
138+
dispatch({ type: "auth.prompt" })
139+
return
140+
}
133141
dispatch({ type: "auth.pending" })
134142
const start = Date.now()
135143
await globalSDK.client.provider.oauth
136144
.authorize(
137145
{
138146
providerID: props.provider,
139147
method: index,
148+
inputs,
140149
},
141150
{ throwOnError: true },
142151
)
@@ -163,6 +172,122 @@ export function DialogConnectProvider(props: { provider: string }) {
163172
}
164173
}
165174

175+
function OAuthPromptsView() {
176+
const [formStore, setFormStore] = createStore({
177+
value: {} as Record<string, string>,
178+
index: 0,
179+
})
180+
181+
const prompts = createMemo(() => method()?.prompts ?? [])
182+
const matches = (prompt: NonNullable<ReturnType<typeof prompts>[number]>, value: Record<string, string>) => {
183+
if (!prompt.when) return true
184+
const actual = value[prompt.when.key]
185+
if (actual === undefined) return false
186+
return prompt.when.op === "eq" ? actual === prompt.when.value : actual !== prompt.when.value
187+
}
188+
const current = createMemo(() => {
189+
const all = prompts()
190+
const index = all.findIndex((prompt, index) => index >= formStore.index && matches(prompt, formStore.value))
191+
if (index === -1) return
192+
return {
193+
index,
194+
prompt: all[index],
195+
}
196+
})
197+
const valid = createMemo(() => {
198+
const item = current()
199+
if (!item || item.prompt.type !== "text") return false
200+
const value = formStore.value[item.prompt.key] ?? ""
201+
return value.trim().length > 0
202+
})
203+
204+
async function next(index: number, value: Record<string, string>) {
205+
if (store.methodIndex === undefined) return
206+
const next = prompts().findIndex((prompt, i) => i > index && matches(prompt, value))
207+
if (next !== -1) {
208+
setFormStore("index", next)
209+
return
210+
}
211+
await selectMethod(store.methodIndex, value)
212+
}
213+
214+
async function handleSubmit(e: SubmitEvent) {
215+
e.preventDefault()
216+
const item = current()
217+
if (!item || item.prompt.type !== "text") return
218+
if (!valid()) return
219+
await next(item.index, formStore.value)
220+
}
221+
222+
const item = () => current()
223+
const text = createMemo(() => {
224+
const prompt = item()?.prompt
225+
if (!prompt || prompt.type !== "text") return
226+
return prompt
227+
})
228+
const select = createMemo(() => {
229+
const prompt = item()?.prompt
230+
if (!prompt || prompt.type !== "select") return
231+
return prompt
232+
})
233+
234+
return (
235+
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
236+
<Switch>
237+
<Match when={item()?.prompt.type === "text"}>
238+
<TextField
239+
type="text"
240+
label={text()?.message ?? ""}
241+
placeholder={text()?.placeholder}
242+
value={text() ? (formStore.value[text()!.key] ?? "") : ""}
243+
onChange={(value) => {
244+
const prompt = text()
245+
if (!prompt) return
246+
setFormStore("value", prompt.key, value)
247+
}}
248+
/>
249+
<Button class="w-auto" type="submit" size="large" variant="primary" disabled={!valid()}>
250+
{language.t("common.continue")}
251+
</Button>
252+
</Match>
253+
<Match when={item()?.prompt.type === "select"}>
254+
<div class="w-full flex flex-col gap-1.5">
255+
<div class="text-14-regular text-text-base">{select()?.message}</div>
256+
<div>
257+
<List
258+
items={select()?.options ?? []}
259+
key={(x) => x.value}
260+
current={select()?.options.find((x) => x.value === formStore.value[select()!.key])}
261+
onSelect={(value) => {
262+
if (!value) return
263+
const prompt = select()
264+
if (!prompt) return
265+
const nextValue = {
266+
...formStore.value,
267+
[prompt.key]: value.value,
268+
}
269+
setFormStore("value", prompt.key, value.value)
270+
void next(item()!.index, nextValue)
271+
}}
272+
>
273+
{(option) => (
274+
<div class="w-full flex items-center gap-x-2">
275+
<div class="w-4 h-2 rounded-[1px] bg-input-base shadow-xs-border-base flex items-center justify-center">
276+
<div class="w-2.5 h-0.5 ml-0 bg-icon-strong-base hidden" data-slot="list-item-extra-icon" />
277+
</div>
278+
<span>{option.label}</span>
279+
<span class="text-14-regular text-text-weak">{option.hint}</span>
280+
</div>
281+
)}
282+
</List>
283+
</div>
284+
</div>
285+
</Match>
286+
</Switch>
287+
</form>
288+
)
289+
}
290+
166291
let listRef: ListRef | undefined
167292
function handleKey(e: KeyboardEvent) {
168293
if (e.key === "Enter" && e.target instanceof HTMLInputElement) {
@@ -301,7 +426,7 @@ export function DialogConnectProvider(props: { provider: string }) {
301426
error={formStore.error}
302427
/>
303428
<Button class="w-auto" type="submit" size="large" variant="primary">
304-
{language.t("common.submit")}
429+
{language.t("common.continue")}
305430
</Button>
306431
</form>
307432
</div>
@@ -314,12 +439,6 @@ export function DialogConnectProvider(props: { provider: string }) {
314439
error: undefined as string | undefined,
315440
})
316441

317-
onMount(() => {
318-
if (store.authorization?.method === "code" && store.authorization?.url) {
319-
platform.openLink(store.authorization.url)
320-
}
321-
})
322-
323442
async function handleSubmit(e: SubmitEvent) {
324443
e.preventDefault()
325444

@@ -368,7 +487,7 @@ export function DialogConnectProvider(props: { provider: string }) {
368487
error={formStore.error}
369488
/>
370489
<Button class="w-auto" type="submit" size="large" variant="primary">
371-
{language.t("common.submit")}
490+
{language.t("common.continue")}
372491
</Button>
373492
</form>
374493
</div>
@@ -386,10 +505,6 @@ export function DialogConnectProvider(props: { provider: string }) {
386505

387506
onMount(() => {
388507
void (async () => {
389-
if (store.authorization?.url) {
390-
platform.openLink(store.authorization.url)
391-
}
392-
393508
const result = await globalSDK.client.provider.oauth
394509
.callback({
395510
providerID: props.provider,
@@ -470,6 +585,9 @@ export function DialogConnectProvider(props: { provider: string }) {
470585
</div>
471586
</div>
472587
</Match>
588+
<Match when={store.state === "prompt"}>
589+
<OAuthPromptsView />
590+
</Match>
473591
<Match when={store.state === "error"}>
474592
<div class="text-14-regular text-text-base">
475593
<div class="flex items-center gap-x-2">

packages/app/src/i18n/ar.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ export const dict = {
204204
"common.cancel": "إلغاء",
205205
"common.connect": "اتصال",
206206
"common.disconnect": "قطع الاتصال",
207+
"common.continue": "إرسال",
207208
"common.submit": "إرسال",
208209
"common.save": "حفظ",
209210
"common.saving": "جارٍ الحفظ...",

packages/app/src/i18n/br.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ export const dict = {
204204
"common.cancel": "Cancelar",
205205
"common.connect": "Conectar",
206206
"common.disconnect": "Desconectar",
207+
"common.continue": "Enviar",
207208
"common.submit": "Enviar",
208209
"common.save": "Salvar",
209210
"common.saving": "Salvando...",

packages/app/src/i18n/bs.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ export const dict = {
221221
"common.cancel": "Otkaži",
222222
"common.connect": "Poveži",
223223
"common.disconnect": "Prekini vezu",
224+
"common.continue": "Pošalji",
224225
"common.submit": "Pošalji",
225226
"common.save": "Sačuvaj",
226227
"common.saving": "Čuvanje...",

packages/app/src/i18n/da.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ export const dict = {
219219
"common.cancel": "Annuller",
220220
"common.connect": "Forbind",
221221
"common.disconnect": "Frakobl",
222+
"common.continue": "Indsend",
222223
"common.submit": "Indsend",
223224
"common.save": "Gem",
224225
"common.saving": "Gemmer...",

packages/app/src/i18n/de.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ export const dict = {
209209
"common.cancel": "Abbrechen",
210210
"common.connect": "Verbinden",
211211
"common.disconnect": "Trennen",
212+
"common.continue": "Absenden",
212213
"common.submit": "Absenden",
213214
"common.save": "Speichern",
214215
"common.saving": "Speichert...",

packages/app/src/i18n/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ export const dict = {
221221
"common.open": "Open",
222222
"common.connect": "Connect",
223223
"common.disconnect": "Disconnect",
224+
"common.continue": "Continue",
224225
"common.submit": "Submit",
225226
"common.save": "Save",
226227
"common.saving": "Saving...",

packages/app/src/i18n/es.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ export const dict = {
220220
"common.cancel": "Cancelar",
221221
"common.connect": "Conectar",
222222
"common.disconnect": "Desconectar",
223+
"common.continue": "Enviar",
223224
"common.submit": "Enviar",
224225
"common.save": "Guardar",
225226
"common.saving": "Guardando...",

packages/app/src/i18n/fr.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ export const dict = {
204204
"common.cancel": "Annuler",
205205
"common.connect": "Connecter",
206206
"common.disconnect": "Déconnecter",
207+
"common.continue": "Soumettre",
207208
"common.submit": "Soumettre",
208209
"common.save": "Enregistrer",
209210
"common.saving": "Enregistrement...",

packages/app/src/i18n/ja.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ export const dict = {
203203
"common.cancel": "キャンセル",
204204
"common.connect": "接続",
205205
"common.disconnect": "切断",
206+
"common.continue": "送信",
206207
"common.submit": "送信",
207208
"common.save": "保存",
208209
"common.saving": "保存中...",

0 commit comments

Comments
 (0)