Desktop app shell (apps/desktop): Tauri 2 + React 19 around the storebound sidecar
- Rust host: spawns storebound serve --stdio with fixed args, hands over a per-session secret, mints confirm tokens only after a native dialog showing the summary and the dry run - every app command re-checks window label and page origin; one capability (main window, app commands + event listen only, no plugin API in the webview); strict CSP; tests/security.rs pins it - store keys picked in native dialogs, kept in the OS keychain, passed per call in memory only - plugins: shell (Rust only), single-instance, deep-link storebound://, window-state, opener, dialog, notification, updater (placeholder endpoint and key until signing), process - app-centric UI: Portfolio, App (Releases, Catalog, Listings, Health, Reviews, TestFlight), App folders, Accounts, Dependencies, Settings; light / dark with the storebound.dev tokens, reduced motion, home folder shown as ~ - macOS menu: Cmd+Q quits, Edit menu, Window menu keeps the main window; Linux DMABUF fix - Bun workspace (npm files unchanged), ci runs desktop typecheck, build, cargo test and clippy; scrub covers home paths; docs/desktop.md (protocol, safety, dependency sources and licenses)
This commit is contained in:
parent
ab13d95898
commit
61a1e832a0
60 changed files with 9355 additions and 7 deletions
88
apps/desktop/src/screens/Accounts.tsx
Normal file
88
apps/desktop/src/screens/Accounts.tsx
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import { useState } from 'react';
|
||||
import { type Account, api, errorText } from '../api';
|
||||
import type { Data } from '../App';
|
||||
|
||||
const KIND: Record<Account['kind'], string> = { play: 'Google Play', apple: 'App Store Connect' };
|
||||
|
||||
export function Accounts({ data }: { data: Data }) {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [saved, setSaved] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState<string | null>(null);
|
||||
const [playLabel, setPlayLabel] = useState('');
|
||||
const [appleLabel, setAppleLabel] = useState('');
|
||||
const [keyId, setKeyId] = useState('');
|
||||
const [issuerId, setIssuerId] = useState('');
|
||||
|
||||
const act = async (key: string, fn: () => Promise<Account | null | void>) => {
|
||||
setBusy(key); setError(null); setSaved(null);
|
||||
try {
|
||||
const r = await fn();
|
||||
if (r) setSaved(`Saved "${r.label}" in the system keychain.`);
|
||||
await data.reload();
|
||||
} catch (e) { setError(errorText(e)); } finally { setBusy(null); }
|
||||
};
|
||||
const addPlay = () => act('play', async () => { const a = await api.addPlayAccount(playLabel); if (a) setPlayLabel(''); return a; });
|
||||
const addApple = () => act('apple', async () => { const a = await api.addAppleAccount(appleLabel, keyId, issuerId); if (a) { setAppleLabel(''); setKeyId(''); setIssuerId(''); } return a; });
|
||||
const used = (a: Account) => data.projects.filter(p => p.playAccount === a.id || p.appleAccount === a.id).map(p => p.name);
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="head">
|
||||
<div>
|
||||
<h1>Accounts</h1>
|
||||
<p>Store API keys live in the system keychain (Keychain on macOS, Credential Manager on Windows, Secret Service on Linux). They are never shown again, never written to your app folders and only handed to storebound in memory for a call.</p>
|
||||
</div>
|
||||
</header>
|
||||
<div className="stack">
|
||||
{error && <div className="notice notice-bad" role="alert">{error}</div>}
|
||||
{saved && <div className="notice notice-ok" role="status">{saved}</div>}
|
||||
<div className="card">
|
||||
{!data.accounts.length ? (
|
||||
<div className="empty"><h2>No accounts</h2><p>Add a Google Play service account or an App Store Connect API key below.</p></div>
|
||||
) : (
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>Store</th><th>Used by</th><th>Added</th><th><span className="sr">Actions</span></th></tr></thead>
|
||||
<tbody>
|
||||
{data.accounts.map(a => (
|
||||
<tr key={a.id}>
|
||||
<td className="name">{a.label}</td>
|
||||
<td><span className={`tag ${a.kind === 'play' ? 'tag-ok' : 'tag-local'}`}>{KIND[a.kind]}</span></td>
|
||||
<td>{used(a).join(', ') || <span className="tag tag-muted">no project</span>}</td>
|
||||
<td>{a.created.slice(0, 10)}</td>
|
||||
<td style={{ textAlign: 'right' }}><button className="btn btn-quiet" disabled={!!busy} onClick={() => act(`rm-${a.id}`, () => api.removeAccount(a.id))} aria-label={`Remove ${a.label} and delete its key from the keychain`}>Remove</button></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid2">
|
||||
<form className="card card-pad form" onSubmit={e => { e.preventDefault(); addPlay(); }}>
|
||||
<div><h2>Google Play</h2><p className="sub">Service account JSON key with access to your apps in Play Console.</p></div>
|
||||
<div className="field">
|
||||
<label htmlFor="play-label">Name</label>
|
||||
<input id="play-label" type="text" value={playLabel} onChange={e => setPlayLabel(e.target.value)} placeholder="Studio Play account" maxLength={60} autoComplete="off" />
|
||||
</div>
|
||||
<div><button className="btn btn-primary" type="submit" disabled={!playLabel.trim() || !!busy}>Choose JSON key</button></div>
|
||||
</form>
|
||||
<form className="card card-pad form" onSubmit={e => { e.preventDefault(); addApple(); }}>
|
||||
<div><h2>App Store Connect</h2><p className="sub">Team API key (.p8) from Users and Access, Integrations.</p></div>
|
||||
<div className="field">
|
||||
<label htmlFor="apple-label">Name</label>
|
||||
<input id="apple-label" type="text" value={appleLabel} onChange={e => setAppleLabel(e.target.value)} placeholder="Studio App Store key" maxLength={60} autoComplete="off" />
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="key-id">Key ID</label>
|
||||
<input id="key-id" type="text" value={keyId} onChange={e => setKeyId(e.target.value.toUpperCase())} placeholder="10 letters or digits" maxLength={10} autoComplete="off" spellCheck={false} />
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="issuer-id">Issuer ID</label>
|
||||
<input id="issuer-id" type="text" value={issuerId} onChange={e => setIssuerId(e.target.value)} placeholder="UUID shown above the keys" maxLength={36} autoComplete="off" spellCheck={false} />
|
||||
</div>
|
||||
<div><button className="btn btn-primary" type="submit" disabled={!appleLabel.trim() || keyId.length !== 10 || issuerId.length < 36 || !!busy}>Choose .p8 key</button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue