import { computed, readonly, ref } from 'vue'; import { isNativeAppState, updateIsBusy } from '@/lib/native-app'; import type { NativeAction, NativeAppState } from '@/types/native-app'; const state = ref(null); const pending = ref(false); const error = ref(null); let initializing = false; let refreshing: Promise | null = null; async function request( action: NativeAction, payload: Record = {}, ) { if (typeof window === 'undefined' || window.kanbanNative?.version !== 1) { throw new Error('bridgeUnavailable'); } let timer: ReturnType | undefined; try { return await Promise.race([ window.kanbanNative.request(action, payload), new Promise((_, reject) => { timer = setTimeout( () => reject(new Error('bridgeTimeout')), 11000, ); }), ]); } finally { clearTimeout(timer); } } async function refresh() { if (refreshing) { return refreshing; } refreshing = (async () => { const result = await request('getState'); if (!isNativeAppState(result)) { throw new Error('incompatibleVersion'); } state.value = result; })(); try { await refreshing; } finally { refreshing = null; } } export async function initializeNativeApp() { if ( initializing || state.value || typeof window === 'undefined' || window.kanbanNative?.version !== 1 ) { return; } initializing = true; try { await refresh(); await request('settingsReady'); } catch { state.value = null; } finally { initializing = false; } } export function useNativeApp() { const busy = computed( () => pending.value || updateIsBusy(state.value?.updates), ); async function act( action: NativeAction, payload: Record = {}, ) { if ( pending.value || (updateIsBusy(state.value?.updates) && action !== 'openSettings') ) { return false; } pending.value = true; error.value = null; try { await request(action, payload); if (action !== 'setWorkspace') { await refresh(); } return true; } catch (cause) { error.value = cause instanceof Error ? cause.message : 'bridgeUnavailable'; return false; } finally { pending.value = false; } } async function poll() { try { await refresh(); } catch (cause) { error.value = cause instanceof Error ? cause.message : 'bridgeUnavailable'; } } return { state: readonly(state), busy, error: readonly(error), act, poll }; } export async function recoverNativeWorkspace() { if (!state.value) { return; } try { await request('openSettings'); } catch { /* Native recovery remains available from the app menu or toolbar. */ } } export function registerNativeRecovery() { window.addEventListener('offline', () => { void recoverNativeWorkspace(); }); document.addEventListener('inertia:exception', () => { void recoverNativeWorkspace(); }); document.addEventListener('inertia:invalid', (event) => { if (event.detail.response.status >= 400) { void recoverNativeWorkspace(); } }); }