+
{t('accounts_heading')}
+ {accounts.map((account) => {
+ const host = new URL(account.origin).host;
+ return (
+
+
+ {account.connected
+ ? t('account_on', { label: account.username ? `${account.label} (${account.username})` : account.label, host })
+ : t('account_waiting', { label: account.label, host })}
+
+
+
+ );
+ })}
+
+ );
+}
+
// ─── Auto-generated Setting Field ────────────────────────────
interface PluginSettingFieldProps {
diff --git a/lib/plugin-sandbox/connect-dialog.ts b/lib/plugin-sandbox/connect-dialog.ts
new file mode 100644
index 000000000..18c86d184
--- /dev/null
+++ b/lib/plugin-sandbox/connect-dialog.ts
@@ -0,0 +1,148 @@
+// Host-side "Connect account" requests. A plugin asks to connect one of the
+// user-scope credentials its manifest declares (api.proxy.connect); the host
+// shows its own dialog naming the plugin and the server, the user types the
+// password there, and it goes straight to /api/plugin-secrets. The plugin
+// only learns whether the account is connected.
+
+import type { UserCredentialSpec } from '@/lib/plugin-secrets/credentials';
+import { apiFetch } from '../browser-navigation';
+
+export interface ConnectRequest {
+ id: string;
+ pluginId: string;
+ pluginName: string;
+ credentialId: string;
+ spec: UserCredentialSpec;
+ /**
+ * `password`: the user types a password or key here.
+ * `signin`: only the server is approved here; the plugin then runs a
+ * sign-in flow whose answer the proxy captures (Nextcloud Login Flow v2).
+ */
+ mode: 'password' | 'signin';
+ /** Server address to suggest when the manifest leaves it to the user. */
+ suggestedOrigin?: string;
+ suggestedUsername?: string;
+ resolve: (result: ConnectResult) => void;
+}
+
+export type ConnectResult = { connected: boolean; origin?: string };
+
+let current: ConnectRequest | null = null;
+const listeners = new Set<() => void>();
+
+function notify(): void {
+ for (const l of listeners) {
+ try { l(); } catch { /* ignore */ }
+ }
+}
+
+export function subscribeConnect(listener: () => void): () => void {
+ listeners.add(listener);
+ return () => { listeners.delete(listener); };
+}
+
+export function currentConnect(): ConnectRequest | null {
+ return current;
+}
+
+export class ConnectBusyError extends Error {
+ constructor() {
+ super('Another account connection is already open');
+ }
+}
+
+/** Ask the user to connect an account. One request at a time, app-wide. */
+export function requestConnect(input: Omit