@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.
Files changed (217) hide show
  1. package/.opencode/agent/css.md +149 -0
  2. package/README.md +32 -0
  3. package/package.json +49 -0
  4. package/public/apple-touch-icon-v3.png +1 -0
  5. package/public/apple-touch-icon.png +1 -0
  6. package/public/email +1 -0
  7. package/public/favicon-96x96-v3.png +1 -0
  8. package/public/favicon-96x96.png +1 -0
  9. package/public/favicon-v3.ico +1 -0
  10. package/public/favicon-v3.svg +1 -0
  11. package/public/favicon.ico +1 -0
  12. package/public/favicon.svg +1 -0
  13. package/public/opencode-brand-assets.zip +0 -0
  14. package/public/robots.txt +6 -0
  15. package/public/site.webmanifest +1 -0
  16. package/public/social-share-black.png +1 -0
  17. package/public/social-share-zen.png +1 -0
  18. package/public/social-share.png +1 -0
  19. package/public/theme.json +182 -0
  20. package/public/web-app-manifest-192x192.png +1 -0
  21. package/public/web-app-manifest-512x512.png +1 -0
  22. package/script/generate-sitemap.ts +103 -0
  23. package/src/app.css +1 -0
  24. package/src/app.tsx +27 -0
  25. package/src/asset/black/hero.png +0 -0
  26. package/src/asset/brand/opencode-brand-assets.zip +0 -0
  27. package/src/asset/brand/opencode-logo-dark.png +0 -0
  28. package/src/asset/brand/opencode-logo-dark.svg +16 -0
  29. package/src/asset/brand/opencode-logo-light.png +0 -0
  30. package/src/asset/brand/opencode-logo-light.svg +16 -0
  31. package/src/asset/brand/opencode-wordmark-dark.png +0 -0
  32. package/src/asset/brand/opencode-wordmark-dark.svg +30 -0
  33. package/src/asset/brand/opencode-wordmark-light.png +0 -0
  34. package/src/asset/brand/opencode-wordmark-light.svg +30 -0
  35. package/src/asset/brand/opencode-wordmark-simple-dark.png +0 -0
  36. package/src/asset/brand/opencode-wordmark-simple-dark.svg +22 -0
  37. package/src/asset/brand/opencode-wordmark-simple-light.png +0 -0
  38. package/src/asset/brand/opencode-wordmark-simple-light.svg +22 -0
  39. package/src/asset/brand/preview-opencode-dark.png +0 -0
  40. package/src/asset/brand/preview-opencode-logo-dark.png +0 -0
  41. package/src/asset/brand/preview-opencode-logo-light.png +0 -0
  42. package/src/asset/brand/preview-opencode-wordmark-dark.png +0 -0
  43. package/src/asset/brand/preview-opencode-wordmark-light.png +0 -0
  44. package/src/asset/brand/preview-opencode-wordmark-simple-dark.png +0 -0
  45. package/src/asset/brand/preview-opencode-wordmark-simple-light.png +0 -0
  46. package/src/asset/lander/avatar-adam.png +0 -0
  47. package/src/asset/lander/avatar-david.png +0 -0
  48. package/src/asset/lander/avatar-dax.png +0 -0
  49. package/src/asset/lander/avatar-frank.png +0 -0
  50. package/src/asset/lander/avatar-jay.png +0 -0
  51. package/src/asset/lander/brand-assets-dark.svg +10 -0
  52. package/src/asset/lander/brand-assets-light.svg +10 -0
  53. package/src/asset/lander/brand.png +0 -0
  54. package/src/asset/lander/check.svg +3 -0
  55. package/src/asset/lander/copy.svg +3 -0
  56. package/src/asset/lander/desktop-app-icon.png +0 -0
  57. package/src/asset/lander/dock.png +0 -0
  58. package/src/asset/lander/logo-dark.svg +11 -0
  59. package/src/asset/lander/logo-light.svg +11 -0
  60. package/src/asset/lander/opencode-comparison-min.mp4 +0 -0
  61. package/src/asset/lander/opencode-comparison-poster.png +0 -0
  62. package/src/asset/lander/opencode-desktop-icon.png +0 -0
  63. package/src/asset/lander/opencode-logo-dark.svg +11 -0
  64. package/src/asset/lander/opencode-logo-light.svg +11 -0
  65. package/src/asset/lander/opencode-min.mp4 +0 -0
  66. package/src/asset/lander/opencode-poster.png +0 -0
  67. package/src/asset/lander/opencode-wordmark-dark.svg +25 -0
  68. package/src/asset/lander/opencode-wordmark-light.svg +25 -0
  69. package/src/asset/lander/screenshot-github.png +0 -0
  70. package/src/asset/lander/screenshot-splash.png +0 -0
  71. package/src/asset/lander/screenshot-vscode.png +0 -0
  72. package/src/asset/lander/screenshot.png +0 -0
  73. package/src/asset/lander/wordmark-dark.svg +3 -0
  74. package/src/asset/lander/wordmark-light.svg +3 -0
  75. package/src/asset/logo-ornate-dark.svg +18 -0
  76. package/src/asset/logo-ornate-light.svg +18 -0
  77. package/src/asset/logo.svg +18 -0
  78. package/src/asset/zen-ornate-dark.svg +8 -0
  79. package/src/asset/zen-ornate-light.svg +8 -0
  80. package/src/component/dropdown.css +80 -0
  81. package/src/component/dropdown.tsx +79 -0
  82. package/src/component/email-signup.tsx +48 -0
  83. package/src/component/faq.tsx +33 -0
  84. package/src/component/footer.tsx +38 -0
  85. package/src/component/header-context-menu.css +63 -0
  86. package/src/component/header.tsx +279 -0
  87. package/src/component/icon.tsx +257 -0
  88. package/src/component/legal.tsx +20 -0
  89. package/src/component/modal.css +66 -0
  90. package/src/component/modal.tsx +24 -0
  91. package/src/component/spotlight.css +15 -0
  92. package/src/component/spotlight.tsx +820 -0
  93. package/src/config.ts +29 -0
  94. package/src/context/auth.session.ts +0 -0
  95. package/src/context/auth.ts +116 -0
  96. package/src/context/auth.withActor.ts +7 -0
  97. package/src/entry-client.tsx +4 -0
  98. package/src/entry-server.tsx +30 -0
  99. package/src/global.d.ts +5 -0
  100. package/src/lib/github.ts +38 -0
  101. package/src/middleware.ts +5 -0
  102. package/src/routes/[...404].css +130 -0
  103. package/src/routes/[...404].tsx +38 -0
  104. package/src/routes/api/enterprise.ts +47 -0
  105. package/src/routes/auth/[...callback].ts +41 -0
  106. package/src/routes/auth/authorize.ts +10 -0
  107. package/src/routes/auth/index.ts +12 -0
  108. package/src/routes/auth/logout.ts +17 -0
  109. package/src/routes/auth/status.ts +7 -0
  110. package/src/routes/bench/[id].tsx +365 -0
  111. package/src/routes/bench/index.tsx +86 -0
  112. package/src/routes/bench/submission.ts +29 -0
  113. package/src/routes/black/common.tsx +62 -0
  114. package/src/routes/black/index.tsx +108 -0
  115. package/src/routes/black/subscribe/[plan].tsx +449 -0
  116. package/src/routes/black/workspace.css +214 -0
  117. package/src/routes/black/workspace.tsx +229 -0
  118. package/src/routes/black.css +828 -0
  119. package/src/routes/black.tsx +285 -0
  120. package/src/routes/brand/index.css +555 -0
  121. package/src/routes/brand/index.tsx +252 -0
  122. package/src/routes/changelog/index.css +477 -0
  123. package/src/routes/changelog/index.tsx +147 -0
  124. package/src/routes/debug/index.ts +13 -0
  125. package/src/routes/desktop-feedback.ts +5 -0
  126. package/src/routes/discord.ts +5 -0
  127. package/src/routes/docs/[...path].ts +20 -0
  128. package/src/routes/docs/index.ts +20 -0
  129. package/src/routes/download/[platform].ts +38 -0
  130. package/src/routes/download/index.css +750 -0
  131. package/src/routes/download/index.tsx +482 -0
  132. package/src/routes/download/types.ts +4 -0
  133. package/src/routes/enterprise/index.css +578 -0
  134. package/src/routes/enterprise/index.tsx +251 -0
  135. package/src/routes/index.css +1251 -0
  136. package/src/routes/index.tsx +840 -0
  137. package/src/routes/legal/privacy-policy/index.css +343 -0
  138. package/src/routes/legal/privacy-policy/index.tsx +1512 -0
  139. package/src/routes/legal/terms-of-service/index.css +254 -0
  140. package/src/routes/legal/terms-of-service/index.tsx +512 -0
  141. package/src/routes/openapi.json.ts +7 -0
  142. package/src/routes/s/[id].ts +20 -0
  143. package/src/routes/stripe/webhook.ts +532 -0
  144. package/src/routes/t/[...path].tsx +20 -0
  145. package/src/routes/temp.tsx +172 -0
  146. package/src/routes/user-menu.css +18 -0
  147. package/src/routes/user-menu.tsx +32 -0
  148. package/src/routes/workspace/[id]/billing/billing-section.module.css +185 -0
  149. package/src/routes/workspace/[id]/billing/billing-section.tsx +240 -0
  150. package/src/routes/workspace/[id]/billing/black-section.module.css +142 -0
  151. package/src/routes/workspace/[id]/billing/black-section.tsx +269 -0
  152. package/src/routes/workspace/[id]/billing/black-waitlist-section.module.css +23 -0
  153. package/src/routes/workspace/[id]/billing/index.tsx +32 -0
  154. package/src/routes/workspace/[id]/billing/monthly-limit-section.module.css +96 -0
  155. package/src/routes/workspace/[id]/billing/monthly-limit-section.tsx +133 -0
  156. package/src/routes/workspace/[id]/billing/payment-section.module.css +93 -0
  157. package/src/routes/workspace/[id]/billing/payment-section.tsx +122 -0
  158. package/src/routes/workspace/[id]/billing/reload-section.module.css +261 -0
  159. package/src/routes/workspace/[id]/billing/reload-section.tsx +213 -0
  160. package/src/routes/workspace/[id]/graph-section.module.css +145 -0
  161. package/src/routes/workspace/[id]/graph-section.tsx +475 -0
  162. package/src/routes/workspace/[id]/index.tsx +81 -0
  163. package/src/routes/workspace/[id]/keys/index.tsx +11 -0
  164. package/src/routes/workspace/[id]/keys/key-section.module.css +197 -0
  165. package/src/routes/workspace/[id]/keys/key-section.tsx +176 -0
  166. package/src/routes/workspace/[id]/members/index.tsx +11 -0
  167. package/src/routes/workspace/[id]/members/member-section.module.css +249 -0
  168. package/src/routes/workspace/[id]/members/member-section.tsx +343 -0
  169. package/src/routes/workspace/[id]/members/role-dropdown.css +72 -0
  170. package/src/routes/workspace/[id]/members/role-dropdown.tsx +43 -0
  171. package/src/routes/workspace/[id]/model-section.module.css +173 -0
  172. package/src/routes/workspace/[id]/model-section.tsx +174 -0
  173. package/src/routes/workspace/[id]/new-user-section.module.css +143 -0
  174. package/src/routes/workspace/[id]/new-user-section.tsx +104 -0
  175. package/src/routes/workspace/[id]/provider-section.module.css +138 -0
  176. package/src/routes/workspace/[id]/provider-section.tsx +188 -0
  177. package/src/routes/workspace/[id]/settings/index.tsx +11 -0
  178. package/src/routes/workspace/[id]/settings/settings-section.module.css +94 -0
  179. package/src/routes/workspace/[id]/settings/settings-section.tsx +122 -0
  180. package/src/routes/workspace/[id]/usage-section.module.css +185 -0
  181. package/src/routes/workspace/[id]/usage-section.tsx +200 -0
  182. package/src/routes/workspace/[id].css +308 -0
  183. package/src/routes/workspace/[id].tsx +62 -0
  184. package/src/routes/workspace/common.tsx +120 -0
  185. package/src/routes/workspace-picker.css +74 -0
  186. package/src/routes/workspace-picker.tsx +122 -0
  187. package/src/routes/workspace.css +107 -0
  188. package/src/routes/workspace.tsx +38 -0
  189. package/src/routes/zen/index.css +866 -0
  190. package/src/routes/zen/index.tsx +343 -0
  191. package/src/routes/zen/util/dataDumper.ts +44 -0
  192. package/src/routes/zen/util/error.ts +13 -0
  193. package/src/routes/zen/util/handler.ts +784 -0
  194. package/src/routes/zen/util/logger.ts +12 -0
  195. package/src/routes/zen/util/provider/anthropic.ts +752 -0
  196. package/src/routes/zen/util/provider/google.ts +75 -0
  197. package/src/routes/zen/util/provider/openai-compatible.ts +546 -0
  198. package/src/routes/zen/util/provider/openai.ts +630 -0
  199. package/src/routes/zen/util/provider/provider.ts +210 -0
  200. package/src/routes/zen/util/rateLimiter.ts +41 -0
  201. package/src/routes/zen/util/stickyProviderTracker.ts +16 -0
  202. package/src/routes/zen/util/trialLimiter.ts +49 -0
  203. package/src/routes/zen/v1/chat/completions.ts +11 -0
  204. package/src/routes/zen/v1/messages.ts +11 -0
  205. package/src/routes/zen/v1/models/[model].ts +13 -0
  206. package/src/routes/zen/v1/models.ts +60 -0
  207. package/src/routes/zen/v1/responses.ts +11 -0
  208. package/src/style/base.css +21 -0
  209. package/src/style/component/button.css +102 -0
  210. package/src/style/index.css +8 -0
  211. package/src/style/reset.css +76 -0
  212. package/src/style/token/color.css +91 -0
  213. package/src/style/token/font.css +21 -0
  214. package/src/style/token/space.css +46 -0
  215. package/sst-env.d.ts +9 -0
  216. package/tsconfig.json +21 -0
  217. 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
+ }