@hanzo/build 0.2.80 → 0.2.82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/lib/activity.d.ts +3 -7
- package/lib/activity.js +18 -5
- package/lib/api/agents.d.ts +1 -20
- package/lib/api/agents.js +16 -8
- package/lib/api/auto.d.ts +3 -23
- package/lib/api/auto.js +39 -22
- package/lib/api/billing.d.ts +0 -35
- package/lib/api/billing.js +6 -5
- package/lib/api/call.d.ts +16 -8
- package/lib/api/call.js +32 -1
- package/lib/api/capabilities.d.ts +0 -11
- package/lib/api/capabilities.js +2 -1
- package/lib/api/codebases.d.ts +0 -10
- package/lib/api/codebases.js +3 -2
- package/lib/api/coding.d.ts +3 -32
- package/lib/api/coding.js +13 -7
- package/lib/api/connectors.d.ts +1 -17
- package/lib/api/connectors.js +5 -4
- package/lib/api/environment.d.ts +6 -20
- package/lib/api/environment.js +22 -19
- package/lib/api/github.d.ts +0 -22
- package/lib/api/github.js +3 -2
- package/lib/api/keys.d.ts +0 -14
- package/lib/api/keys.js +4 -3
- package/lib/api/library.d.ts +3 -17
- package/lib/api/library.js +23 -22
- package/lib/api/links.d.ts +1 -1
- package/lib/api/links.js +1 -1
- package/lib/api/machines.d.ts +0 -15
- package/lib/api/machines.js +6 -5
- package/lib/api/mcp.d.ts +0 -9
- package/lib/api/mcp.js +3 -2
- package/lib/api/members.d.ts +0 -15
- package/lib/api/members.js +4 -3
- package/lib/api/memory.d.ts +0 -11
- package/lib/api/memory.js +2 -1
- package/lib/api/places.d.ts +1 -9
- package/lib/api/places.js +12 -1
- package/lib/api/plugins.d.ts +1 -16
- package/lib/api/plugins.js +4 -3
- package/lib/api/profile.d.ts +0 -10
- package/lib/api/profile.js +5 -4
- package/lib/api/projects.d.ts +0 -14
- package/lib/api/projects.js +11 -10
- package/lib/api/provider.d.ts +2 -14
- package/lib/api/provider.js +12 -10
- package/lib/api/sandbox.d.ts +0 -17
- package/lib/api/sandbox.js +5 -4
- package/lib/api/sessions.d.ts +0 -17
- package/lib/api/sessions.js +5 -4
- package/lib/api/skills.d.ts +1 -15
- package/lib/api/skills.js +5 -4
- package/lib/api/studio.d.ts +0 -14
- package/lib/api/studio.js +6 -5
- package/lib/api/turn.d.ts +6 -28
- package/lib/api/turn.js +105 -49
- package/lib/api/verdict.d.ts +0 -9
- package/lib/api/verdict.js +2 -1
- package/lib/api/webhooks.d.ts +1 -14
- package/lib/api/webhooks.js +5 -4
- package/lib/ask.js +7 -4
- package/lib/automations.d.ts +5 -4
- package/lib/automations.js +95 -74
- package/lib/back.js +10 -4
- package/lib/builder.js +7 -5
- package/lib/carry.js +20 -16
- package/lib/customize/agents.js +78 -31
- package/lib/customize/connectors.js +62 -48
- package/lib/customize/index.js +43 -10
- package/lib/customize/plugins.js +34 -22
- package/lib/customize/say.d.ts +13 -1
- package/lib/customize/say.js +15 -21
- package/lib/customize/skills.js +31 -22
- package/lib/customize/ui.d.ts +5 -9
- package/lib/customize/ui.js +26 -16
- package/lib/data.d.ts +1 -1
- package/lib/data.js +3 -2
- package/lib/desk.js +45 -33
- package/lib/door.js +35 -10
- package/lib/environment.js +25 -19
- package/lib/files.js +43 -34
- package/lib/find.js +12 -9
- package/lib/foot.js +7 -4
- package/lib/forge.js +61 -40
- package/lib/gear.d.ts +2 -1
- package/lib/gear.js +6 -4
- package/lib/git.js +29 -19
- package/lib/landing.d.ts +5 -5
- package/lib/landing.js +52 -44
- package/lib/library.js +73 -51
- package/lib/merge.d.ts +2 -2
- package/lib/meter.d.ts +1 -1
- package/lib/meter.js +14 -5
- package/lib/mic.js +12 -5
- package/lib/mind.d.ts +1 -0
- package/lib/mind.js +5 -3
- package/lib/nav.d.ts +6 -5
- package/lib/nav.js +37 -30
- package/lib/panel.d.ts +2 -1
- package/lib/panel.js +52 -36
- package/lib/plan.d.ts +4 -20
- package/lib/plan.js +26 -10
- package/lib/plans.js +49 -30
- package/lib/plugins/design.js +3 -2
- package/lib/plugins/index.d.ts +23 -4
- package/lib/plugins/index.js +11 -26
- package/lib/plugins/pdf.js +2 -1
- package/lib/plugins/project.js +7 -4
- package/lib/plugins/research.js +3 -2
- package/lib/plugins/sketch.js +6 -3
- package/lib/plugins/slides.js +3 -2
- package/lib/plugins/template.js +6 -3
- package/lib/plus.js +25 -13
- package/lib/prefs.d.ts +0 -14
- package/lib/prefs.js +2 -1
- package/lib/project.js +69 -62
- package/lib/prompt.d.ts +12 -11
- package/lib/prompt.js +38 -20
- package/lib/publish.js +17 -12
- package/lib/reply.js +6 -3
- package/lib/run.js +72 -54
- package/lib/say.d.ts +2 -0
- package/lib/say.js +7 -0
- package/lib/scheduled.js +54 -43
- package/lib/section.d.ts +1 -0
- package/lib/section.js +8 -2
- package/lib/settings/account.d.ts +1 -0
- package/lib/settings/account.js +31 -21
- package/lib/settings/billing.js +55 -28
- package/lib/settings/capabilities.js +26 -15
- package/lib/settings/card.js +19 -12
- package/lib/settings/code.js +22 -11
- package/lib/settings/credits.d.ts +4 -4
- package/lib/settings/credits.js +34 -21
- package/lib/settings/environments.js +21 -14
- package/lib/settings/general.js +26 -14
- package/lib/settings/index.js +6 -3
- package/lib/settings/integrations.js +42 -28
- package/lib/settings/keys.js +37 -19
- package/lib/settings/list.d.ts +0 -5
- package/lib/settings/list.js +24 -15
- package/lib/settings/machines.d.ts +3 -0
- package/lib/settings/machines.js +63 -38
- package/lib/settings/members.js +23 -14
- package/lib/settings/memory.js +12 -6
- package/lib/settings/notifications.js +32 -19
- package/lib/settings/plan.d.ts +1 -1
- package/lib/settings/plan.js +40 -17
- package/lib/settings/privacy.js +26 -13
- package/lib/settings/ui.d.ts +3 -2
- package/lib/settings/ui.js +9 -3
- package/lib/settings/usage.js +7 -3
- package/lib/shelf.js +20 -17
- package/lib/side.d.ts +1 -26
- package/lib/side.js +11 -5
- package/lib/standing.d.ts +2 -10
- package/lib/standing.js +5 -3
- package/lib/suggest.d.ts +8 -28
- package/lib/suggest.js +39 -4
- package/lib/suggestions.js +14 -5
- package/lib/summary.d.ts +1 -1
- package/lib/summary.js +4 -1
- package/lib/sync.js +22 -12
- package/lib/tools.d.ts +4 -3
- package/lib/tools.js +10 -7
- package/lib/transcript.js +10 -8
- package/package.json +4 -2
package/lib/section.js
CHANGED
|
@@ -11,6 +11,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
11
11
|
* host's own `go`.
|
|
12
12
|
*/
|
|
13
13
|
import { SizableText } from '@hanzo/gui';
|
|
14
|
+
import { useI18n } from '@hanzo/i18n/react';
|
|
14
15
|
import { SidebarItem, SidebarSection, StatusDot } from '@hanzo/ui/chat';
|
|
15
16
|
import { useMemo } from 'react';
|
|
16
17
|
import { useRecents } from './data.js';
|
|
@@ -36,7 +37,8 @@ const Note = ({ children }) => (_jsx(SizableText, { size: "$1", color: "$soft",
|
|
|
36
37
|
* own. `onPick` fires after a move, so a host that draws its rail as a drawer
|
|
37
38
|
* on a phone can close it.
|
|
38
39
|
*/
|
|
39
|
-
export function DevSection({ host, onPick, label
|
|
40
|
+
export function DevSection({ host, onPick, label, children, }) {
|
|
41
|
+
const { t } = useI18n();
|
|
40
42
|
const sessions = useSessions(host);
|
|
41
43
|
const r = route(host.path);
|
|
42
44
|
// Every row, a place or a run, closes a drawer behind it.
|
|
@@ -44,5 +46,9 @@ export function DevSection({ host, onPick, label = 'Runs', children, }) {
|
|
|
44
46
|
press?.();
|
|
45
47
|
onPick?.();
|
|
46
48
|
};
|
|
47
|
-
return (_jsxs(_Fragment, { children: [nav(host, host.go).map((g) => (_jsx(SidebarSection, { label: g.label, children: g.links.map((l) => (_jsx(SidebarItem, { icon: l.icon, active: l.active, onPress: then(l.onPress), children: l.label }, l.id))) }, g.id))), children, _jsx(SidebarSection, { label: label, children: !host.person ? (_jsx(Note, { children:
|
|
49
|
+
return (_jsxs(_Fragment, { children: [nav(host, host.go, t).map((g) => (_jsx(SidebarSection, { label: g.label, children: g.links.map((l) => (_jsx(SidebarItem, { icon: l.icon, active: l.active, onPress: then(l.onPress), children: l.label }, l.id))) }, g.id))), children, _jsx(SidebarSection, { label: label ?? t('Runs'), children: !host.person ? (_jsx(Note, { children: t('Sign in to see your runs.') })) : sessions.value.length ? (sessions.value.map((s) => (_jsx(SidebarItem, { icon: _jsx(StatusDot, { status: DOTS[s.status] ?? 'idle' }), active: r.kind === 'run' && r.id === s.id, onPress: then(() => host.go(s.id)), children: s.title || t('Untitled run') }, s.id)))) : (_jsx(Note, { children: sessions.error
|
|
50
|
+
? t(sessions.error.message)
|
|
51
|
+
: sessions.loading
|
|
52
|
+
? t('Reading…')
|
|
53
|
+
: `${t('No runs yet.')} ${t('Say what to build and it lands here.')}` })) })] }));
|
|
48
54
|
}
|
package/lib/settings/account.js
CHANGED
|
@@ -6,33 +6,41 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
6
6
|
* to, the one this page acts in, a way to act in another, and logging out.
|
|
7
7
|
*
|
|
8
8
|
* The email is how the person signs in, so it is shown and not edited here.
|
|
9
|
+
*
|
|
10
|
+
* Its words are the reader's language (@hanzo/i18n `t`); a name, an email and
|
|
11
|
+
* an organization's id are drawn as given.
|
|
9
12
|
*/
|
|
10
13
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
14
|
+
import { phrase } from '@hanzo/i18n';
|
|
15
|
+
import { useI18n } from '@hanzo/i18n/react';
|
|
11
16
|
import { Check, Copy, LogOut } from '@hanzogui/lucide-icons-2';
|
|
12
17
|
import { Button, Input, Textarea } from '@hanzo/ui';
|
|
13
18
|
import { ChipSelect } from '@hanzo/ui/product';
|
|
14
19
|
import { useEffect, useRef, useState } from 'react';
|
|
20
|
+
import { told } from '../api/call.js';
|
|
15
21
|
import { PHOTO, photo, rename } from '../api/profile.js';
|
|
16
22
|
import { useHost, useTarget } from '../host.js';
|
|
17
23
|
import { usePrefs } from '../prefs.js';
|
|
18
24
|
import { Card, Field, Group, Heading, Note, Row, Soft } from './ui.js';
|
|
25
|
+
/** What a person may say they work on, each label English and marked (`phrase`), drawn through `t`. */
|
|
19
26
|
export const WORK = [
|
|
20
|
-
{ id: 'engineering', label: 'Software engineering' },
|
|
21
|
-
{ id: 'data', label: 'Data science' },
|
|
22
|
-
{ id: 'design', label: 'Design' },
|
|
23
|
-
{ id: 'product', label: 'Product management' },
|
|
24
|
-
{ id: 'research', label: 'Research' },
|
|
25
|
-
{ id: 'writing', label: 'Writing' },
|
|
26
|
-
{ id: 'education', label: 'Education' },
|
|
27
|
-
{ id: 'operations', label: 'Operations' },
|
|
28
|
-
{ id: 'marketing', label: 'Marketing and sales' },
|
|
29
|
-
{ id: 'other', label: 'Something else' },
|
|
27
|
+
{ id: 'engineering', label: phrase('Software engineering') },
|
|
28
|
+
{ id: 'data', label: phrase('Data science') },
|
|
29
|
+
{ id: 'design', label: phrase('Design') },
|
|
30
|
+
{ id: 'product', label: phrase('Product management') },
|
|
31
|
+
{ id: 'research', label: phrase('Research') },
|
|
32
|
+
{ id: 'writing', label: phrase('Writing') },
|
|
33
|
+
{ id: 'education', label: phrase('Education') },
|
|
34
|
+
{ id: 'operations', label: phrase('Operations') },
|
|
35
|
+
{ id: 'marketing', label: phrase('Marketing and sales') },
|
|
36
|
+
{ id: 'other', label: phrase('Something else') },
|
|
30
37
|
];
|
|
31
38
|
/** Long enough for real guidance, short enough to sit in the settings document beside everything else. */
|
|
32
39
|
const INSTRUCTIONS = 3000;
|
|
33
40
|
export function Account() {
|
|
41
|
+
const { t } = useI18n();
|
|
34
42
|
const host = useHost();
|
|
35
|
-
const
|
|
43
|
+
const target = useTarget();
|
|
36
44
|
const { prefs, save } = usePrefs();
|
|
37
45
|
const who = host.person;
|
|
38
46
|
const [name, setName] = useState(who?.name ?? '');
|
|
@@ -57,7 +65,7 @@ export function Account() {
|
|
|
57
65
|
setInstructions(prefs.instructions ?? '');
|
|
58
66
|
}, [prefs.callName, prefs.work, prefs.instructions]);
|
|
59
67
|
if (!who) {
|
|
60
|
-
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title:
|
|
68
|
+
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: t('Account') }), _jsx(Soft, { children: t('Sign in to see your account.') }), host.signIn ? (_jsx(XStack, { justify: "center", children: _jsx(Button, { size: "sm", onPress: () => host.signIn?.(), children: t('Sign in') }) })) : null] }));
|
|
61
69
|
}
|
|
62
70
|
const patch = {};
|
|
63
71
|
if (callName.trim() !== (prefs.callName ?? ''))
|
|
@@ -74,13 +82,13 @@ export function Account() {
|
|
|
74
82
|
setNote('');
|
|
75
83
|
try {
|
|
76
84
|
if (renamed)
|
|
77
|
-
setShownName((await rename(
|
|
85
|
+
setShownName((await rename(target, name)).displayName || name.trim());
|
|
78
86
|
if (Object.keys(patch).length)
|
|
79
87
|
await save(patch);
|
|
80
|
-
setNote('Saved');
|
|
88
|
+
setNote(t('Saved'));
|
|
81
89
|
}
|
|
82
90
|
catch (e) {
|
|
83
|
-
setNote(e
|
|
91
|
+
setNote(told(e, t));
|
|
84
92
|
}
|
|
85
93
|
finally {
|
|
86
94
|
setBusy(false);
|
|
@@ -90,11 +98,11 @@ export function Account() {
|
|
|
90
98
|
setBusy(true);
|
|
91
99
|
setNote('');
|
|
92
100
|
try {
|
|
93
|
-
setFace(await photo(
|
|
94
|
-
setNote('Your photo is saved');
|
|
101
|
+
setFace(await photo(target, file));
|
|
102
|
+
setNote(t('Your photo is saved'));
|
|
95
103
|
}
|
|
96
104
|
catch (e) {
|
|
97
|
-
setNote(e
|
|
105
|
+
setNote(told(e, t));
|
|
98
106
|
}
|
|
99
107
|
finally {
|
|
100
108
|
setBusy(false);
|
|
@@ -102,12 +110,14 @@ export function Account() {
|
|
|
102
110
|
};
|
|
103
111
|
const org = host.org;
|
|
104
112
|
const orgs = host.memberships ?? (org ? [org] : []);
|
|
105
|
-
|
|
106
|
-
|
|
113
|
+
// The list's words in the reader's language; a work the list does not hold is the person's own, as given.
|
|
114
|
+
const works = WORK.map((w) => ({ ...w, label: t(w.label) }));
|
|
115
|
+
const chosenWork = works.find((w) => w.id === work) ?? (work ? { id: work, label: work } : null);
|
|
116
|
+
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: t('Account'), detail: t('Your Hanzo identity, and what Hanzo should know about you.'), action: host.signOut ? (_jsxs(Button, { size: "sm", variant: "outline", onPress: () => host.signOut?.(), children: [_jsx(LogOut, { size: 14 }), " ", t('Log out')] })) : undefined }), _jsx(Group, { title: t('Profile'), children: _jsxs(YStack, { gap: "$4", children: [_jsxs(XStack, { items: "center", gap: "$3", children: [face ? (_jsx("img", { src: face, alt: "", width: 48, height: 48, style: { borderRadius: 999, objectFit: 'cover' } })) : (_jsx(YStack, { width: 48, height: 48, rounded: 999, bg: "$raised", items: "center", justify: "center", children: _jsx(SizableText, { size: "$5", color: "$ink", children: (shownName || who.email || '?').charAt(0).toUpperCase() }) })), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(SizableText, { size: "$4", color: "$ink", numberOfLines: 1, children: shownName || who.email }), _jsx(SizableText, { size: "$2", color: "$soft", numberOfLines: 1, children: who.email })] }), _jsx(Button, { size: "sm", variant: "outline", disabled: busy, onPress: () => picker.current?.click(), children: t('Change photo') }), _jsx("input", { ref: picker, type: "file", accept: PHOTO, hidden: true, style: { display: 'none' }, tabIndex: -1, "aria-label": t('Photo'), onChange: (e) => {
|
|
107
117
|
const input = e.currentTarget;
|
|
108
118
|
const file = input.files?.[0];
|
|
109
119
|
input.value = '';
|
|
110
120
|
if (file)
|
|
111
121
|
void upload(file);
|
|
112
|
-
} })] }), _jsx(Field, { label:
|
|
122
|
+
} })] }), _jsx(Field, { label: t('Full name'), children: _jsx(Input, { value: name, onChangeText: setName, "aria-label": t('Full name'), placeholder: t('Your name') }) }), _jsx(Field, { label: t('Email'), hint: t('You sign in with this address, so it is not changed here.'), children: _jsx(SizableText, { size: "$2", color: "$soft", numberOfLines: 1, children: who.email || t('No email on this account') }) }), _jsx(Field, { label: t('What should Hanzo call you?'), children: _jsx(Input, { value: callName, onChangeText: setCallName, "aria-label": t('What should Hanzo call you?'), placeholder: shownName.split(' ')[0] || t('A first name') }) }), _jsx(Field, { label: t('What best describes your work?'), children: _jsx(XStack, { children: _jsx(ChipSelect, { name: t('Work'), label: chosenWork?.label ?? t('Choose one'), chosen: chosenWork, items: works, onChange: (w) => setWork(w.id), placement: "bottom-start", width: 260 }) }) }), _jsx(Field, { label: t('Instructions for Hanzo'), hint: t('Every coding run reads these before it starts. {used}/{most}', { used: instructions.length, most: INSTRUCTIONS }), children: _jsx(Textarea, { value: instructions, onChangeText: (v) => setInstructions(v.slice(0, INSTRUCTIONS)), "aria-label": t('Instructions for Hanzo'), placeholder: t('How you like to work: the languages and tools you prefer, the style to follow, what to avoid.'), rows: 5 }) }), _jsxs(XStack, { items: "center", gap: "$3", children: [_jsx(Button, { size: "sm", disabled: busy || !changed, onPress: () => void submit(), children: t('Save') }), _jsx(Note, { children: note })] })] }) }), org ? (_jsx(Group, { title: t('Organization'), detail: t('The organization this page acts in. Runs, codebases and settings belong to it.'), children: _jsx(Card, { children: _jsx(Row, { first: true, mono: true, title: org, detail: host.admin ? t('Organization ID · you are an admin') : t('Organization ID'), trailing: _jsx(Button, { size: "icon-sm", variant: "ghost", "aria-label": copied ? t('Organization ID copied') : t('Copy organization ID'), onPress: () => void navigator.clipboard?.writeText(org).then(() => setCopied(true)), children: copied ? _jsx(Check, { size: 14 }) : _jsx(Copy, { size: 14 }) }) }) }) })) : null, _jsx(Group, { title: t('Organizations'), detail: t('Every organization you belong to. Switch to act in another.'), children: orgs.length ? (_jsx(Card, { children: orgs.map((o, i) => (_jsx(Row, { first: i === 0, title: o, detail: o === host.org ? (host.admin ? t('Acting in {org} as an admin', { org: o }) : t('Acting in {org}', { org: o })) : undefined, trailing: o === host.org ? (_jsx(Check, { size: 16 })) : host.chooseOrg ? (_jsx(Button, { size: "sm", variant: "ghost", onPress: () => host.chooseOrg?.(o), children: t('Switch') })) : null }, o))) })) : (_jsx(Soft, { children: t('You belong to no organization yet.') })) })] }));
|
|
113
123
|
}
|
package/lib/settings/billing.js
CHANGED
|
@@ -4,12 +4,18 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
* credits (`Credits`: the balance, grants, top-ups, auto-reload, this month's
|
|
5
5
|
* spend and its limit), the cards it pays with, its invoices, and ending the
|
|
6
6
|
* plan. Changing plan is the Plans screen; this page says what is true now.
|
|
7
|
+
*
|
|
8
|
+
* Its words are the reader's language (@hanzo/i18n `t`); a plan's name, a
|
|
9
|
+
* card's, an invoice's lines and its status are billing's, drawn as given.
|
|
7
10
|
*/
|
|
8
11
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
12
|
+
import { phrase } from '@hanzo/i18n';
|
|
13
|
+
import { useI18n } from '@hanzo/i18n/react';
|
|
9
14
|
import { CreditCard, Download, Plus, X } from '@hanzogui/lucide-icons-2';
|
|
10
15
|
import { Button, Dialog, DialogContent, DialogTitle, Input, toast } from '@hanzo/ui';
|
|
11
16
|
import { useEffect, useState } from 'react';
|
|
12
17
|
import { cancel, chosen, current, detach, formatAddress, invoices, label, makeDefault, methods, money, pdf, reactivate, subscriptions, tier, updateMethod, } from '../api/billing.js';
|
|
18
|
+
import { told } from '../api/call.js';
|
|
13
19
|
import { useRead } from '../data.js';
|
|
14
20
|
import { useHost, useTarget } from '../host.js';
|
|
15
21
|
import { Title } from '../meter.js';
|
|
@@ -19,13 +25,14 @@ import { AddCard } from './card.js';
|
|
|
19
25
|
import { Credits } from './credits.js';
|
|
20
26
|
import { Card, day, Field, Group, Heading, Note, Row, Soft } from './ui.js';
|
|
21
27
|
export function Billing() {
|
|
28
|
+
const { t } = useI18n();
|
|
22
29
|
const host = useHost();
|
|
23
|
-
const
|
|
30
|
+
const target = useTarget();
|
|
24
31
|
const signed = Boolean(host.person);
|
|
25
|
-
const subs = useRead(signed ? () => subscriptions(
|
|
26
|
-
const cards = useRead(signed ? () => methods(
|
|
27
|
-
const bills = useRead(signed ? () => invoices(
|
|
28
|
-
const billed = useRead(signed ? () => tier(
|
|
32
|
+
const subs = useRead(signed ? () => subscriptions(target) : null, [], [target, signed]);
|
|
33
|
+
const cards = useRead(signed ? () => methods(target) : null, [], [target, signed]);
|
|
34
|
+
const bills = useRead(signed ? () => invoices(target) : null, [], [target, signed]);
|
|
35
|
+
const billed = useRead(signed ? () => tier(target) : null, null, [target, signed]);
|
|
29
36
|
const [adding, setAdding] = useState(false);
|
|
30
37
|
const [ending, setEnding] = useState(false);
|
|
31
38
|
const [editingBilling, setEditingBilling] = useState(null);
|
|
@@ -33,7 +40,7 @@ export function Billing() {
|
|
|
33
40
|
const [working, setWorking] = useState(false);
|
|
34
41
|
const [note, setNote] = useState('');
|
|
35
42
|
if (!signed) {
|
|
36
|
-
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title:
|
|
43
|
+
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: t('Billing') }), _jsx(Soft, { children: t('Sign in to see this organization’s billing.') })] }));
|
|
37
44
|
}
|
|
38
45
|
const plan = current(subs.value);
|
|
39
46
|
// The plan by its family, `Max` with `20x` beside it; the subscription names
|
|
@@ -54,32 +61,33 @@ export function Billing() {
|
|
|
54
61
|
toast.success(done);
|
|
55
62
|
}
|
|
56
63
|
catch (e) {
|
|
57
|
-
setNote(e
|
|
64
|
+
setNote(told(e, t));
|
|
58
65
|
}
|
|
59
66
|
finally {
|
|
60
67
|
setWorking(false);
|
|
61
68
|
}
|
|
62
69
|
};
|
|
63
|
-
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title:
|
|
64
|
-
m.expires ?
|
|
65
|
-
m.default ? 'Default' : '',
|
|
70
|
+
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: t('Billing'), detail: host.org ? t('The plan {org} is on, the cards it pays with, and its invoices.', { org: host.org }) : t('The plan this organization is on, the cards it pays with, and its invoices.') }), _jsxs(XStack, { items: "center", gap: "$3", px: "$4", py: "$4", borderWidth: 1, borderColor: "$borderColor", rounded: "$3", flexWrap: "wrap", children: [_jsxs(YStack, { flex: 1, minW: 200, gap: "$1", "data-slot": "billing-plan", children: [reading ? (_jsx(SizableText, { size: "$5", color: "$ink", children: t('Reading your plan…') })) : (_jsx(Title, { label: title ?? { name: phrase('Free'), tag: '' }, size: "$5" })), _jsx(SizableText, { size: "$2", color: "$soft", children: subs.error ? told(subs.error, t) : plan ? terms(plan, t) : title ? t('Your plan.') : t('No paid plan. Usage is paid from credits.') })] }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'plans' })), children: t('Adjust plan') })] }), _jsx(Credits, { target: target, admin: host.admin, onCard: () => setAdding(true) }), _jsxs(Group, { title: t('Payment'), detail: t('The card a plan renews on and a top-up charges.'), action: host.admin && (cards.error || cards.value.length > 0) ? (_jsxs(Button, { size: "sm", variant: "outline", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), " ", t('Add card')] })) : undefined, children: [cards.error ? (_jsx(Soft, { children: told(cards.error, t) })) : cards.loading && !cards.value.length ? (_jsx(Soft, { children: t('Reading cards…') })) : empty ? (_jsxs(XStack, { items: "center", gap: "$3", px: "$4", py: "$4", borderWidth: 1, borderColor: "$borderColor", rounded: "$3", flexWrap: "wrap", children: [_jsx(CreditCard, { size: 20 }), _jsxs(YStack, { flex: 1, minW: 180, gap: "$1", children: [_jsx(SizableText, { size: "$3", color: "$ink", children: t('No card on file.') }), _jsx(SizableText, { size: "$2", color: "$soft", children: host.admin ? t('Add one to pay for a plan or a top-up. Adding it charges nothing.') : t('An org admin adds cards.') })] }), host.admin ? (_jsxs(Button, { variant: "primary", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 16 }), " ", t('Add card')] })) : null] })) : (_jsx(Card, { children: cards.value.map((m, i) => (_jsx(Row, { first: i === 0, leading: _jsx(CreditCard, { size: 16 }), title: method(m, t), detail: [
|
|
71
|
+
m.expires ? t('Expires {date}', { date: m.expires }) : '',
|
|
72
|
+
m.default ? t('Default') : '',
|
|
66
73
|
m.billingAddress ? formatAddress(m.billingAddress) : '',
|
|
67
74
|
]
|
|
68
75
|
.filter(Boolean)
|
|
69
|
-
.join(' · ') || undefined, trailing: host.admin ? (_jsxs(XStack, { gap: "$2", items: "center", children: [!m.default ? (_jsx(Button, { size: "sm", variant: "ghost", "aria-label":
|
|
76
|
+
.join(' · ') || undefined, trailing: host.admin ? (_jsxs(XStack, { gap: "$2", items: "center", children: [!m.default ? (_jsx(Button, { size: "sm", variant: "ghost", "aria-label": t('Make {card} default', { card: method(m, t) }), disabled: working, onPress: () => void act(() => makeDefault(target, m.id), t('{card} is now the default', { card: method(m, t) }), cards.reload), children: t('Make default') })) : null, _jsx(Button, { size: "sm", variant: "ghost", "aria-label": t('Edit billing info for {card}', { card: method(m, t) }), disabled: working, onPress: () => setEditingBilling(m), children: t('Edit info') }), _jsx(Button, { size: "sm", variant: "ghost", "aria-label": t('Remove {card}', { card: method(m, t) }), disabled: working, onPress: () => void act(() => detach(target, m.id), t('{card} is removed', { card: method(m, t) }), cards.reload), children: t('Remove') })] })) : undefined }, m.id))) })), !host.admin && cards.value.length ? (_jsx(SizableText, { size: "$1", color: "$soft", children: t('An org admin adds and removes cards.') })) : null] }), _jsx(Group, { title: t('Billing details'), detail: t('Information printed on invoices and payment receipts.'), action: host.admin && defaultCard ? (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setEditingBilling(defaultCard), children: defaultCard.billingAddress ? t('Edit details') : t('Add billing info') })) : undefined, children: defaultCard?.billingAddress ? (_jsx(Card, { children: _jsxs(YStack, { p: "$3", gap: "$1.5", children: [defaultCard.billingAddress.name ? (_jsx(SizableText, { size: "$3", color: "$ink", style: { fontWeight: 600 }, children: defaultCard.billingAddress.name })) : null, defaultCard.billingAddress.line1 ? (_jsxs(SizableText, { size: "$2", color: "$soft", children: [defaultCard.billingAddress.line1, defaultCard.billingAddress.line2 ? `, ${defaultCard.billingAddress.line2}` : ''] })) : null, defaultCard.billingAddress.city || defaultCard.billingAddress.state || defaultCard.billingAddress.postalCode ? (_jsx(SizableText, { size: "$2", color: "$soft", children: [defaultCard.billingAddress.city, defaultCard.billingAddress.state, defaultCard.billingAddress.postalCode]
|
|
70
77
|
.filter(Boolean)
|
|
71
|
-
.join(' ') })) : null, defaultCard.billingAddress.country ? (_jsx(SizableText, { size: "$2", color: "$soft", children: defaultCard.billingAddress.country })) : null] }) })) : (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", px: "$4", py: "$3", borderWidth: 1, borderColor: "$borderColor", rounded: "$3", flexWrap: "wrap", children: [_jsxs(YStack, { flex: 1, minW: 180, gap: "$1", children: [_jsx(SizableText, { size: "$2", color: "$ink", children:
|
|
78
|
+
.join(' ') })) : null, defaultCard.billingAddress.country ? (_jsx(SizableText, { size: "$2", color: "$soft", children: defaultCard.billingAddress.country })) : null] }) })) : (_jsxs(XStack, { items: "center", justify: "space-between", gap: "$3", px: "$4", py: "$3", borderWidth: 1, borderColor: "$borderColor", rounded: "$3", flexWrap: "wrap", children: [_jsxs(YStack, { flex: 1, minW: 180, gap: "$1", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: t('No billing address on file.') }), _jsx(SizableText, { size: "$1", color: "$soft", children: t('Add your company name and address so they appear on monthly invoices and receipts.') })] }), host.admin ? (_jsxs(Button, { size: "sm", variant: "outline", onPress: () => (defaultCard ? setEditingBilling(defaultCard) : setAdding(true)), children: [_jsx(Plus, { size: 14 }), " ", defaultCard ? t('Add billing info') : t('Set billing info')] })) : null] })) }), _jsx(Group, { title: t('Invoices'), children: bills.error ? (_jsx(Soft, { children: told(bills.error, t) })) : bills.loading && !bills.value.length ? (_jsx(Soft, { children: t('Reading invoices…') })) : bills.value.length === 0 ? (_jsx(Card, { children: _jsx(Soft, { children: t('No invoices yet.') }) })) : (_jsxs(Card, { children: [_jsxs(XStack, { px: "$3", py: "$2", gap: "$3", children: [_jsx(SizableText, { flex: 1, size: "$1", color: "$soft", children: t('Date') }), _jsx(SizableText, { width: 90, size: "$1", color: "$soft", text: "end", children: t('Total') }), _jsx(SizableText, { width: 70, size: "$1", color: "$soft", "$max-sm": { display: 'none' }, children: t('Status') }), _jsx(YStack, { width: 64 })] }), bills.value.map((b) => (_jsxs(XStack, { items: "center", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { flex: 1, size: "$2", color: "$ink", numberOfLines: 1, children: day(b.date) || b.number || b.id }), _jsx(SizableText, { width: 90, size: "$2", color: "$ink", text: "end", children: money(b.total, b.currency) }), _jsx(SizableText, { width: 70, size: "$1", color: "$soft", "$max-sm": { display: 'none' }, children: word(b.status) }), _jsx(XStack, { width: 64, justify: "flex-end", children: _jsx(Button, { size: "sm", variant: "ghost", "aria-label": t('View invoice {invoice}', { invoice: b.number || day(b.date) }), onPress: () => setOpen(b), children: t('View') }) })] }, b.id)))] })) }), plan ? (_jsx(Group, { title: t('Cancellation'), children: _jsx(Card, { children: _jsx(Row, { first: true, title: plan.ending ? t('Your plan ends on {date}', { date: day(plan.ends) }) : t('Cancel plan'), detail: plan.ending ? t('Keep it, and it renews as before.') : t('It runs to the end of the period you paid for, then ends.'), trailing: !host.admin ? undefined : plan.ending ? (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void act(() => reactivate(target, plan.id), t('Your plan renews as before'), subs.reload), children: t('Keep plan') })) : (_jsx(Button, { size: "sm", variant: "destructive", disabled: working, onPress: () => setEnding(true), children: t('Cancel') })) }) }) })) : null, _jsx(Note, { children: note }), _jsx(AddCard, { open: adding, onOpenChange: setAdding, onSaved: (m) => {
|
|
72
79
|
cards.reload();
|
|
73
|
-
toast.success(
|
|
80
|
+
toast.success(t('{card} is saved', { card: method(m, t) }));
|
|
74
81
|
} }), _jsx(EditBilling, { open: editingBilling !== null, onOpenChange: (o) => !o && setEditingBilling(null), card: editingBilling, onSaved: () => {
|
|
75
82
|
cards.reload();
|
|
76
|
-
} }), plan ? (_jsx(Ending, { plan: plan, open: ending, working: working, onOpenChange: setEnding, onConfirm: () => void act(() => cancel(
|
|
83
|
+
} }), plan ? (_jsx(Ending, { plan: plan, open: ending, working: working, onOpenChange: setEnding, onConfirm: () => void act(() => cancel(target, plan.id), t('Your plan ends on {date}', { date: day(plan.ends) }), () => {
|
|
77
84
|
setEnding(false);
|
|
78
85
|
subs.reload();
|
|
79
86
|
}) })) : null, open ? _jsx(Bill, { invoice: open, onClose: () => setOpen(null) }) : null] }));
|
|
80
87
|
}
|
|
81
88
|
function EditBilling({ open, onOpenChange, card, onSaved, }) {
|
|
82
|
-
const t =
|
|
89
|
+
const { t } = useI18n();
|
|
90
|
+
const target = useTarget();
|
|
83
91
|
const [name, setName] = useState('');
|
|
84
92
|
const [line1, setLine1] = useState('');
|
|
85
93
|
const [line2, setLine2] = useState('');
|
|
@@ -125,39 +133,58 @@ function EditBilling({ open, onOpenChange, card, onSaved, }) {
|
|
|
125
133
|
postalCode: postalCode.trim() || undefined,
|
|
126
134
|
country: country.trim() || undefined,
|
|
127
135
|
};
|
|
128
|
-
await updateMethod(
|
|
136
|
+
await updateMethod(target, card.id, { billingAddress: address });
|
|
129
137
|
onSaved();
|
|
130
138
|
onOpenChange(false);
|
|
131
|
-
toast.success('Billing details updated');
|
|
139
|
+
toast.success(t('Billing details updated'));
|
|
132
140
|
}
|
|
133
141
|
catch (e) {
|
|
134
|
-
setNote(e
|
|
142
|
+
setNote(told(e, t));
|
|
135
143
|
}
|
|
136
144
|
finally {
|
|
137
145
|
setWorking(false);
|
|
138
146
|
}
|
|
139
147
|
};
|
|
140
|
-
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 460, showCloseButton: false, children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsx(DialogTitle, { children:
|
|
148
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 460, showCloseButton: false, children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsx(DialogTitle, { children: t('Billing details') }), _jsx(XStack, { render: "button", "aria-label": t('Close'), p: "$1", onPress: () => onOpenChange(false), children: _jsx(X, { size: 16 }) })] }), _jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: t('These details appear on your invoices and receipts.') }), _jsx(Field, { label: t('Business or personal name'), children: _jsx(Input, { value: name, onChangeText: setName, placeholder: t('e.g. Webby AI, Inc. or Joshua Lynch') }) }), _jsx(Field, { label: t('Address line 1'), children: _jsx(Input, { value: line1, onChangeText: setLine1, placeholder: t('Street address or P.O. Box') }) }), _jsx(Field, { label: t('Address line 2'), hint: t('Optional'), children: _jsx(Input, { value: line2, onChangeText: setLine2, placeholder: t('Suite, unit, building, floor') }) }), _jsxs(XStack, { gap: "$2", children: [_jsx(YStack, { flex: 1, children: _jsx(Field, { label: t('City'), children: _jsx(Input, { value: city, onChangeText: setCity, placeholder: t('City') }) }) }), _jsx(YStack, { width: 100, children: _jsx(Field, { label: t('State'), children: _jsx(Input, { value: stateVal, onChangeText: setStateVal, placeholder: t('State / Prov') }) }) })] }), _jsxs(XStack, { gap: "$2", children: [_jsx(YStack, { flex: 1, children: _jsx(Field, { label: t('Postal / ZIP code'), children: _jsx(Input, { value: postalCode, onChangeText: setPostalCode, placeholder: t('ZIP or Postal Code') }) }) }), _jsx(YStack, { width: 110, children: _jsx(Field, { label: t('Country'), children: _jsx(Input, { value: country, onChangeText: setCountry, placeholder: t('Country (US)') }) }) })] }), note ? (_jsx(SizableText, { size: "$2", color: "$red10", role: "alert", children: note })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", pt: "$2", children: [_jsx(Button, { size: "sm", variant: "ghost", disabled: working, onPress: () => onOpenChange(false), children: t('Cancel') }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void save(), children: t('Save details') })] })] })] }) }));
|
|
141
149
|
}
|
|
142
|
-
/** What
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
150
|
+
/** What a plan costs by its interval, alone and for its seats: one sentence each, so a language says the interval its own way. */
|
|
151
|
+
const COST = {
|
|
152
|
+
month: { one: phrase('{amount} a month.'), seats: phrase('{amount} a month for {seats} seats.') },
|
|
153
|
+
year: { one: phrase('{amount} a year.'), seats: phrase('{amount} a year for {seats} seats.') },
|
|
154
|
+
};
|
|
155
|
+
/** What the plan costs, and when it renews or ends, in the words of `t`. */
|
|
156
|
+
function terms(p, t) {
|
|
157
|
+
const amount = money(p.price * p.seats, p.currency);
|
|
158
|
+
const by = COST[p.interval];
|
|
159
|
+
const cost = by
|
|
160
|
+
? t(p.seats > 1 ? by.seats : by.one, { amount, seats: p.seats })
|
|
161
|
+
: p.seats > 1
|
|
162
|
+
? t('{amount} a {interval} for {seats} seats.', { amount, interval: p.interval, seats: p.seats })
|
|
163
|
+
: t('{amount} a {interval}.', { amount, interval: p.interval });
|
|
164
|
+
if (!p.ends)
|
|
165
|
+
return cost;
|
|
166
|
+
return `${cost} ${p.ending ? t('Ends on {date}.', { date: day(p.ends) }) : t('Renews on {date}.', { date: day(p.ends) })}`;
|
|
146
167
|
}
|
|
168
|
+
/** A saved method as a row names it (api/billing.ts `label`); a card with no brand is said in the words of `t`. */
|
|
169
|
+
const method = (m, t) => (m.last4 && !m.brand ? t('Card •••• {last4}', { last4: m.last4 }) : label(m));
|
|
147
170
|
const word = (s) => (s ? s.charAt(0).toUpperCase() + s.slice(1).replace(/_/g, ' ') : '');
|
|
148
171
|
function Ending({ plan, open, working, onOpenChange, onConfirm, }) {
|
|
149
|
-
|
|
172
|
+
const { t } = useI18n();
|
|
173
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 420, showCloseButton: false, children: [_jsx(DialogTitle, { children: t('Cancel your plan?') }), _jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: day(plan.ends)
|
|
174
|
+
? t('{plan} stays on until {date}, and nothing is charged after that. You can keep it any time before then.', { plan: plan.name, date: day(plan.ends) })
|
|
175
|
+
: t('{plan} stays on until the end of this period, and nothing is charged after that. You can keep it any time before then.', { plan: plan.name }) }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => onOpenChange(false), children: t('Keep plan') }), _jsx(Button, { size: "sm", variant: "destructive", disabled: working, onPress: onConfirm, children: t('Cancel plan') })] })] })] }) }));
|
|
150
176
|
}
|
|
151
177
|
/** One invoice, read in the page, with its PDF to keep. Drawn while it is open, so each opens anew. */
|
|
152
178
|
function Bill({ invoice: b, onClose }) {
|
|
153
|
-
const t =
|
|
179
|
+
const { t } = useI18n();
|
|
180
|
+
const target = useTarget();
|
|
154
181
|
const [note, setNote] = useState('');
|
|
155
182
|
const [working, setWorking] = useState(false);
|
|
156
183
|
const keep = async () => {
|
|
157
184
|
setWorking(true);
|
|
158
185
|
setNote('');
|
|
159
186
|
try {
|
|
160
|
-
const blob = await pdf(
|
|
187
|
+
const blob = await pdf(target, b.id);
|
|
161
188
|
const url = URL.createObjectURL(blob);
|
|
162
189
|
const a = document.createElement('a');
|
|
163
190
|
a.href = url;
|
|
@@ -166,12 +193,12 @@ function Bill({ invoice: b, onClose }) {
|
|
|
166
193
|
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
167
194
|
}
|
|
168
195
|
catch (e) {
|
|
169
|
-
setNote(e
|
|
196
|
+
setNote(told(e, t));
|
|
170
197
|
}
|
|
171
198
|
finally {
|
|
172
199
|
setWorking(false);
|
|
173
200
|
}
|
|
174
201
|
};
|
|
175
202
|
const line = (what, cents, strong = false) => (_jsxs(XStack, { justify: "space-between", gap: "$3", children: [_jsx(SizableText, { size: "$2", color: strong ? '$ink' : '$soft', children: what }), _jsx(SizableText, { size: "$2", color: strong ? '$ink' : '$soft', children: money(cents, b.currency) })] }, what));
|
|
176
|
-
return (_jsx(Dialog, { open: true, onOpenChange: (o) => !o && onClose(), children: _jsxs(DialogContent, { maxW: 480, showCloseButton: false, children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsx(DialogTitle, { children: b.number ?
|
|
203
|
+
return (_jsx(Dialog, { open: true, onOpenChange: (o) => !o && onClose(), children: _jsxs(DialogContent, { maxW: 480, showCloseButton: false, children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsx(DialogTitle, { children: b.number ? t('Invoice {number}', { number: b.number }) : t('Invoice') }), _jsx(XStack, { render: "button", "aria-label": t('Close'), p: "$1", onPress: onClose, children: _jsx(X, { size: 16 }) })] }), _jsxs(YStack, { gap: "$3", children: [_jsx(SizableText, { size: "$2", color: "$soft", children: [day(b.date), word(b.status), b.start && b.end ? t('for {start} – {end}', { start: day(b.start), end: day(b.end) }) : ''].filter(Boolean).join(' · ') }), _jsx(YStack, { gap: "$1.5", borderTopWidth: 1, borderColor: "$borderColor", pt: "$3", children: b.lines.length ? b.lines.map((l, i) => line(l.description || t('Line {n}', { n: i + 1 }), l.amount)) : line(t('Subtotal'), b.subtotal) }), _jsxs(YStack, { gap: "$1.5", borderTopWidth: 1, borderColor: "$borderColor", pt: "$3", children: [b.discount ? line(t('Discount'), -b.discount) : null, b.tax ? line(t('Tax'), b.tax) : null, line(t('Total'), b.total, true), b.credit ? line(t('Credit applied'), -b.credit) : null, line(t('Paid'), b.paid), b.due ? line(t('Due'), b.due, true) : null] }), _jsx(Note, { children: note }), _jsx(XStack, { justify: "flex-end", children: _jsxs(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void keep(), children: [_jsx(Download, { size: 14 }), " ", t('Download PDF')] }) })] })] }) }));
|
|
177
204
|
}
|
|
@@ -5,51 +5,62 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
5
5
|
* servers. A tool that is off is still listed to the agent's discovery but is
|
|
6
6
|
* refused when called. Switching a kind on turns on every tool of that kind;
|
|
7
7
|
* off turns them all off. It is the organization's setting, not only yours.
|
|
8
|
+
*
|
|
9
|
+
* Its words are the reader's language (@hanzo/i18n `t`).
|
|
8
10
|
*/
|
|
9
11
|
import { SizableText, YStack } from '@hanzo/gui';
|
|
12
|
+
import { phrase } from '@hanzo/i18n';
|
|
13
|
+
import { useI18n } from '@hanzo/i18n/react';
|
|
10
14
|
import { Switch } from '@hanzo/ui';
|
|
11
15
|
import { useState } from 'react';
|
|
16
|
+
import { told } from '../api/call.js';
|
|
12
17
|
import { activate, SOURCES, tools } from '../api/capabilities.js';
|
|
13
18
|
import { useRead } from '../data.js';
|
|
14
19
|
import { useHost, useTarget } from '../host.js';
|
|
15
20
|
import { Card, Group, Heading, Note, Row, Soft } from './ui.js';
|
|
21
|
+
/** Each kind's name and the sentence that says what it is, English and marked (`phrase`), drawn through `t`. */
|
|
16
22
|
const KINDS = {
|
|
17
|
-
connector: { label: 'Connector actions', about: 'Actions in the apps your organization connected' },
|
|
18
|
-
function: { label: 'Functions', about: 'Functions your organization wrote' },
|
|
19
|
-
'zap-service': { label: 'Services', about: 'Routes of the services your organization runs' },
|
|
20
|
-
agent: { label: 'Agents', about: 'Your organization’s agents, called as tools' },
|
|
21
|
-
skill: { label: 'Skills', about: 'Skills attached to agents' },
|
|
22
|
-
mcp: { label: 'MCP servers', about: 'Tools on the MCP servers your organization added' },
|
|
23
|
+
connector: { label: phrase('Connector actions'), about: phrase('Actions in the apps your organization connected.') },
|
|
24
|
+
function: { label: phrase('Functions'), about: phrase('Functions your organization wrote.') },
|
|
25
|
+
'zap-service': { label: phrase('Services'), about: phrase('Routes of the services your organization runs.') },
|
|
26
|
+
agent: { label: phrase('Agents'), about: phrase('Your organization’s agents, called as tools.') },
|
|
27
|
+
skill: { label: phrase('Skills'), about: phrase('Skills attached to agents.') },
|
|
28
|
+
mcp: { label: phrase('MCP servers'), about: phrase('Tools on the MCP servers your organization added.') },
|
|
23
29
|
};
|
|
24
30
|
export function Capabilities() {
|
|
31
|
+
const { t } = useI18n();
|
|
25
32
|
const host = useHost();
|
|
26
|
-
const
|
|
33
|
+
const target = useTarget();
|
|
27
34
|
const signed = Boolean(host.person);
|
|
28
|
-
const list = useRead(signed ? () => tools(
|
|
35
|
+
const list = useRead(signed ? () => tools(target) : null, [], [target, signed]);
|
|
29
36
|
const [busy, setBusy] = useState('');
|
|
30
37
|
const [note, setNote] = useState('');
|
|
31
38
|
if (!signed) {
|
|
32
|
-
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title:
|
|
39
|
+
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: t('Capabilities') }), _jsx(Soft, { children: t('Sign in to see what the agent may use.') })] }));
|
|
33
40
|
}
|
|
34
41
|
const kinds = SOURCES.map((s) => ({ source: s, all: list.value.filter((x) => x.source === s) })).filter((k) => k.all.length);
|
|
35
42
|
const flip = async (source, all, on) => {
|
|
36
43
|
setBusy(source);
|
|
37
44
|
setNote('');
|
|
38
45
|
try {
|
|
39
|
-
await activate(
|
|
46
|
+
await activate(target, all.map((x) => x.name), on);
|
|
40
47
|
list.reload();
|
|
41
48
|
}
|
|
42
49
|
catch (e) {
|
|
43
|
-
setNote(e
|
|
50
|
+
setNote(told(e, t));
|
|
44
51
|
}
|
|
45
52
|
finally {
|
|
46
53
|
setBusy('');
|
|
47
54
|
}
|
|
48
55
|
};
|
|
49
|
-
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title:
|
|
50
|
-
? '
|
|
51
|
-
: '
|
|
56
|
+
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: t('Capabilities'), detail: host.org
|
|
57
|
+
? t('What the agent may use in {org}. A tool that is off is refused when the agent calls it.', { org: host.org })
|
|
58
|
+
: t('What the agent may use in this organization. A tool that is off is refused when the agent calls it.') }), _jsx(Group, { title: t('Tools'), detail: host.admin
|
|
59
|
+
? t('Switching a kind on or off applies to every tool of that kind, for everyone in the organization.')
|
|
60
|
+
: t('An org admin switches these, for everyone in the organization.'), children: list.error ? (_jsx(Soft, { children: told(list.error, t) })) : list.loading && !list.value.length ? (_jsx(Soft, { children: t('Reading tools…') })) : !kinds.length ? (_jsx(Soft, { children: t('No tools are available to this organization yet.') })) : (_jsx(Card, { children: kinds.map(({ source, all }, i) => {
|
|
52
61
|
const on = all.filter((x) => x.activated).length;
|
|
53
|
-
|
|
62
|
+
// What the kind is, then how much of it is on: two sentences.
|
|
63
|
+
const count = on === all.length ? t('All {n} on.', { n: all.length }) : on ? t('{on} of {n} on.', { on, n: all.length }) : t('{n} available, all off.', { n: all.length });
|
|
64
|
+
return (_jsx(Row, { first: i === 0, title: t(KINDS[source].label), detail: `${t(KINDS[source].about)} ${count}`, trailing: host.admin ? (_jsx(Switch, { "aria-label": t(KINDS[source].label), checked: on === all.length, disabled: busy !== '', onCheckedChange: (v) => void flip(source, all, v) })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: on === all.length ? t('On') : on ? t('Partly on') : t('Off') })) }, source));
|
|
54
65
|
}) })) }), _jsx(Note, { children: note })] }));
|
|
55
66
|
}
|
package/lib/settings/card.js
CHANGED
|
@@ -6,12 +6,18 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
6
6
|
* the card from that. Which processor, and its sandbox or live account, is the
|
|
7
7
|
* platform's answer at /v1/billing/settings, so the card is saved to the account
|
|
8
8
|
* it will be charged on.
|
|
9
|
+
*
|
|
10
|
+
* Its words are the reader's language (@hanzo/i18n `t`); what the processor's
|
|
11
|
+
* field says of a card is the processor's, drawn as given.
|
|
9
12
|
*/
|
|
10
13
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
14
|
+
import { phrase } from '@hanzo/i18n';
|
|
15
|
+
import { useI18n } from '@hanzo/i18n/react';
|
|
11
16
|
import { ChevronDown, ChevronUp, X } from '@hanzogui/lucide-icons-2';
|
|
12
17
|
import { Button, Dialog, DialogContent, DialogTitle, Input } from '@hanzo/ui';
|
|
13
18
|
import { useEffect, useRef, useState } from 'react';
|
|
14
19
|
import { processor, save } from '../api/billing.js';
|
|
20
|
+
import { told } from '../api/call.js';
|
|
15
21
|
import { useRead } from '../data.js';
|
|
16
22
|
import { useTarget } from '../host.js';
|
|
17
23
|
import { Field as FormField } from './ui.js';
|
|
@@ -29,10 +35,10 @@ function load(environment) {
|
|
|
29
35
|
const s = document.createElement('script');
|
|
30
36
|
s.src = src;
|
|
31
37
|
s.async = true;
|
|
32
|
-
s.onload = () => (window.Square ? resolve(window.Square) : reject(new Error('The card form did not load')));
|
|
38
|
+
s.onload = () => (window.Square ? resolve(window.Square) : reject(new Error(phrase('The card form did not load'))));
|
|
33
39
|
s.onerror = () => {
|
|
34
40
|
loads.delete(src);
|
|
35
|
-
reject(new Error('The card form did not load'));
|
|
41
|
+
reject(new Error(phrase('The card form did not load')));
|
|
36
42
|
};
|
|
37
43
|
document.head.appendChild(s);
|
|
38
44
|
});
|
|
@@ -41,8 +47,9 @@ function load(environment) {
|
|
|
41
47
|
}
|
|
42
48
|
const ready = (p) => Boolean(p && p.provider === 'square' && p.application && p.location);
|
|
43
49
|
export function AddCard({ open, onOpenChange, onSaved }) {
|
|
44
|
-
const t =
|
|
45
|
-
const
|
|
50
|
+
const { t } = useI18n();
|
|
51
|
+
const target = useTarget();
|
|
52
|
+
const cfg = useRead(open ? () => processor(target) : null, null, [target, open]);
|
|
46
53
|
const [box, setBox] = useState(null);
|
|
47
54
|
const field = useRef(null);
|
|
48
55
|
const [mounted, setMounted] = useState(false);
|
|
@@ -75,7 +82,7 @@ export function AddCard({ open, onOpenChange, onSaved }) {
|
|
|
75
82
|
})
|
|
76
83
|
.catch((e) => {
|
|
77
84
|
if (!gone)
|
|
78
|
-
setNote(e instanceof Error ? e
|
|
85
|
+
setNote(e instanceof Error ? told(e, t) : t('The card form did not load'));
|
|
79
86
|
});
|
|
80
87
|
return () => {
|
|
81
88
|
gone = true;
|
|
@@ -94,7 +101,7 @@ export function AddCard({ open, onOpenChange, onSaved }) {
|
|
|
94
101
|
try {
|
|
95
102
|
const r = await f.tokenize();
|
|
96
103
|
if (r.status !== 'OK' || !r.token)
|
|
97
|
-
throw new Error(r.errors?.map((e) => e.message).filter(Boolean).join('; ') || 'The card details are incomplete');
|
|
104
|
+
throw new Error(r.errors?.map((e) => e.message).filter(Boolean).join('; ') || phrase('The card details are incomplete'));
|
|
98
105
|
const billingAddress = name.trim() || postalCode.trim() || line1.trim() || city.trim() || stateVal.trim()
|
|
99
106
|
? {
|
|
100
107
|
name: name.trim() || undefined,
|
|
@@ -105,19 +112,19 @@ export function AddCard({ open, onOpenChange, onSaved }) {
|
|
|
105
112
|
country: country.trim() || 'US',
|
|
106
113
|
}
|
|
107
114
|
: undefined;
|
|
108
|
-
const m = await save(
|
|
115
|
+
const m = await save(target, r.token, billingAddress);
|
|
109
116
|
onSaved(m);
|
|
110
117
|
onOpenChange(false);
|
|
111
118
|
}
|
|
112
119
|
catch (e) {
|
|
113
|
-
setNote(e instanceof Error ? e
|
|
120
|
+
setNote(e instanceof Error ? told(e, t) : t('The card was not saved'));
|
|
114
121
|
}
|
|
115
122
|
finally {
|
|
116
123
|
setWorking(false);
|
|
117
124
|
}
|
|
118
125
|
};
|
|
119
|
-
const why = cfg.error ? cfg.error
|
|
120
|
-
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 460, showCloseButton: false, children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsx(DialogTitle, { children:
|
|
121
|
-
? 'Sandbox: use a test card. Nothing is charged.'
|
|
122
|
-
: 'The card number goes to the payment processor, never to Hanzo. Saving it charges nothing.' })] })), note ? (_jsx(SizableText, { size: "$2", color: "$red10", role: "alert", children: note })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => onOpenChange(false), children:
|
|
126
|
+
const why = cfg.error ? told(cfg.error, t) : !cfg.loading && !ready(p) ? t('This deployment takes no cards yet.') : '';
|
|
127
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { maxW: 460, showCloseButton: false, children: [_jsxs(XStack, { items: "center", justify: "space-between", gap: "$2", children: [_jsx(DialogTitle, { children: t('Add a card') }), _jsx(XStack, { render: "button", "aria-label": t('Close'), p: "$1", onPress: () => onOpenChange(false), children: _jsx(X, { size: 16 }) })] }), _jsxs(YStack, { gap: "$3", children: [why ? (_jsx(SizableText, { size: "$2", color: "$soft", children: why })) : (_jsxs(_Fragment, { children: [_jsxs(YStack, { minH: 96, "aria-label": t('Card details'), children: [_jsx("div", { ref: setBox, style: { minHeight: 96 } }), !mounted && !note ? (_jsx(SizableText, { size: "$1", color: "$soft", children: t('Loading the card form…') })) : null] }), _jsxs(XStack, { gap: "$2", children: [_jsx(YStack, { flex: 1, children: _jsx(FormField, { label: t('Name on card'), hint: t('Optional'), children: _jsx(Input, { value: name, onChangeText: setName, placeholder: t('Name or business') }) }) }), _jsx(YStack, { width: 120, children: _jsx(FormField, { label: t('Postal code'), hint: t('Optional'), children: _jsx(Input, { value: postalCode, onChangeText: setPostalCode, placeholder: t('ZIP / Postal') }) }) })] }), _jsx(XStack, { children: _jsxs(Button, { size: "sm", variant: "ghost", onPress: () => setMoreAddress(!moreAddress), children: [moreAddress ? _jsx(ChevronUp, { size: 14 }) : _jsx(ChevronDown, { size: 14 }), moreAddress ? t('Hide address') : t('Add billing address')] }) }), moreAddress ? (_jsxs(YStack, { gap: "$2", pt: "$1", children: [_jsx(FormField, { label: t('Street address'), children: _jsx(Input, { value: line1, onChangeText: setLine1, placeholder: t('Address line 1') }) }), _jsxs(XStack, { gap: "$2", children: [_jsx(YStack, { flex: 1, children: _jsx(FormField, { label: t('City'), children: _jsx(Input, { value: city, onChangeText: setCity, placeholder: t('City') }) }) }), _jsx(YStack, { width: 90, children: _jsx(FormField, { label: t('State'), children: _jsx(Input, { value: stateVal, onChangeText: setStateVal, placeholder: t('State') }) }) }), _jsx(YStack, { width: 90, children: _jsx(FormField, { label: t('Country'), children: _jsx(Input, { value: country, onChangeText: setCountry, placeholder: t('Country (US)') }) }) })] })] })) : null, _jsx(SizableText, { size: "$1", color: "$soft", children: p && p.environment !== 'production'
|
|
128
|
+
? t('Sandbox: use a test card. Nothing is charged.')
|
|
129
|
+
: t('The card number goes to the payment processor, never to Hanzo. Saving it charges nothing.') })] })), note ? (_jsx(SizableText, { size: "$2", color: "$red10", role: "alert", children: note })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => onOpenChange(false), children: t('Cancel') }), _jsx(Button, { size: "sm", disabled: !mounted || working, onPress: () => void add(), children: t('Save card') })] })] })] }) }));
|
|
123
130
|
}
|