Преглед изворни кода

feat: integrate multistep auth flows into desktop app (#18103)

Aiden Cline пре 1 месец
родитељ
комит
8e09e8c612

+ 134 - 16
packages/app/src/components/dialog-connect-provider.tsx

@@ -15,7 +15,6 @@ import { Link } from "@/components/link"
 import { useLanguage } from "@/context/language"
 import { useGlobalSDK } from "@/context/global-sdk"
 import { useGlobalSync } from "@/context/global-sync"
-import { usePlatform } from "@/context/platform"
 import { DialogSelectModel } from "./dialog-select-model"
 import { DialogSelectProvider } from "./dialog-select-provider"
 
@@ -23,7 +22,6 @@ export function DialogConnectProvider(props: { provider: string }) {
   const dialog = useDialog()
   const globalSync = useGlobalSync()
   const globalSDK = useGlobalSDK()
-  const platform = usePlatform()
   const language = useLanguage()
 
   const alive = { value: true }
@@ -49,13 +47,14 @@ export function DialogConnectProvider(props: { provider: string }) {
   const [store, setStore] = createStore({
     methodIndex: undefined as undefined | number,
     authorization: undefined as undefined | ProviderAuthAuthorization,
-    state: "pending" as undefined | "pending" | "complete" | "error",
+    state: "pending" as undefined | "pending" | "complete" | "error" | "prompt",
     error: undefined as string | undefined,
   })
 
   type Action =
     | { type: "method.select"; index: number }
     | { type: "method.reset" }
+    | { type: "auth.prompt" }
     | { type: "auth.pending" }
     | { type: "auth.complete"; authorization: ProviderAuthAuthorization }
     | { type: "auth.error"; error: string }
@@ -77,6 +76,11 @@ export function DialogConnectProvider(props: { provider: string }) {
           draft.error = undefined
           return
         }
+        if (action.type === "auth.prompt") {
+          draft.state = "prompt"
+          draft.error = undefined
+          return
+        }
         if (action.type === "auth.pending") {
           draft.state = "pending"
           draft.error = undefined
@@ -120,7 +124,7 @@ export function DialogConnectProvider(props: { provider: string }) {
     return fallback
   }
 
-  async function selectMethod(index: number) {
+  async function selectMethod(index: number, inputs?: Record<string, string>) {
     if (timer.current !== undefined) {
       clearTimeout(timer.current)
       timer.current = undefined
@@ -130,6 +134,10 @@ export function DialogConnectProvider(props: { provider: string }) {
     dispatch({ type: "method.select", index })
 
     if (method.type === "oauth") {
+      if (method.prompts?.length && !inputs) {
+        dispatch({ type: "auth.prompt" })
+        return
+      }
       dispatch({ type: "auth.pending" })
       const start = Date.now()
       await globalSDK.client.provider.oauth
@@ -137,6 +145,7 @@ export function DialogConnectProvider(props: { provider: string }) {
           {
             providerID: props.provider,
             method: index,
+            inputs,
           },
           { throwOnError: true },
         )
@@ -163,6 +172,122 @@ export function DialogConnectProvider(props: { provider: string }) {
     }
   }
 
+  function OAuthPromptsView() {
+    const [formStore, setFormStore] = createStore({
+      value: {} as Record<string, string>,
+      index: 0,
+    })
+
+    const prompts = createMemo(() => method()?.prompts ?? [])
+    const matches = (prompt: NonNullable<ReturnType<typeof prompts>[number]>, value: Record<string, string>) => {
+      if (!prompt.when) return true
+      const actual = value[prompt.when.key]
+      if (actual === undefined) return false
+      return prompt.when.op === "eq" ? actual === prompt.when.value : actual !== prompt.when.value
+    }
+    const current = createMemo(() => {
+      const all = prompts()
+      const index = all.findIndex((prompt, index) => index >= formStore.index && matches(prompt, formStore.value))
+      if (index === -1) return
+      return {
+        index,
+        prompt: all[index],
+      }
+    })
+    const valid = createMemo(() => {
+      const item = current()
+      if (!item || item.prompt.type !== "text") return false
+      const value = formStore.value[item.prompt.key] ?? ""
+      return value.trim().length > 0
+    })
+
+    async function next(index: number, value: Record<string, string>) {
+      if (store.methodIndex === undefined) return
+      const next = prompts().findIndex((prompt, i) => i > index && matches(prompt, value))
+      if (next !== -1) {
+        setFormStore("index", next)
+        return
+      }
+      await selectMethod(store.methodIndex, value)
+    }
+
+    async function handleSubmit(e: SubmitEvent) {
+      e.preventDefault()
+      const item = current()
+      if (!item || item.prompt.type !== "text") return
+      if (!valid()) return
+      await next(item.index, formStore.value)
+    }
+
+    const item = () => current()
+    const text = createMemo(() => {
+      const prompt = item()?.prompt
+      if (!prompt || prompt.type !== "text") return
+      return prompt
+    })
+    const select = createMemo(() => {
+      const prompt = item()?.prompt
+      if (!prompt || prompt.type !== "select") return
+      return prompt
+    })
+
+    return (
+      <form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
+        <Switch>
+          <Match when={item()?.prompt.type === "text"}>
+            <TextField
+              type="text"
+              label={text()?.message ?? ""}
+              placeholder={text()?.placeholder}
+              value={text() ? (formStore.value[text()!.key] ?? "") : ""}
+              onChange={(value) => {
+                const prompt = text()
+                if (!prompt) return
+                setFormStore("value", prompt.key, value)
+              }}
+            />
+            <Button class="w-auto" type="submit" size="large" variant="primary" disabled={!valid()}>
+              {language.t("common.continue")}
+            </Button>
+          </Match>
+          <Match when={item()?.prompt.type === "select"}>
+            <div class="w-full flex flex-col gap-1.5">
+              <div class="text-14-regular text-text-base">{select()?.message}</div>
+              <div>
+                <List
+                  items={select()?.options ?? []}
+                  key={(x) => x.value}
+                  current={select()?.options.find((x) => x.value === formStore.value[select()!.key])}
+                  onSelect={(value) => {
+                    if (!value) return
+                    const prompt = select()
+                    if (!prompt) return
+                    const nextValue = {
+                      ...formStore.value,
+                      [prompt.key]: value.value,
+                    }
+                    setFormStore("value", prompt.key, value.value)
+                    void next(item()!.index, nextValue)
+                  }}
+                >
+                  {(option) => (
+                    <div class="w-full flex items-center gap-x-2">
+                      <div class="w-4 h-2 rounded-[1px] bg-input-base shadow-xs-border-base flex items-center justify-center">
+                        <div class="w-2.5 h-0.5 ml-0 bg-icon-strong-base hidden" data-slot="list-item-extra-icon" />
+                      </div>
+                      <span>{option.label}</span>
+                      <span class="text-14-regular text-text-weak">{option.hint}</span>
+                    </div>
+                  )}
+                </List>
+              </div>
+            </div>
+          </Match>
+        </Switch>
+      </form>
+    )
+  }
+
   let listRef: ListRef | undefined
   function handleKey(e: KeyboardEvent) {
     if (e.key === "Enter" && e.target instanceof HTMLInputElement) {
@@ -301,7 +426,7 @@ export function DialogConnectProvider(props: { provider: string }) {
             error={formStore.error}
           />
           <Button class="w-auto" type="submit" size="large" variant="primary">
-            {language.t("common.submit")}
+            {language.t("common.continue")}
           </Button>
         </form>
       </div>
@@ -314,12 +439,6 @@ export function DialogConnectProvider(props: { provider: string }) {
       error: undefined as string | undefined,
     })
 
-    onMount(() => {
-      if (store.authorization?.method === "code" && store.authorization?.url) {
-        platform.openLink(store.authorization.url)
-      }
-    })
-
     async function handleSubmit(e: SubmitEvent) {
       e.preventDefault()
 
@@ -368,7 +487,7 @@ export function DialogConnectProvider(props: { provider: string }) {
             error={formStore.error}
           />
           <Button class="w-auto" type="submit" size="large" variant="primary">
-            {language.t("common.submit")}
+            {language.t("common.continue")}
           </Button>
         </form>
       </div>
@@ -386,10 +505,6 @@ export function DialogConnectProvider(props: { provider: string }) {
 
     onMount(() => {
       void (async () => {
-        if (store.authorization?.url) {
-          platform.openLink(store.authorization.url)
-        }
-
         const result = await globalSDK.client.provider.oauth
           .callback({
             providerID: props.provider,
@@ -470,6 +585,9 @@ export function DialogConnectProvider(props: { provider: string }) {
                   </div>
                 </div>
               </Match>
+              <Match when={store.state === "prompt"}>
+                <OAuthPromptsView />
+              </Match>
               <Match when={store.state === "error"}>
                 <div class="text-14-regular text-text-base">
                   <div class="flex items-center gap-x-2">

+ 1 - 0
packages/app/src/i18n/ar.ts

@@ -204,6 +204,7 @@ export const dict = {
   "common.cancel": "إلغاء",
   "common.connect": "اتصال",
   "common.disconnect": "قطع الاتصال",
+  "common.continue": "إرسال",
   "common.submit": "إرسال",
   "common.save": "حفظ",
   "common.saving": "جارٍ الحفظ...",

+ 1 - 0
packages/app/src/i18n/br.ts

@@ -204,6 +204,7 @@ export const dict = {
   "common.cancel": "Cancelar",
   "common.connect": "Conectar",
   "common.disconnect": "Desconectar",
+  "common.continue": "Enviar",
   "common.submit": "Enviar",
   "common.save": "Salvar",
   "common.saving": "Salvando...",

+ 1 - 0
packages/app/src/i18n/bs.ts

@@ -221,6 +221,7 @@ export const dict = {
   "common.cancel": "Otkaži",
   "common.connect": "Poveži",
   "common.disconnect": "Prekini vezu",
+  "common.continue": "Pošalji",
   "common.submit": "Pošalji",
   "common.save": "Sačuvaj",
   "common.saving": "Čuvanje...",

+ 1 - 0
packages/app/src/i18n/da.ts

@@ -219,6 +219,7 @@ export const dict = {
   "common.cancel": "Annuller",
   "common.connect": "Forbind",
   "common.disconnect": "Frakobl",
+  "common.continue": "Indsend",
   "common.submit": "Indsend",
   "common.save": "Gem",
   "common.saving": "Gemmer...",

+ 1 - 0
packages/app/src/i18n/de.ts

@@ -209,6 +209,7 @@ export const dict = {
   "common.cancel": "Abbrechen",
   "common.connect": "Verbinden",
   "common.disconnect": "Trennen",
+  "common.continue": "Absenden",
   "common.submit": "Absenden",
   "common.save": "Speichern",
   "common.saving": "Speichert...",

+ 1 - 0
packages/app/src/i18n/en.ts

@@ -221,6 +221,7 @@ export const dict = {
   "common.open": "Open",
   "common.connect": "Connect",
   "common.disconnect": "Disconnect",
+  "common.continue": "Continue",
   "common.submit": "Submit",
   "common.save": "Save",
   "common.saving": "Saving...",

+ 1 - 0
packages/app/src/i18n/es.ts

@@ -220,6 +220,7 @@ export const dict = {
   "common.cancel": "Cancelar",
   "common.connect": "Conectar",
   "common.disconnect": "Desconectar",
+  "common.continue": "Enviar",
   "common.submit": "Enviar",
   "common.save": "Guardar",
   "common.saving": "Guardando...",

+ 1 - 0
packages/app/src/i18n/fr.ts

@@ -204,6 +204,7 @@ export const dict = {
   "common.cancel": "Annuler",
   "common.connect": "Connecter",
   "common.disconnect": "Déconnecter",
+  "common.continue": "Soumettre",
   "common.submit": "Soumettre",
   "common.save": "Enregistrer",
   "common.saving": "Enregistrement...",

+ 1 - 0
packages/app/src/i18n/ja.ts

@@ -203,6 +203,7 @@ export const dict = {
   "common.cancel": "キャンセル",
   "common.connect": "接続",
   "common.disconnect": "切断",
+  "common.continue": "送信",
   "common.submit": "送信",
   "common.save": "保存",
   "common.saving": "保存中...",

+ 1 - 0
packages/app/src/i18n/ko.ts

@@ -207,6 +207,7 @@ export const dict = {
   "common.cancel": "취소",
   "common.connect": "연결",
   "common.disconnect": "연결 해제",
+  "common.continue": "제출",
   "common.submit": "제출",
   "common.save": "저장",
   "common.saving": "저장 중...",

+ 1 - 0
packages/app/src/i18n/no.ts

@@ -223,6 +223,7 @@ export const dict = {
   "common.cancel": "Avbryt",
   "common.connect": "Koble til",
   "common.disconnect": "Koble fra",
+  "common.continue": "Send inn",
   "common.submit": "Send inn",
   "common.save": "Lagre",
   "common.saving": "Lagrer...",

+ 1 - 0
packages/app/src/i18n/pl.ts

@@ -205,6 +205,7 @@ export const dict = {
   "common.cancel": "Anuluj",
   "common.connect": "Połącz",
   "common.disconnect": "Rozłącz",
+  "common.continue": "Prześlij",
   "common.submit": "Prześlij",
   "common.save": "Zapisz",
   "common.saving": "Zapisywanie...",

+ 1 - 0
packages/app/src/i18n/ru.ts

@@ -220,6 +220,7 @@ export const dict = {
   "common.cancel": "Отмена",
   "common.connect": "Подключить",
   "common.disconnect": "Отключить",
+  "common.continue": "Отправить",
   "common.submit": "Отправить",
   "common.save": "Сохранить",
   "common.saving": "Сохранение...",

+ 1 - 0
packages/app/src/i18n/th.ts

@@ -220,6 +220,7 @@ export const dict = {
   "common.cancel": "ยกเลิก",
   "common.connect": "เชื่อมต่อ",
   "common.disconnect": "ยกเลิกการเชื่อมต่อ",
+  "common.continue": "ส่ง",
   "common.submit": "ส่ง",
   "common.save": "บันทึก",
   "common.saving": "กำลังบันทึก...",

+ 1 - 0
packages/app/src/i18n/tr.ts

@@ -225,6 +225,7 @@ export const dict = {
   "common.cancel": "İptal",
   "common.connect": "Bağlan",
   "common.disconnect": "Bağlantı Kes",
+  "common.continue": "Gönder",
   "common.submit": "Gönder",
   "common.save": "Kaydet",
   "common.saving": "Kaydediliyor...",

+ 1 - 0
packages/app/src/i18n/zh.ts

@@ -242,6 +242,7 @@ export const dict = {
   "common.cancel": "取消",
   "common.connect": "连接",
   "common.disconnect": "断开连接",
+  "common.continue": "提交",
   "common.submit": "提交",
   "common.save": "保存",
   "common.saving": "保存中...",

+ 1 - 0
packages/app/src/i18n/zht.ts

@@ -220,6 +220,7 @@ export const dict = {
   "common.cancel": "取消",
   "common.connect": "連線",
   "common.disconnect": "中斷連線",
+  "common.continue": "提交",
   "common.submit": "提交",
   "common.save": "儲存",
   "common.saving": "儲存中...",