@jonsoc/console-app 1.1.34
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/.opencode/agent/css.md +149 -0
- package/README.md +32 -0
- package/package.json +49 -0
- package/public/apple-touch-icon-v3.png +1 -0
- package/public/apple-touch-icon.png +1 -0
- package/public/email +1 -0
- package/public/favicon-96x96-v3.png +1 -0
- package/public/favicon-96x96.png +1 -0
- package/public/favicon-v3.ico +1 -0
- package/public/favicon-v3.svg +1 -0
- package/public/favicon.ico +1 -0
- package/public/favicon.svg +1 -0
- package/public/opencode-brand-assets.zip +0 -0
- package/public/robots.txt +6 -0
- package/public/site.webmanifest +1 -0
- package/public/social-share-black.png +1 -0
- package/public/social-share-zen.png +1 -0
- package/public/social-share.png +1 -0
- package/public/theme.json +182 -0
- package/public/web-app-manifest-192x192.png +1 -0
- package/public/web-app-manifest-512x512.png +1 -0
- package/script/generate-sitemap.ts +103 -0
- package/src/app.css +1 -0
- package/src/app.tsx +27 -0
- package/src/asset/black/hero.png +0 -0
- package/src/asset/brand/opencode-brand-assets.zip +0 -0
- package/src/asset/brand/opencode-logo-dark.png +0 -0
- package/src/asset/brand/opencode-logo-dark.svg +16 -0
- package/src/asset/brand/opencode-logo-light.png +0 -0
- package/src/asset/brand/opencode-logo-light.svg +16 -0
- package/src/asset/brand/opencode-wordmark-dark.png +0 -0
- package/src/asset/brand/opencode-wordmark-dark.svg +30 -0
- package/src/asset/brand/opencode-wordmark-light.png +0 -0
- package/src/asset/brand/opencode-wordmark-light.svg +30 -0
- package/src/asset/brand/opencode-wordmark-simple-dark.png +0 -0
- package/src/asset/brand/opencode-wordmark-simple-dark.svg +22 -0
- package/src/asset/brand/opencode-wordmark-simple-light.png +0 -0
- package/src/asset/brand/opencode-wordmark-simple-light.svg +22 -0
- package/src/asset/brand/preview-opencode-dark.png +0 -0
- package/src/asset/brand/preview-opencode-logo-dark.png +0 -0
- package/src/asset/brand/preview-opencode-logo-light.png +0 -0
- package/src/asset/brand/preview-opencode-wordmark-dark.png +0 -0
- package/src/asset/brand/preview-opencode-wordmark-light.png +0 -0
- package/src/asset/brand/preview-opencode-wordmark-simple-dark.png +0 -0
- package/src/asset/brand/preview-opencode-wordmark-simple-light.png +0 -0
- package/src/asset/lander/avatar-adam.png +0 -0
- package/src/asset/lander/avatar-david.png +0 -0
- package/src/asset/lander/avatar-dax.png +0 -0
- package/src/asset/lander/avatar-frank.png +0 -0
- package/src/asset/lander/avatar-jay.png +0 -0
- package/src/asset/lander/brand-assets-dark.svg +10 -0
- package/src/asset/lander/brand-assets-light.svg +10 -0
- package/src/asset/lander/brand.png +0 -0
- package/src/asset/lander/check.svg +3 -0
- package/src/asset/lander/copy.svg +3 -0
- package/src/asset/lander/desktop-app-icon.png +0 -0
- package/src/asset/lander/dock.png +0 -0
- package/src/asset/lander/logo-dark.svg +11 -0
- package/src/asset/lander/logo-light.svg +11 -0
- package/src/asset/lander/opencode-comparison-min.mp4 +0 -0
- package/src/asset/lander/opencode-comparison-poster.png +0 -0
- package/src/asset/lander/opencode-desktop-icon.png +0 -0
- package/src/asset/lander/opencode-logo-dark.svg +11 -0
- package/src/asset/lander/opencode-logo-light.svg +11 -0
- package/src/asset/lander/opencode-min.mp4 +0 -0
- package/src/asset/lander/opencode-poster.png +0 -0
- package/src/asset/lander/opencode-wordmark-dark.svg +25 -0
- package/src/asset/lander/opencode-wordmark-light.svg +25 -0
- package/src/asset/lander/screenshot-github.png +0 -0
- package/src/asset/lander/screenshot-splash.png +0 -0
- package/src/asset/lander/screenshot-vscode.png +0 -0
- package/src/asset/lander/screenshot.png +0 -0
- package/src/asset/lander/wordmark-dark.svg +3 -0
- package/src/asset/lander/wordmark-light.svg +3 -0
- package/src/asset/logo-ornate-dark.svg +18 -0
- package/src/asset/logo-ornate-light.svg +18 -0
- package/src/asset/logo.svg +18 -0
- package/src/asset/zen-ornate-dark.svg +8 -0
- package/src/asset/zen-ornate-light.svg +8 -0
- package/src/component/dropdown.css +80 -0
- package/src/component/dropdown.tsx +79 -0
- package/src/component/email-signup.tsx +48 -0
- package/src/component/faq.tsx +33 -0
- package/src/component/footer.tsx +38 -0
- package/src/component/header-context-menu.css +63 -0
- package/src/component/header.tsx +279 -0
- package/src/component/icon.tsx +257 -0
- package/src/component/legal.tsx +20 -0
- package/src/component/modal.css +66 -0
- package/src/component/modal.tsx +24 -0
- package/src/component/spotlight.css +15 -0
- package/src/component/spotlight.tsx +820 -0
- package/src/config.ts +29 -0
- package/src/context/auth.session.ts +0 -0
- package/src/context/auth.ts +116 -0
- package/src/context/auth.withActor.ts +7 -0
- package/src/entry-client.tsx +4 -0
- package/src/entry-server.tsx +30 -0
- package/src/global.d.ts +5 -0
- package/src/lib/github.ts +38 -0
- package/src/middleware.ts +5 -0
- package/src/routes/[...404].css +130 -0
- package/src/routes/[...404].tsx +38 -0
- package/src/routes/api/enterprise.ts +47 -0
- package/src/routes/auth/[...callback].ts +41 -0
- package/src/routes/auth/authorize.ts +10 -0
- package/src/routes/auth/index.ts +12 -0
- package/src/routes/auth/logout.ts +17 -0
- package/src/routes/auth/status.ts +7 -0
- package/src/routes/bench/[id].tsx +365 -0
- package/src/routes/bench/index.tsx +86 -0
- package/src/routes/bench/submission.ts +29 -0
- package/src/routes/black/common.tsx +62 -0
- package/src/routes/black/index.tsx +108 -0
- package/src/routes/black/subscribe/[plan].tsx +449 -0
- package/src/routes/black/workspace.css +214 -0
- package/src/routes/black/workspace.tsx +229 -0
- package/src/routes/black.css +828 -0
- package/src/routes/black.tsx +285 -0
- package/src/routes/brand/index.css +555 -0
- package/src/routes/brand/index.tsx +252 -0
- package/src/routes/changelog/index.css +477 -0
- package/src/routes/changelog/index.tsx +147 -0
- package/src/routes/debug/index.ts +13 -0
- package/src/routes/desktop-feedback.ts +5 -0
- package/src/routes/discord.ts +5 -0
- package/src/routes/docs/[...path].ts +20 -0
- package/src/routes/docs/index.ts +20 -0
- package/src/routes/download/[platform].ts +38 -0
- package/src/routes/download/index.css +750 -0
- package/src/routes/download/index.tsx +482 -0
- package/src/routes/download/types.ts +4 -0
- package/src/routes/enterprise/index.css +578 -0
- package/src/routes/enterprise/index.tsx +251 -0
- package/src/routes/index.css +1251 -0
- package/src/routes/index.tsx +840 -0
- package/src/routes/legal/privacy-policy/index.css +343 -0
- package/src/routes/legal/privacy-policy/index.tsx +1512 -0
- package/src/routes/legal/terms-of-service/index.css +254 -0
- package/src/routes/legal/terms-of-service/index.tsx +512 -0
- package/src/routes/openapi.json.ts +7 -0
- package/src/routes/s/[id].ts +20 -0
- package/src/routes/stripe/webhook.ts +532 -0
- package/src/routes/t/[...path].tsx +20 -0
- package/src/routes/temp.tsx +172 -0
- package/src/routes/user-menu.css +18 -0
- package/src/routes/user-menu.tsx +32 -0
- package/src/routes/workspace/[id]/billing/billing-section.module.css +185 -0
- package/src/routes/workspace/[id]/billing/billing-section.tsx +240 -0
- package/src/routes/workspace/[id]/billing/black-section.module.css +142 -0
- package/src/routes/workspace/[id]/billing/black-section.tsx +269 -0
- package/src/routes/workspace/[id]/billing/black-waitlist-section.module.css +23 -0
- package/src/routes/workspace/[id]/billing/index.tsx +32 -0
- package/src/routes/workspace/[id]/billing/monthly-limit-section.module.css +96 -0
- package/src/routes/workspace/[id]/billing/monthly-limit-section.tsx +133 -0
- package/src/routes/workspace/[id]/billing/payment-section.module.css +93 -0
- package/src/routes/workspace/[id]/billing/payment-section.tsx +122 -0
- package/src/routes/workspace/[id]/billing/reload-section.module.css +261 -0
- package/src/routes/workspace/[id]/billing/reload-section.tsx +213 -0
- package/src/routes/workspace/[id]/graph-section.module.css +145 -0
- package/src/routes/workspace/[id]/graph-section.tsx +475 -0
- package/src/routes/workspace/[id]/index.tsx +81 -0
- package/src/routes/workspace/[id]/keys/index.tsx +11 -0
- package/src/routes/workspace/[id]/keys/key-section.module.css +197 -0
- package/src/routes/workspace/[id]/keys/key-section.tsx +176 -0
- package/src/routes/workspace/[id]/members/index.tsx +11 -0
- package/src/routes/workspace/[id]/members/member-section.module.css +249 -0
- package/src/routes/workspace/[id]/members/member-section.tsx +343 -0
- package/src/routes/workspace/[id]/members/role-dropdown.css +72 -0
- package/src/routes/workspace/[id]/members/role-dropdown.tsx +43 -0
- package/src/routes/workspace/[id]/model-section.module.css +173 -0
- package/src/routes/workspace/[id]/model-section.tsx +174 -0
- package/src/routes/workspace/[id]/new-user-section.module.css +143 -0
- package/src/routes/workspace/[id]/new-user-section.tsx +104 -0
- package/src/routes/workspace/[id]/provider-section.module.css +138 -0
- package/src/routes/workspace/[id]/provider-section.tsx +188 -0
- package/src/routes/workspace/[id]/settings/index.tsx +11 -0
- package/src/routes/workspace/[id]/settings/settings-section.module.css +94 -0
- package/src/routes/workspace/[id]/settings/settings-section.tsx +122 -0
- package/src/routes/workspace/[id]/usage-section.module.css +185 -0
- package/src/routes/workspace/[id]/usage-section.tsx +200 -0
- package/src/routes/workspace/[id].css +308 -0
- package/src/routes/workspace/[id].tsx +62 -0
- package/src/routes/workspace/common.tsx +120 -0
- package/src/routes/workspace-picker.css +74 -0
- package/src/routes/workspace-picker.tsx +122 -0
- package/src/routes/workspace.css +107 -0
- package/src/routes/workspace.tsx +38 -0
- package/src/routes/zen/index.css +866 -0
- package/src/routes/zen/index.tsx +343 -0
- package/src/routes/zen/util/dataDumper.ts +44 -0
- package/src/routes/zen/util/error.ts +13 -0
- package/src/routes/zen/util/handler.ts +784 -0
- package/src/routes/zen/util/logger.ts +12 -0
- package/src/routes/zen/util/provider/anthropic.ts +752 -0
- package/src/routes/zen/util/provider/google.ts +75 -0
- package/src/routes/zen/util/provider/openai-compatible.ts +546 -0
- package/src/routes/zen/util/provider/openai.ts +630 -0
- package/src/routes/zen/util/provider/provider.ts +210 -0
- package/src/routes/zen/util/rateLimiter.ts +41 -0
- package/src/routes/zen/util/stickyProviderTracker.ts +16 -0
- package/src/routes/zen/util/trialLimiter.ts +49 -0
- package/src/routes/zen/v1/chat/completions.ts +11 -0
- package/src/routes/zen/v1/messages.ts +11 -0
- package/src/routes/zen/v1/models/[model].ts +13 -0
- package/src/routes/zen/v1/models.ts +60 -0
- package/src/routes/zen/v1/responses.ts +11 -0
- package/src/style/base.css +21 -0
- package/src/style/component/button.css +102 -0
- package/src/style/index.css +8 -0
- package/src/style/reset.css +76 -0
- package/src/style/token/color.css +91 -0
- package/src/style/token/font.css +21 -0
- package/src/style/token/space.css +46 -0
- package/sst-env.d.ts +9 -0
- package/tsconfig.json +21 -0
- package/vite.config.ts +25 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import "./index.css"
|
|
2
|
+
import { Title } from "@solidjs/meta"
|
|
3
|
+
import { onCleanup, onMount } from "solid-js"
|
|
4
|
+
import logoLight from "../asset/logo-ornate-light.svg"
|
|
5
|
+
import logoDark from "../asset/logo-ornate-dark.svg"
|
|
6
|
+
import IMG_SPLASH from "../asset/lander/screenshot-splash.png"
|
|
7
|
+
import { IconCopy, IconCheck } from "../component/icon"
|
|
8
|
+
|
|
9
|
+
function CopyStatus() {
|
|
10
|
+
return (
|
|
11
|
+
<div data-component="copy-status">
|
|
12
|
+
<IconCopy data-slot="copy" />
|
|
13
|
+
<IconCheck data-slot="check" />
|
|
14
|
+
</div>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function Home() {
|
|
19
|
+
onMount(() => {
|
|
20
|
+
const commands = document.querySelectorAll("[data-copy]")
|
|
21
|
+
for (const button of commands) {
|
|
22
|
+
const callback = () => {
|
|
23
|
+
const text = button.textContent
|
|
24
|
+
if (text) {
|
|
25
|
+
navigator.clipboard.writeText(text)
|
|
26
|
+
button.setAttribute("data-copied", "")
|
|
27
|
+
setTimeout(() => {
|
|
28
|
+
button.removeAttribute("data-copied")
|
|
29
|
+
}, 1500)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
button.addEventListener("click", callback)
|
|
33
|
+
onCleanup(() => {
|
|
34
|
+
button.removeEventListener("click", callback)
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<main data-page="home">
|
|
41
|
+
<Title>jonsoc | AI coding agent built for the terminal</Title>
|
|
42
|
+
|
|
43
|
+
<div data-component="content">
|
|
44
|
+
<section data-component="top">
|
|
45
|
+
<img data-slot="logo light" src={logoLight} alt="jonsoc logo light" />
|
|
46
|
+
<img data-slot="logo dark" src={logoDark} alt="jonsoc logo dark" />
|
|
47
|
+
<h1 data-slot="title">The AI coding agent built for the terminal</h1>
|
|
48
|
+
<div data-slot="login">
|
|
49
|
+
<a href="/auth">jonsoc zen</a>
|
|
50
|
+
</div>
|
|
51
|
+
</section>
|
|
52
|
+
|
|
53
|
+
<section data-component="cta">
|
|
54
|
+
<div data-slot="left">
|
|
55
|
+
<a href="/docs">Get Started</a>
|
|
56
|
+
</div>
|
|
57
|
+
<div data-slot="center">
|
|
58
|
+
<a href="/auth">jonsoc zen</a>
|
|
59
|
+
</div>
|
|
60
|
+
<div data-slot="right">
|
|
61
|
+
<button data-copy data-slot="command">
|
|
62
|
+
<span>
|
|
63
|
+
<span>curl -fsSL </span>
|
|
64
|
+
<span data-slot="protocol">https://</span>
|
|
65
|
+
<span data-slot="highlight">jonsoc.com/install</span>
|
|
66
|
+
<span> | bash</span>
|
|
67
|
+
</span>
|
|
68
|
+
<CopyStatus />
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
</section>
|
|
72
|
+
|
|
73
|
+
<section data-component="features">
|
|
74
|
+
<ul data-slot="list">
|
|
75
|
+
<li>
|
|
76
|
+
<strong>Native TUI</strong> A responsive, native, themeable terminal UI
|
|
77
|
+
</li>
|
|
78
|
+
<li>
|
|
79
|
+
<strong>LSP enabled</strong> Automatically loads the right LSPs for the LLM
|
|
80
|
+
</li>
|
|
81
|
+
<li>
|
|
82
|
+
<strong>jonsoc zen</strong> A <a href="/docs/zen">curated list of models</a> provided by jonsoc{" "}
|
|
83
|
+
<label>New</label>
|
|
84
|
+
</li>
|
|
85
|
+
<li>
|
|
86
|
+
<strong>Multi-session</strong> Start multiple agents in parallel on the same project
|
|
87
|
+
</li>
|
|
88
|
+
<li>
|
|
89
|
+
<strong>Shareable links</strong> Share a link to any sessions for reference or to debug
|
|
90
|
+
</li>
|
|
91
|
+
<li>
|
|
92
|
+
<strong>GitHub Copilot</strong> Log in with GitHub to use your Copilot account
|
|
93
|
+
</li>
|
|
94
|
+
<li>
|
|
95
|
+
<strong>ChatGPT Plus/Pro</strong> Log in with OpenAI to use your ChatGPT Plus or Pro account
|
|
96
|
+
</li>
|
|
97
|
+
<li>
|
|
98
|
+
<strong>Use any model</strong> Supports 75+ LLM providers through{" "}
|
|
99
|
+
<a href="https://models.dev">Models.dev</a>, including local models
|
|
100
|
+
</li>
|
|
101
|
+
</ul>
|
|
102
|
+
</section>
|
|
103
|
+
|
|
104
|
+
<section data-component="install">
|
|
105
|
+
<div data-component="method">
|
|
106
|
+
<h3 data-component="title">npm</h3>
|
|
107
|
+
<button data-copy data-slot="button">
|
|
108
|
+
<span>
|
|
109
|
+
npm install -g <strong>jonsoc</strong>
|
|
110
|
+
</span>
|
|
111
|
+
<CopyStatus />
|
|
112
|
+
</button>
|
|
113
|
+
</div>
|
|
114
|
+
<div data-component="method">
|
|
115
|
+
<h3 data-component="title">bun</h3>
|
|
116
|
+
<button data-copy data-slot="button">
|
|
117
|
+
<span>
|
|
118
|
+
bun install -g <strong>jonsoc</strong>
|
|
119
|
+
</span>
|
|
120
|
+
<CopyStatus />
|
|
121
|
+
</button>
|
|
122
|
+
</div>
|
|
123
|
+
<div data-component="method">
|
|
124
|
+
<h3 data-component="title">homebrew</h3>
|
|
125
|
+
<button data-copy data-slot="button">
|
|
126
|
+
<span>
|
|
127
|
+
brew install <strong>jonsoc</strong>
|
|
128
|
+
</span>
|
|
129
|
+
<CopyStatus />
|
|
130
|
+
</button>
|
|
131
|
+
</div>
|
|
132
|
+
<div data-component="method">
|
|
133
|
+
<h3 data-component="title">paru</h3>
|
|
134
|
+
<button data-copy data-slot="button">
|
|
135
|
+
<span>
|
|
136
|
+
paru -S <strong>jonsoc-bin</strong>
|
|
137
|
+
</span>
|
|
138
|
+
<CopyStatus />
|
|
139
|
+
</button>
|
|
140
|
+
</div>
|
|
141
|
+
</section>
|
|
142
|
+
|
|
143
|
+
<section data-component="screenshots">
|
|
144
|
+
<figure>
|
|
145
|
+
<figcaption>jonsoc TUI with the tokyonight theme</figcaption>
|
|
146
|
+
<a href="/docs/cli">
|
|
147
|
+
<img src={IMG_SPLASH} alt="jonsoc TUI with tokyonight theme" />
|
|
148
|
+
</a>
|
|
149
|
+
</figure>
|
|
150
|
+
</section>
|
|
151
|
+
|
|
152
|
+
<footer data-component="footer">
|
|
153
|
+
<div data-slot="cell">
|
|
154
|
+
<a href="https://x.com/jonsoc">X.com</a>
|
|
155
|
+
</div>
|
|
156
|
+
<div data-slot="cell">
|
|
157
|
+
<a href="https://github.com/anomalyco/jonsoc">GitHub</a>
|
|
158
|
+
</div>
|
|
159
|
+
<div data-slot="cell">
|
|
160
|
+
<a href="https://jonsoc.com/discord">Discord</a>
|
|
161
|
+
</div>
|
|
162
|
+
</footer>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<div data-component="legal">
|
|
166
|
+
<span>
|
|
167
|
+
©2025 <a href="https://anoma.ly">Anomaly</a>
|
|
168
|
+
</span>
|
|
169
|
+
</div>
|
|
170
|
+
</main>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[data-component="user-menu"] {
|
|
2
|
+
[data-component="dropdown"] {
|
|
3
|
+
[data-slot="trigger"] span {
|
|
4
|
+
color: var(--color-text-muted);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
[data-slot="dropdown"] {
|
|
8
|
+
form {
|
|
9
|
+
width: 100%;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
[data-slot="item"] {
|
|
14
|
+
color: var(--color-danger);
|
|
15
|
+
text-decoration: none;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { action } from "@solidjs/router"
|
|
2
|
+
import { getRequestEvent } from "solid-js/web"
|
|
3
|
+
import { useAuthSession } from "~/context/auth"
|
|
4
|
+
import { Dropdown } from "~/component/dropdown"
|
|
5
|
+
import "./user-menu.css"
|
|
6
|
+
|
|
7
|
+
const logout = action(async () => {
|
|
8
|
+
"use server"
|
|
9
|
+
const auth = await useAuthSession()
|
|
10
|
+
const event = getRequestEvent()
|
|
11
|
+
const current = auth.data.current
|
|
12
|
+
if (current)
|
|
13
|
+
await auth.update((val) => {
|
|
14
|
+
delete val.account?.[current]
|
|
15
|
+
const first = Object.keys(val.account ?? {})[0]
|
|
16
|
+
val.current = first
|
|
17
|
+
event!.locals.actor = undefined
|
|
18
|
+
return val
|
|
19
|
+
})
|
|
20
|
+
}, "auth.logout")
|
|
21
|
+
|
|
22
|
+
export function UserMenu(props: { email: string | null | undefined }) {
|
|
23
|
+
return (
|
|
24
|
+
<div data-component="user-menu">
|
|
25
|
+
<Dropdown trigger={props.email ?? ""} align="right">
|
|
26
|
+
<a href="/auth/logout" data-slot="item">
|
|
27
|
+
Logout
|
|
28
|
+
</a>
|
|
29
|
+
</Dropdown>
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
[data-slot="reload-error"] {
|
|
3
|
+
display: flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
justify-content: space-between;
|
|
6
|
+
gap: var(--space-4);
|
|
7
|
+
|
|
8
|
+
p {
|
|
9
|
+
color: var(--color-danger);
|
|
10
|
+
font-size: var(--font-size-sm);
|
|
11
|
+
line-height: 1.4;
|
|
12
|
+
margin: 0;
|
|
13
|
+
flex: 1;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
[data-slot="create-form"] {
|
|
17
|
+
display: flex;
|
|
18
|
+
gap: var(--space-2);
|
|
19
|
+
margin: 0;
|
|
20
|
+
flex-shrink: 0;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
[data-slot="section-content"] {
|
|
25
|
+
display: flex;
|
|
26
|
+
flex-direction: column;
|
|
27
|
+
gap: var(--space-3);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
[data-slot="balance-display"] {
|
|
31
|
+
display: flex;
|
|
32
|
+
align-items: flex-start;
|
|
33
|
+
gap: var(--space-3);
|
|
34
|
+
|
|
35
|
+
@media (max-width: 30rem) {
|
|
36
|
+
flex-direction: column;
|
|
37
|
+
align-items: flex-start;
|
|
38
|
+
gap: var(--space-2);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
[data-slot="balance-amount"] {
|
|
42
|
+
display: flex;
|
|
43
|
+
flex-direction: column;
|
|
44
|
+
align-items: center;
|
|
45
|
+
justify-content: center;
|
|
46
|
+
text-align: center;
|
|
47
|
+
padding: var(--space-4);
|
|
48
|
+
border: 1px solid var(--color-border);
|
|
49
|
+
border-radius: var(--border-radius-sm);
|
|
50
|
+
background-color: var(--color-bg-surface);
|
|
51
|
+
align-self: stretch;
|
|
52
|
+
|
|
53
|
+
[data-slot="balance-label"] {
|
|
54
|
+
font-size: var(--font-size-sm);
|
|
55
|
+
color: var(--color-text-muted);
|
|
56
|
+
margin-top: var(--space-2);
|
|
57
|
+
font-weight: 400;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
[data-slot="balance-value"] {
|
|
61
|
+
font-size: var(--font-size-2xl);
|
|
62
|
+
font-weight: 600;
|
|
63
|
+
color: var(--color-text);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
[data-slot="balance-right-section"] {
|
|
68
|
+
display: flex;
|
|
69
|
+
flex-direction: column;
|
|
70
|
+
gap: var(--space-3);
|
|
71
|
+
flex: 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
[data-slot="add-balance-form-container"] {
|
|
75
|
+
display: flex;
|
|
76
|
+
flex-direction: column;
|
|
77
|
+
gap: var(--space-2);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
[data-slot="add-balance-form"] {
|
|
81
|
+
display: flex;
|
|
82
|
+
flex-direction: row;
|
|
83
|
+
align-items: center;
|
|
84
|
+
gap: var(--space-3);
|
|
85
|
+
|
|
86
|
+
label {
|
|
87
|
+
font-size: var(--font-size-sm);
|
|
88
|
+
font-weight: 500;
|
|
89
|
+
color: var(--color-text-muted);
|
|
90
|
+
white-space: nowrap;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
input[data-component="input"] {
|
|
94
|
+
padding: var(--space-2) var(--space-3);
|
|
95
|
+
border: 1px solid var(--color-border);
|
|
96
|
+
border-radius: var(--border-radius-sm);
|
|
97
|
+
background-color: var(--color-bg);
|
|
98
|
+
color: var(--color-text);
|
|
99
|
+
font-size: var(--font-size-sm);
|
|
100
|
+
line-height: 1.5;
|
|
101
|
+
|
|
102
|
+
&:focus {
|
|
103
|
+
outline: none;
|
|
104
|
+
border-color: var(--color-accent);
|
|
105
|
+
box-shadow: 0 0 0 3px var(--color-accent-alpha);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
&::placeholder {
|
|
109
|
+
color: var(--color-text-disabled);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
[data-slot="form-actions"] {
|
|
114
|
+
display: flex;
|
|
115
|
+
gap: var(--space-2);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
[data-slot="form-error"] {
|
|
120
|
+
color: var(--color-danger);
|
|
121
|
+
font-size: var(--font-size-sm);
|
|
122
|
+
line-height: 1.4;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
[data-slot="credit-card"] {
|
|
126
|
+
padding: var(--space-2) var(--space-4);
|
|
127
|
+
background-color: var(--color-bg-surface);
|
|
128
|
+
border-radius: var(--border-radius-sm);
|
|
129
|
+
display: flex;
|
|
130
|
+
align-items: center;
|
|
131
|
+
gap: var(--space-3);
|
|
132
|
+
min-width: 150px;
|
|
133
|
+
align-self: flex-start;
|
|
134
|
+
|
|
135
|
+
[data-slot="card-icon"] {
|
|
136
|
+
display: flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
color: var(--color-text-muted);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
[data-slot="card-details"] {
|
|
142
|
+
display: flex;
|
|
143
|
+
align-items: baseline;
|
|
144
|
+
gap: var(--space-1);
|
|
145
|
+
flex: 1;
|
|
146
|
+
justify-content: flex-end;
|
|
147
|
+
|
|
148
|
+
[data-slot="secret"] {
|
|
149
|
+
font-size: var(--font-size-sm);
|
|
150
|
+
color: var(--color-text-muted);
|
|
151
|
+
font-weight: 400;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
[data-slot="number"] {
|
|
155
|
+
font-size: var(--font-size-sm);
|
|
156
|
+
font-weight: 500;
|
|
157
|
+
color: var(--color-text-muted);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
[data-slot="type"] {
|
|
161
|
+
font-size: var(--font-size-sm);
|
|
162
|
+
font-weight: 400;
|
|
163
|
+
color: var(--color-text-muted);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
button {
|
|
168
|
+
white-space: nowrap;
|
|
169
|
+
flex-shrink: 0;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
button {
|
|
174
|
+
align-self: flex-start;
|
|
175
|
+
white-space: nowrap;
|
|
176
|
+
flex-shrink: 0;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
[data-slot="enable-billing-button"] {
|
|
181
|
+
align-self: flex-start;
|
|
182
|
+
padding: var(--space-4);
|
|
183
|
+
min-width: 150px;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { action, useParams, useAction, createAsync, useSubmission, json } from "@solidjs/router"
|
|
2
|
+
import { createMemo, Match, Show, Switch, createEffect } from "solid-js"
|
|
3
|
+
import { createStore } from "solid-js/store"
|
|
4
|
+
import { Billing } from "@jonsoc/console-core/billing.js"
|
|
5
|
+
import { withActor } from "~/context/auth.withActor"
|
|
6
|
+
import { IconCreditCard, IconStripe } from "~/component/icon"
|
|
7
|
+
import styles from "./billing-section.module.css"
|
|
8
|
+
import { createCheckoutUrl, formatBalance, queryBillingInfo } from "../../common"
|
|
9
|
+
|
|
10
|
+
const createSessionUrl = action(async (workspaceID: string, returnUrl: string) => {
|
|
11
|
+
"use server"
|
|
12
|
+
return json(
|
|
13
|
+
await withActor(
|
|
14
|
+
() =>
|
|
15
|
+
Billing.generateSessionUrl({ returnUrl })
|
|
16
|
+
.then((data) => ({ error: undefined, data }))
|
|
17
|
+
.catch((e) => ({
|
|
18
|
+
error: e.message as string,
|
|
19
|
+
data: undefined,
|
|
20
|
+
})),
|
|
21
|
+
workspaceID,
|
|
22
|
+
),
|
|
23
|
+
{ revalidate: queryBillingInfo.key },
|
|
24
|
+
)
|
|
25
|
+
}, "sessionUrl")
|
|
26
|
+
|
|
27
|
+
export function BillingSection() {
|
|
28
|
+
const params = useParams()
|
|
29
|
+
// ORIGINAL CODE - COMMENTED OUT FOR TESTING
|
|
30
|
+
const billingInfo = createAsync(() => queryBillingInfo(params.id!))
|
|
31
|
+
const checkoutAction = useAction(createCheckoutUrl)
|
|
32
|
+
const checkoutSubmission = useSubmission(createCheckoutUrl)
|
|
33
|
+
const sessionAction = useAction(createSessionUrl)
|
|
34
|
+
const sessionSubmission = useSubmission(createSessionUrl)
|
|
35
|
+
const [store, setStore] = createStore({
|
|
36
|
+
showAddBalanceForm: false,
|
|
37
|
+
addBalanceAmount: billingInfo()?.reloadAmount.toString() ?? "",
|
|
38
|
+
checkoutRedirecting: false,
|
|
39
|
+
sessionRedirecting: false,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
createEffect(() => {
|
|
43
|
+
const info = billingInfo()
|
|
44
|
+
if (info) {
|
|
45
|
+
setStore("addBalanceAmount", info.reloadAmount.toString())
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
const balance = createMemo(() => formatBalance(billingInfo()?.balance ?? 0))
|
|
49
|
+
|
|
50
|
+
async function onClickCheckout() {
|
|
51
|
+
const amount = parseInt(store.addBalanceAmount)
|
|
52
|
+
const baseUrl = window.location.href
|
|
53
|
+
|
|
54
|
+
const checkout = await checkoutAction(params.id!, amount, baseUrl, baseUrl)
|
|
55
|
+
if (checkout && checkout.data) {
|
|
56
|
+
setStore("checkoutRedirecting", true)
|
|
57
|
+
window.location.href = checkout.data
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function onClickSession() {
|
|
62
|
+
const baseUrl = window.location.href
|
|
63
|
+
const sessionUrl = await sessionAction(params.id!, baseUrl)
|
|
64
|
+
if (sessionUrl && sessionUrl.data) {
|
|
65
|
+
setStore("sessionRedirecting", true)
|
|
66
|
+
window.location.href = sessionUrl.data
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function showAddBalanceForm() {
|
|
71
|
+
while (true) {
|
|
72
|
+
checkoutSubmission.clear()
|
|
73
|
+
if (!checkoutSubmission.result) break
|
|
74
|
+
}
|
|
75
|
+
setStore({
|
|
76
|
+
showAddBalanceForm: true,
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function hideAddBalanceForm() {
|
|
81
|
+
setStore("showAddBalanceForm", false)
|
|
82
|
+
checkoutSubmission.clear()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// DUMMY DATA FOR TESTING - UNCOMMENT ONE OF THE SCENARIOS BELOW
|
|
86
|
+
|
|
87
|
+
// Scenario 1: User has not added billing details and has no balance
|
|
88
|
+
// const balanceInfo = () => ({
|
|
89
|
+
// balance: 0,
|
|
90
|
+
// paymentMethodType: null as string | null,
|
|
91
|
+
// paymentMethodLast4: null as string | null,
|
|
92
|
+
// reload: false,
|
|
93
|
+
// reloadError: null as string | null,
|
|
94
|
+
// timeReloadError: null as Date | null,
|
|
95
|
+
// })
|
|
96
|
+
|
|
97
|
+
// Scenario 2: User has not added billing details but has a balance
|
|
98
|
+
// const balanceInfo = () => ({
|
|
99
|
+
// balance: 1500000000, // $15.00
|
|
100
|
+
// paymentMethodType: null as string | null,
|
|
101
|
+
// paymentMethodLast4: null as string | null,
|
|
102
|
+
// reload: false,
|
|
103
|
+
// reloadError: null as string | null,
|
|
104
|
+
// timeReloadError: null as Date | null
|
|
105
|
+
// })
|
|
106
|
+
|
|
107
|
+
// Scenario 3: User has added billing details (reload enabled)
|
|
108
|
+
// const balanceInfo = () => ({
|
|
109
|
+
// balance: 750000000, // $7.50
|
|
110
|
+
// paymentMethodType: "card",
|
|
111
|
+
// paymentMethodLast4: "4242",
|
|
112
|
+
// reload: true,
|
|
113
|
+
// reloadError: null as string | null,
|
|
114
|
+
// timeReloadError: null as Date | null
|
|
115
|
+
// })
|
|
116
|
+
|
|
117
|
+
// Scenario 4: User has billing details but reload failed
|
|
118
|
+
// const balanceInfo = () => ({
|
|
119
|
+
// balance: 250000000, // $2.50
|
|
120
|
+
// paymentMethodType: "card",
|
|
121
|
+
// paymentMethodLast4: "4242",
|
|
122
|
+
// reload: true,
|
|
123
|
+
// reloadError: "Your card was declined." as string,
|
|
124
|
+
// timeReloadError: new Date(Date.now() - 3600000) as Date // 1 hour ago
|
|
125
|
+
// })
|
|
126
|
+
|
|
127
|
+
// Scenario 5: User has Link payment method
|
|
128
|
+
// const balanceInfo = () => ({
|
|
129
|
+
// balance: 500000000, // $5.00
|
|
130
|
+
// paymentMethodType: "link",
|
|
131
|
+
// paymentMethodLast4: null as string | null,
|
|
132
|
+
// reload: true,
|
|
133
|
+
// reloadError: null as string | null,
|
|
134
|
+
// timeReloadError: null as Date | null
|
|
135
|
+
// })
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<section class={styles.root}>
|
|
139
|
+
<div data-slot="section-title">
|
|
140
|
+
<h2>Billing</h2>
|
|
141
|
+
<p>
|
|
142
|
+
Manage payments methods. <a href="mailto:contact@anoma.ly">Contact us</a> if you have any questions.
|
|
143
|
+
</p>
|
|
144
|
+
</div>
|
|
145
|
+
<div data-slot="section-content">
|
|
146
|
+
<div data-slot="balance-display">
|
|
147
|
+
<div data-slot="balance-amount">
|
|
148
|
+
<span data-slot="balance-value">${balance()}</span>
|
|
149
|
+
<span data-slot="balance-label">Current Balance</span>
|
|
150
|
+
</div>
|
|
151
|
+
<Show when={billingInfo()?.customerID}>
|
|
152
|
+
<div data-slot="balance-right-section">
|
|
153
|
+
<Show
|
|
154
|
+
when={!store.showAddBalanceForm}
|
|
155
|
+
fallback={
|
|
156
|
+
<div data-slot="add-balance-form-container">
|
|
157
|
+
<div data-slot="add-balance-form">
|
|
158
|
+
<label>Add $</label>
|
|
159
|
+
<input
|
|
160
|
+
data-component="input"
|
|
161
|
+
type="number"
|
|
162
|
+
min={billingInfo()?.reloadAmountMin.toString()}
|
|
163
|
+
step="1"
|
|
164
|
+
value={store.addBalanceAmount}
|
|
165
|
+
onInput={(e) => {
|
|
166
|
+
setStore("addBalanceAmount", e.currentTarget.value)
|
|
167
|
+
checkoutSubmission.clear()
|
|
168
|
+
}}
|
|
169
|
+
placeholder="Enter amount"
|
|
170
|
+
/>
|
|
171
|
+
<div data-slot="form-actions">
|
|
172
|
+
<button data-color="ghost" type="button" onClick={() => hideAddBalanceForm()}>
|
|
173
|
+
Cancel
|
|
174
|
+
</button>
|
|
175
|
+
<button
|
|
176
|
+
data-color="primary"
|
|
177
|
+
type="button"
|
|
178
|
+
disabled={!store.addBalanceAmount || checkoutSubmission.pending || store.checkoutRedirecting}
|
|
179
|
+
onClick={onClickCheckout}
|
|
180
|
+
>
|
|
181
|
+
{checkoutSubmission.pending || store.checkoutRedirecting ? "Loading..." : "Add"}
|
|
182
|
+
</button>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
<Show when={checkoutSubmission.result && (checkoutSubmission.result as any).error}>
|
|
186
|
+
{(err: any) => <div data-slot="form-error">{err()}</div>}
|
|
187
|
+
</Show>
|
|
188
|
+
</div>
|
|
189
|
+
}
|
|
190
|
+
>
|
|
191
|
+
<button data-color="primary" onClick={() => showAddBalanceForm()}>
|
|
192
|
+
Add Balance
|
|
193
|
+
</button>
|
|
194
|
+
</Show>
|
|
195
|
+
<div data-slot="credit-card">
|
|
196
|
+
<div data-slot="card-icon">
|
|
197
|
+
<Switch fallback={<IconCreditCard style={{ width: "24px", height: "24px" }} />}>
|
|
198
|
+
<Match when={billingInfo()?.paymentMethodType === "link"}>
|
|
199
|
+
<IconStripe style={{ width: "24px", height: "24px" }} />
|
|
200
|
+
</Match>
|
|
201
|
+
</Switch>
|
|
202
|
+
</div>
|
|
203
|
+
<div data-slot="card-details">
|
|
204
|
+
<Switch>
|
|
205
|
+
<Match when={billingInfo()?.paymentMethodType === "card"}>
|
|
206
|
+
<Show when={billingInfo()?.paymentMethodLast4} fallback={<span data-slot="number">----</span>}>
|
|
207
|
+
<span data-slot="secret">••••</span>
|
|
208
|
+
<span data-slot="number">{billingInfo()?.paymentMethodLast4}</span>
|
|
209
|
+
</Show>
|
|
210
|
+
</Match>
|
|
211
|
+
<Match when={billingInfo()?.paymentMethodType === "link"}>
|
|
212
|
+
<span data-slot="type">Linked to Stripe</span>
|
|
213
|
+
</Match>
|
|
214
|
+
</Switch>
|
|
215
|
+
</div>
|
|
216
|
+
<button
|
|
217
|
+
data-color="ghost"
|
|
218
|
+
disabled={sessionSubmission.pending || store.sessionRedirecting}
|
|
219
|
+
onClick={onClickSession}
|
|
220
|
+
>
|
|
221
|
+
{sessionSubmission.pending || store.sessionRedirecting ? "Loading..." : "Manage"}
|
|
222
|
+
</button>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</Show>
|
|
226
|
+
</div>
|
|
227
|
+
<Show when={!billingInfo()?.customerID}>
|
|
228
|
+
<button
|
|
229
|
+
data-slot="enable-billing-button"
|
|
230
|
+
data-color="primary"
|
|
231
|
+
disabled={checkoutSubmission.pending || store.checkoutRedirecting}
|
|
232
|
+
onClick={onClickCheckout}
|
|
233
|
+
>
|
|
234
|
+
{checkoutSubmission.pending || store.checkoutRedirecting ? "Loading..." : "Enable Billing"}
|
|
235
|
+
</button>
|
|
236
|
+
</Show>
|
|
237
|
+
</div>
|
|
238
|
+
</section>
|
|
239
|
+
)
|
|
240
|
+
}
|