mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-20 08:37:42 +02:00
[eric] OpenSwarm Pro: new managed plan alongside BYO — deep-link activation from billing checkout, onboarding + Settings card with live usage, gradient OpenSwarm Pro group in
the model picker, inline error cards for rate-limit/connection failures, debugger startup fix
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useAppDispatch } from '@/shared/hooks';
|
||||
import { activateSubscription } from '@/shared/state/settingsSlice';
|
||||
import { fetchModels } from '@/shared/state/modelsSlice';
|
||||
import { trackEvent } from '@/shared/analytics';
|
||||
|
||||
// Listens for openswarm://auth?token=...&plan=...&expires=... URLs coming
|
||||
// from the Electron main process via window.openswarm.onAuthUrl. Parses the
|
||||
// payload and dispatches activateSubscription so the backend validates and
|
||||
// persists the bearer.
|
||||
//
|
||||
// Safe no-op in web/browser contexts where window.openswarm isn't defined.
|
||||
export function useDeepLink(): void {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
const api = (window as any).openswarm as OpenSwarmAPI | undefined;
|
||||
if (!api?.onAuthUrl) return;
|
||||
|
||||
const unsubscribe = api.onAuthUrl((rawUrl: string) => {
|
||||
try {
|
||||
// openswarm://auth?token=... (host = "auth", search carries fields)
|
||||
const url = new URL(rawUrl);
|
||||
if (url.host !== 'auth' && url.pathname !== '//auth' && url.pathname !== '/auth') {
|
||||
console.warn('[deep-link] Unknown openswarm:// host:', url.host);
|
||||
return;
|
||||
}
|
||||
const token = url.searchParams.get('token');
|
||||
if (!token) {
|
||||
console.warn('[deep-link] Missing token in', rawUrl);
|
||||
return;
|
||||
}
|
||||
const plan = url.searchParams.get('plan');
|
||||
const expires = url.searchParams.get('expires');
|
||||
|
||||
trackEvent('subscription.deep_link_received', {
|
||||
plan: plan ?? 'unknown',
|
||||
});
|
||||
|
||||
dispatch(
|
||||
activateSubscription({
|
||||
token,
|
||||
plan,
|
||||
expires,
|
||||
}),
|
||||
)
|
||||
.unwrap()
|
||||
.then((res) => {
|
||||
trackEvent('subscription.activated', { plan: res.plan });
|
||||
// Re-fetch the model list so the Claude models (via OpenSwarm
|
||||
// Pro proxy) show up in the chat picker right away.
|
||||
dispatch(fetchModels());
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('[deep-link] Activation failed:', err);
|
||||
trackEvent('subscription.activation_failed', {
|
||||
message: String(err).slice(0, 120),
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[deep-link] Failed to parse URL', rawUrl, e);
|
||||
}
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [dispatch]);
|
||||
}
|
||||
@@ -27,6 +27,13 @@ export interface CustomProvider {
|
||||
models: Array<{ value: string; label: string; context_window?: number }>;
|
||||
}
|
||||
|
||||
export interface SubscriptionUsage {
|
||||
requests_in_window: number;
|
||||
plan_limit: number;
|
||||
window_hours: number;
|
||||
window_ends_at: number; // unix ms
|
||||
}
|
||||
|
||||
export interface AppSettings {
|
||||
default_system_prompt: string | null;
|
||||
default_folder: string | null;
|
||||
@@ -46,6 +53,20 @@ export interface AppSettings {
|
||||
expand_new_chats_in_dashboard: boolean;
|
||||
auto_reveal_sub_agents: boolean;
|
||||
dev_mode: boolean;
|
||||
// Optional managed-subscription state (surfaces only when user has
|
||||
// subscribed via the cloud). Mirrors AppSettings on the backend.
|
||||
connection_mode?: 'own_key' | 'openswarm-pro';
|
||||
openswarm_bearer_token?: string | null;
|
||||
openswarm_proxy_url?: string | null;
|
||||
openswarm_subscription_plan?: string | null;
|
||||
openswarm_subscription_expires?: string | null;
|
||||
openswarm_usage_cached?: SubscriptionUsage | null;
|
||||
}
|
||||
|
||||
export interface ActivateSubscriptionPayload {
|
||||
token: string;
|
||||
plan?: string | null;
|
||||
expires?: string | null;
|
||||
}
|
||||
|
||||
export interface BrowseResult {
|
||||
@@ -123,6 +144,36 @@ export const browseDirectories = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
// POST /api/subscription/activate — called after the desktop catches an
|
||||
// openswarm://auth?token=... deep link. Validates + persists on the backend,
|
||||
// then refreshes settings so the Settings UI flips to "Pro" mode.
|
||||
export const activateSubscription = createAsyncThunk(
|
||||
'settings/activateSubscription',
|
||||
async (payload: ActivateSubscriptionPayload, { dispatch }) => {
|
||||
const res = await fetch(`${API_BASE}/subscription/activate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!res.ok) throw new Error((await res.text()) || 'Activation failed');
|
||||
// Pull the fresh settings so UI reflects connection_mode + plan.
|
||||
await dispatch(fetchSettings());
|
||||
return (await res.json()) as { ok: boolean; plan: string };
|
||||
}
|
||||
);
|
||||
|
||||
// POST /api/subscription/disconnect — clears bearer + reverts to own_key.
|
||||
// Doesn't cancel the Stripe subscription (that's the Portal).
|
||||
export const disconnectSubscription = createAsyncThunk(
|
||||
'settings/disconnectSubscription',
|
||||
async (_: void, { dispatch }) => {
|
||||
const res = await fetch(`${API_BASE}/subscription/disconnect`, { method: 'POST' });
|
||||
if (!res.ok) throw new Error('Disconnect failed');
|
||||
await dispatch(fetchSettings());
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
const settingsSlice = createSlice({
|
||||
name: 'settings',
|
||||
initialState,
|
||||
|
||||
Reference in New Issue
Block a user