Files
moeka-project/apps/ui-server-auth/src/pages/electron-callback.vue
T

213 lines
5.4 KiB
Vue

<script setup lang="ts">
import { onMounted, shallowRef } from 'vue'
import { parseElectronCallbackQuery } from '../composables/electron-callback.shared'
type CallbackStatus = 'loading' | 'success' | 'fallback' | 'error'
interface CallbackViewModel {
status: CallbackStatus
title: string
description: string
detail?: string
primaryActionLabel?: string
secondaryActionLabel?: string
primaryActionDisabled?: boolean
relayUrl?: string
}
const viewModel = shallowRef<CallbackViewModel>({
description: 'Checking your sign-in response and preparing the handoff to AIRI.',
primaryActionDisabled: true,
status: 'loading',
title: 'Completing sign-in',
})
function setViewModel(next: CallbackViewModel) {
viewModel.value = next
}
function openRelayUrl() {
if (!viewModel.value.relayUrl)
return
window.location.assign(viewModel.value.relayUrl)
}
function copyRelayUrl() {
if (!viewModel.value.relayUrl)
return
void navigator.clipboard?.writeText(viewModel.value.relayUrl)
}
async function runRelayFlow() {
const parsed = parseElectronCallbackQuery(new URLSearchParams(window.location.search))
if (parsed.status === 'error') {
setViewModel({
description: 'We could not use this sign-in response.',
detail: parsed.message,
status: 'error',
title: 'Sign-in failed',
})
return
}
setViewModel({
description: 'Passing your sign-in back to AIRI now. This page should close in a moment.',
primaryActionDisabled: true,
relayUrl: parsed.relayUrl,
status: 'loading',
title: 'Opening AIRI',
})
try {
await fetch(parsed.relayUrl)
setViewModel({
description: 'AIRI accepted the sign-in response. This tab will try to close itself now.',
detail: 'If nothing happens, you can close this tab manually and return to AIRI.',
relayUrl: parsed.relayUrl,
status: 'success',
title: 'You are signed in',
})
window.setTimeout(() => {
window.close()
}, 480)
window.setTimeout(() => {
setViewModel({
description: 'AIRI accepted the sign-in response. You can close this tab and continue in the app.',
detail: 'Some browsers do not allow this page to close itself automatically.',
relayUrl: parsed.relayUrl,
secondaryActionLabel: 'Copy callback link',
status: 'success',
title: 'You are signed in',
})
}, 1200)
}
catch {
setViewModel({
description: 'The browser could not reach AIRI through the local callback port.',
detail: 'We will try opening the local handoff directly. If that still fails, use the button below.',
primaryActionDisabled: false,
primaryActionLabel: 'Open AIRI manually',
relayUrl: parsed.relayUrl,
status: 'fallback',
title: 'Finish sign-in in AIRI',
})
window.setTimeout(() => {
window.location.replace(parsed.relayUrl)
}, 180)
window.setTimeout(() => {
setViewModel({
description: 'Automatic handoff did not finish in this browser session.',
detail: parsed.relayUrl,
primaryActionDisabled: false,
primaryActionLabel: 'Open AIRI manually',
relayUrl: parsed.relayUrl,
secondaryActionLabel: 'Copy callback link',
status: 'fallback',
title: 'Open AIRI to continue',
})
}, 960)
}
}
onMounted(() => {
void runRelayFlow()
})
</script>
<template>
<main
:class="[
'min-h-screen flex items-center justify-center px-6 py-10',
]"
>
<div
:class="[
'max-w-xl w-full rounded-xl border border-neutral-200 bg-white p-6',
'dark:border-neutral-800 dark:bg-neutral-900',
]"
>
<div
:class="[
'text-2xl font-bold',
]"
>
{{ viewModel.title }}
</div>
<p
:class="[
'mt-3 text-sm text-neutral-600 dark:text-neutral-300',
]"
>
{{ viewModel.description }}
</p>
<p
v-if="viewModel.detail"
:class="[
'mt-3 break-all text-xs text-neutral-500 dark:text-neutral-400',
]"
>
{{ viewModel.detail }}
</p>
<div
:class="[
'mt-6 flex flex-wrap items-center gap-2',
]"
>
<button
v-if="viewModel.primaryActionLabel"
type="button"
:disabled="viewModel.primaryActionDisabled"
:class="[
'rounded-md border border-neutral-300 px-3 py-2 text-sm',
'disabled:cursor-not-allowed disabled:opacity-50',
'dark:border-neutral-700',
]"
@click="openRelayUrl"
>
{{ viewModel.primaryActionLabel }}
</button>
<button
v-if="viewModel.secondaryActionLabel"
type="button"
:class="[
'rounded-md border border-neutral-300 px-3 py-2 text-sm',
'dark:border-neutral-700',
]"
@click="copyRelayUrl"
>
{{ viewModel.secondaryActionLabel }}
</button>
</div>
<a
v-if="viewModel.relayUrl && viewModel.status === 'fallback'"
:class="[
'mt-4 block break-all text-xs text-neutral-500 underline decoration-dotted underline-offset-4',
'hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200',
]"
:href="viewModel.relayUrl"
>
{{ viewModel.relayUrl }}
</a>
</div>
</main>
</template>
<route lang="yaml">
meta:
layout: plain
</route>