mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-19 18:35:18 +02:00
Resolve stash merge conflicts: keep both inline-task and billing features
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e27a93d051
commit
27e8fe0f22
6 changed files with 117 additions and 0 deletions
|
|
@ -40,6 +40,7 @@ import { triggerRun as triggerAgentScheduleRun } from '@x/core/dist/agent-schedu
|
||||||
import { search } from '@x/core/dist/search/search.js';
|
import { search } from '@x/core/dist/search/search.js';
|
||||||
import { versionHistory, voice } from '@x/core';
|
import { versionHistory, voice } from '@x/core';
|
||||||
import { classifySchedule } from '@x/core/dist/knowledge/inline_tasks.js';
|
import { classifySchedule } from '@x/core/dist/knowledge/inline_tasks.js';
|
||||||
|
import { getBillingInfo } from '@x/core/dist/billing/billing.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert markdown to a styled HTML document for PDF/DOCX export.
|
* Convert markdown to a styled HTML document for PDF/DOCX export.
|
||||||
|
|
@ -710,5 +711,9 @@ export function setupIpcHandlers() {
|
||||||
'voice:getDeepgramToken': async () => {
|
'voice:getDeepgramToken': async () => {
|
||||||
return voice.getDeepgramToken();
|
return voice.getDeepgramToken();
|
||||||
},
|
},
|
||||||
|
// Billing handler
|
||||||
|
'billing:getInfo': async () => {
|
||||||
|
return await getBillingInfo();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,7 @@ import { ConnectorsPopover } from "@/components/connectors-popover"
|
||||||
import { HelpPopover } from "@/components/help-popover"
|
import { HelpPopover } from "@/components/help-popover"
|
||||||
import { SettingsDialog } from "@/components/settings-dialog"
|
import { SettingsDialog } from "@/components/settings-dialog"
|
||||||
import { toast } from "@/lib/toast"
|
import { toast } from "@/lib/toast"
|
||||||
|
import { useBilling } from "@/hooks/useBilling"
|
||||||
import { ServiceEvent } from "@x/shared/src/service-events.js"
|
import { ServiceEvent } from "@x/shared/src/service-events.js"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
|
|
||||||
|
|
@ -401,6 +402,8 @@ export function SidebarContentPanel({
|
||||||
const [connectorsOpen, setConnectorsOpen] = useState(false)
|
const [connectorsOpen, setConnectorsOpen] = useState(false)
|
||||||
const [openConnectorsAfterClose, setOpenConnectorsAfterClose] = useState(false)
|
const [openConnectorsAfterClose, setOpenConnectorsAfterClose] = useState(false)
|
||||||
const connectorsButtonRef = useRef<HTMLButtonElement | null>(null)
|
const connectorsButtonRef = useRef<HTMLButtonElement | null>(null)
|
||||||
|
const [isRowboatConnected, setIsRowboatConnected] = useState(false)
|
||||||
|
const { billing } = useBilling(isRowboatConnected)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let mounted = true
|
let mounted = true
|
||||||
|
|
@ -412,6 +415,7 @@ export function SidebarContentPanel({
|
||||||
const hasError = Object.values(config).some((entry) => Boolean(entry?.error))
|
const hasError = Object.values(config).some((entry) => Boolean(entry?.error))
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setHasOauthError(hasError)
|
setHasOauthError(hasError)
|
||||||
|
setIsRowboatConnected(config['rowboat']?.connected ?? false)
|
||||||
if (!hasError) {
|
if (!hasError) {
|
||||||
setShowOauthAlert(true)
|
setShowOauthAlert(true)
|
||||||
}
|
}
|
||||||
|
|
@ -420,6 +424,7 @@ export function SidebarContentPanel({
|
||||||
console.error('Failed to fetch OAuth state:', error)
|
console.error('Failed to fetch OAuth state:', error)
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setHasOauthError(false)
|
setHasOauthError(false)
|
||||||
|
setIsRowboatConnected(false)
|
||||||
setShowOauthAlert(true)
|
setShowOauthAlert(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -483,6 +488,24 @@ export function SidebarContentPanel({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
|
{/* Billing status */}
|
||||||
|
{isRowboatConnected && billing && (
|
||||||
|
<div className="px-3 py-2">
|
||||||
|
<div className="flex items-center justify-between rounded-md border border-sidebar-border bg-sidebar-accent/30 px-2.5 py-1.5">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-xs font-medium capitalize text-sidebar-foreground">
|
||||||
|
{billing.subscriptionPlan ?? 'Free'}
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] text-sidebar-foreground/50">
|
||||||
|
8 days left
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button className="rounded-md bg-sidebar-accent px-2.5 py-1 text-[10px] font-medium text-sidebar-foreground hover:bg-sidebar-accent/80 transition-colors">
|
||||||
|
Upgrade
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* Bottom actions */}
|
{/* Bottom actions */}
|
||||||
<div className="border-t border-sidebar-border px-2 py-2">
|
<div className="border-t border-sidebar-border px-2 py-2">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
|
|
|
||||||
37
apps/x/apps/renderer/src/hooks/useBilling.ts
Normal file
37
apps/x/apps/renderer/src/hooks/useBilling.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
|
|
||||||
|
interface BillingInfo {
|
||||||
|
subscriptionPlan: string
|
||||||
|
subscriptionStatus: string
|
||||||
|
trialUsed: boolean
|
||||||
|
sanctionedCredits: number
|
||||||
|
availableCredits: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useBilling(isRowboatConnected: boolean) {
|
||||||
|
const [billing, setBilling] = useState<BillingInfo | null>(null)
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
const fetchBilling = useCallback(async () => {
|
||||||
|
if (!isRowboatConnected) {
|
||||||
|
setBilling(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
const result = await window.ipc.invoke('billing:getInfo', null)
|
||||||
|
setBilling(result)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch billing info:', error)
|
||||||
|
setBilling(null)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}, [isRowboatConnected])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchBilling()
|
||||||
|
}, [fetchBilling])
|
||||||
|
|
||||||
|
return { billing, isLoading, refresh: fetchBilling }
|
||||||
|
}
|
||||||
38
apps/x/packages/core/src/billing/billing.ts
Normal file
38
apps/x/packages/core/src/billing/billing.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { getAccessToken } from '../models/gateway.js';
|
||||||
|
import { ROWBOAT_BILLING_BASE_URL } from '../config/env.js';
|
||||||
|
|
||||||
|
export interface BillingInfo {
|
||||||
|
subscriptionPlan: string | null;
|
||||||
|
subscriptionStatus: string | null;
|
||||||
|
trialUsed: boolean;
|
||||||
|
sanctionedCredits: number;
|
||||||
|
availableCredits: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getBillingInfo(): Promise<BillingInfo> {
|
||||||
|
const accessToken = await getAccessToken();
|
||||||
|
const response = await fetch(`${ROWBOAT_BILLING_BASE_URL}/me`, {
|
||||||
|
headers: { Authorization: `Bearer ${accessToken}` },
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Billing API failed: ${response.status}`);
|
||||||
|
}
|
||||||
|
const body = await response.json() as {
|
||||||
|
customer: {
|
||||||
|
subscriptionPlan: string | null;
|
||||||
|
subscriptionStatus: string | null;
|
||||||
|
trialUsed: boolean;
|
||||||
|
};
|
||||||
|
usage: {
|
||||||
|
sanctionedCredits: number;
|
||||||
|
availableCredits: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
subscriptionPlan: body.customer.subscriptionPlan,
|
||||||
|
subscriptionStatus: body.customer.subscriptionStatus,
|
||||||
|
trialUsed: body.customer.trialUsed,
|
||||||
|
sanctionedCredits: body.usage.sanctionedCredits,
|
||||||
|
availableCredits: body.usage.availableCredits,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -3,3 +3,6 @@ export const API_URL =
|
||||||
|
|
||||||
export const SUPABASE_PROJECT_URL =
|
export const SUPABASE_PROJECT_URL =
|
||||||
process.env.SUPABASE_PROJECT_URL || 'http://127.0.0.1:54321';
|
process.env.SUPABASE_PROJECT_URL || 'http://127.0.0.1:54321';
|
||||||
|
|
||||||
|
export const ROWBOAT_BILLING_BASE_URL =
|
||||||
|
process.env.ROWBOAT_BILLING_BASE_URL || 'https://billing.staging.x.rowboatlabs.com';
|
||||||
|
|
|
||||||
|
|
@ -516,6 +516,17 @@ const ipcSchemas = {
|
||||||
]).nullable(),
|
]).nullable(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
// Billing channels
|
||||||
|
'billing:getInfo': {
|
||||||
|
req: z.null(),
|
||||||
|
res: z.object({
|
||||||
|
subscriptionPlan: z.string().nullable(),
|
||||||
|
subscriptionStatus: z.string().nullable(),
|
||||||
|
trialUsed: z.boolean(),
|
||||||
|
sanctionedCredits: z.number(),
|
||||||
|
availableCredits: z.number(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue