Integration Settings
A list of third-party service cards with connection status, sync health, and an inline auth flow. Composes Card, Badge, and Form components. Reference: Warren's Slack, Teams, and Harvest settings views.
Demo
Post status updates and notifications to channels.
Sync tracked time entries and billable hours.
Receive daily summaries in Microsoft Teams.
Export completed tasks to a Notion database.
Card anatomy
| Element | Description | Token |
|---|---|---|
| Service logo | 40×40 rounded rect, brand logo image or initial fallback | --surface-raised |
| Name | 14 px semibold | — |
| Status badge | Connected = success green; Disconnected = neutral gray | --status-success, --border |
| Description | 12 px, secondary colour | --text-secondary |
| Sync row | Timestamp or error message with icon, 11 px | --text-tertiary / --status-danger |
| Connected tint | Card background gets --status-success / 0.04 fill | --status-success |
Auth flow
Tapping Connect opens either:
- An inline expanded section with an API key input + “Test & Connect” button (for API-key integrations)
- A bottom sheet or modal with OAuth instructions (for OAuth integrations)
The “Test & Connect” button shows a loading spinner during the API check and surfaces error messages inline. On success, the card transitions to the Connected state with an animation.
Sync status indicators
| State | Icon | Colour |
|---|---|---|
| Sync OK | checkmark.circle.fill | --status-success |
| Sync error | exclamationmark.triangle.fill | --status-danger |
| Syncing now | Spinner | --text-tertiary |
Multi-provider timesheet sync
Warren extends the basic integration-card pattern with a multi-provider timesheet sync engine. Each provider— Clockify, Toggl Track, and Timely—conforms to the same TimesheetProvider protocol and is orchestrated by a shared TimesheetSyncEngine singleton.
| Provider | Auth | API endpoint |
|---|---|---|
| Clockify | API key (Header X-Api-Key) | api.clockify.me/v1 |
| Toggl Track | API token (HTTP Basic) | api.track.toggl.com/api/v9 |
| Timely | OAuth 2.0 Bearer token + Account ID | api.timelyapp.com/1.1 |
Protocol
protocol TimesheetProvider: Sendable {
var providerType: ProviderType { get }
func testConnection() async throws
func fetchProjects() async throws -> [LocalProject]
func fetchUsers() async throws -> [LocalUser]
func fetchTimeEntries(since: Date) async throws -> [LocalTimeEntry]
}Sync flow
- Delete window — existing provider-prefixed logs inside the lookback window are removed, clearing stale data before reinsertion.
- Fetch —
fetchTimeEntries(since:)returns entries modified in the last 30 days. - Map —
TimesheetMappingStoretranslates external project/user IDs to Warren IDs. Unmapped entries are skipped and surfaced to the user. - Insert — matched entries are written as
TimeLogobjects with a provider prefix in the description (e.g.[clockify]).
Settings UI
TimesheetSettingsView wraps all four providers (Harvest + the three new ones) under a segmented picker. Each pane shares the same three-section layout: Credentials → Member Mapping → Project Mapping → Sync. Credentials are stored in Keychain; project and member mappings are persisted in TimesheetMappingStore.
// Call from the background to sync all configured providers await TimesheetSyncEngine.shared.syncAll(dataStore: dataStore, lookbackDays: 30)
Implementation
import { useState } from "react";
interface Integration {
id: string;
name: string;
description: string;
status: "connected" | "disconnected";
lastSync?: string | null;
syncError?: string | null;
}
export function IntegrationCard({ integration }: { integration: Integration }) {
const [showAuth, setShowAuth] = useState(false);
const isConnected = integration.status === "connected";
return (
<div
style={{
padding: "16px 20px",
borderRadius: 12,
background: isConnected
? "rgb(var(--status-success) / 0.04)"
: "rgb(var(--surface))",
border: "1px solid rgb(var(--border))",
boxShadow: "var(--shadow-card)",
display: "flex",
alignItems: "flex-start",
gap: 14,
}}
>
{/* Service logo placeholder */}
<div
style={{
width: 40,
height: 40,
borderRadius: 10,
background: "rgb(var(--surface-raised))",
border: "1px solid rgb(var(--border))",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: 18,
flexShrink: 0,
}}
>
{integration.name[0]}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 4 }}>
<span style={{ fontSize: 14, fontWeight: 600 }}>{integration.name}</span>
{/* Status badge */}
<span style={{
fontSize: 11, fontWeight: 600, padding: "2px 8px", borderRadius: 99,
background: isConnected ? "rgb(var(--status-success) / 0.12)" : "rgb(var(--border))",
color: isConnected ? "rgb(var(--status-success))" : "rgb(var(--text-tertiary))",
}}>
{isConnected ? "Connected" : "Disconnected"}
</span>
</div>
<p style={{ fontSize: 12, color: "rgb(var(--text-secondary))", marginBottom: 8 }}>
{integration.description}
</p>
{/* Sync status */}
{isConnected && (
<div style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 11, color: "rgb(var(--text-tertiary))" }}>
{integration.syncError ? (
<>
<AlertCircle size={12} color="rgb(var(--status-danger))" />
<span style={{ color: "rgb(var(--status-danger))" }}>{integration.syncError}</span>
</>
) : (
<>
<CheckCircle2 size={12} color="rgb(var(--status-success))" />
<span>Last synced {integration.lastSync}</span>
</>
)}
</div>
)}
{/* Auth flow */}
{showAuth && !isConnected && (
<div style={{
marginTop: 12,
padding: 14,
borderRadius: 8,
background: "rgb(var(--surface-raised))",
border: "1px solid rgb(var(--border))",
fontSize: 13,
}}>
<p style={{ marginBottom: 10, fontWeight: 500 }}>Connect {integration.name}</p>
<input
type="text"
placeholder="Paste your API key or OAuth token"
style={{
width: "100%",
padding: "8px 12px",
borderRadius: 7,
border: "1px solid rgb(var(--border))",
background: "rgb(var(--surface))",
fontSize: 13,
boxSizing: "border-box",
}}
/>
<div style={{ display: "flex", gap: 8, marginTop: 10 }}>
<button style={{ padding: "7px 14px", borderRadius: 7, background: "rgb(var(--accent))", color: "#fff", border: "none", cursor: "pointer", fontSize: 13, fontWeight: 600 }}>
Test & Connect
</button>
<button onClick={() => setShowAuth(false)} style={{ padding: "7px 14px", borderRadius: 7, background: "transparent", border: "1px solid rgb(var(--border))", cursor: "pointer", fontSize: 13 }}>
Cancel
</button>
</div>
</div>
)}
</div>
{/* Action buttons */}
<div style={{ display: "flex", gap: 6, flexShrink: 0 }}>
{isConnected ? (
<>
<button title="Sync now" style={{ padding: 8, borderRadius: 7, border: "1px solid rgb(var(--border))", background: "rgb(var(--surface-raised))", cursor: "pointer" }}>
<RotateCcw size={14} color="rgb(var(--text-secondary))" />
</button>
<button title="Configure" style={{ padding: 8, borderRadius: 7, border: "1px solid rgb(var(--border))", background: "rgb(var(--surface-raised))", cursor: "pointer" }}>
<Settings size={14} color="rgb(var(--text-secondary))" />
</button>
<button title="Disconnect" style={{ padding: 8, borderRadius: 7, border: "1px solid rgb(var(--border))", background: "rgb(var(--surface-raised))", cursor: "pointer" }}>
<Unplug size={14} color="rgb(var(--status-danger))" />
</button>
</>
) : (
<button
onClick={() => setShowAuth(true)}
style={{ display: "flex", alignItems: "center", gap: 6, padding: "7px 14px", borderRadius: 7, background: "rgb(var(--accent))", color: "#fff", border: "none", cursor: "pointer", fontSize: 13, fontWeight: 600 }}
>
<Plug size={13} />
Connect
</button>
)}
</div>
</div>
);
}