billing-section.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. import { json, query, action, useParams, useAction, createAsync, useSubmission } from "@solidjs/router"
  2. import { createMemo, Show } from "solid-js"
  3. import { Billing } from "@opencode/cloud-core/billing.js"
  4. import { withActor } from "~/context/auth.withActor"
  5. import { IconCreditCard } from "~/component/icon"
  6. import styles from "./billing-section.module.css"
  7. const createCheckoutUrl = action(async (workspaceID: string, successUrl: string, cancelUrl: string) => {
  8. "use server"
  9. return withActor(() => Billing.generateCheckoutUrl({ successUrl, cancelUrl }), workspaceID)
  10. }, "checkoutUrl")
  11. const reload = action(async (form: FormData) => {
  12. "use server"
  13. const workspaceID = form.get("workspaceID")?.toString()
  14. if (!workspaceID) return { error: "Workspace ID is required" }
  15. return json(await withActor(() => Billing.reload(), workspaceID), { revalidate: getBillingInfo.key })
  16. }, "billing.reload")
  17. const disableReload = action(async (form: FormData) => {
  18. "use server"
  19. const workspaceID = form.get("workspaceID")?.toString()
  20. if (!workspaceID) return { error: "Workspace ID is required" }
  21. return json(await withActor(() => Billing.disableReload(), workspaceID), { revalidate: getBillingInfo.key })
  22. }, "billing.disableReload")
  23. const createSessionUrl = action(async (workspaceID: string, returnUrl: string) => {
  24. "use server"
  25. return withActor(() => Billing.generateSessionUrl({ returnUrl }), workspaceID)
  26. }, "sessionUrl")
  27. const getBillingInfo = query(async (workspaceID: string) => {
  28. "use server"
  29. return withActor(async () => {
  30. return await Billing.get()
  31. }, workspaceID)
  32. }, "billing.get")
  33. export function BillingSection() {
  34. const params = useParams()
  35. const balanceInfo = createAsync(() => getBillingInfo(params.id))
  36. const createCheckoutUrlAction = useAction(createCheckoutUrl)
  37. const createCheckoutUrlSubmission = useSubmission(createCheckoutUrl)
  38. const createSessionUrlAction = useAction(createSessionUrl)
  39. const createSessionUrlSubmission = useSubmission(createSessionUrl)
  40. const disableReloadSubmission = useSubmission(disableReload)
  41. const reloadSubmission = useSubmission(reload)
  42. const balanceAmount = createMemo(() => {
  43. return ((balanceInfo()?.balance ?? 0) / 100000000).toFixed(2)
  44. })
  45. return (
  46. <section class={styles.root}>
  47. <div data-slot="section-title">
  48. <h2>Billing</h2>
  49. <p>
  50. Manage payments methods. <a href="mailto:[email protected]">Contact us</a> if you have any questions.
  51. </p>
  52. </div>
  53. <div data-slot="section-content">
  54. <Show when={balanceInfo()?.reloadError}>
  55. <div data-slot="reload-error">
  56. <p>
  57. Reload failed at{" "}
  58. {balanceInfo()?.timeReloadError!.toLocaleString("en-US", {
  59. month: "short",
  60. day: "numeric",
  61. hour: "numeric",
  62. minute: "2-digit",
  63. second: "2-digit",
  64. })}
  65. . Reason: {balanceInfo()?.reloadError?.replace(/\.$/, "")}. Please update your payment method and try
  66. again.
  67. </p>
  68. <form action={reload} method="post" data-slot="create-form">
  69. <input type="hidden" name="workspaceID" value={params.id} />
  70. <button data-color="primary" type="submit" disabled={reloadSubmission.pending}>
  71. {reloadSubmission.pending ? "Reloading..." : "Reload"}
  72. </button>
  73. </form>
  74. </div>
  75. </Show>
  76. <div data-slot="payment">
  77. <div data-slot="credit-card">
  78. <div data-slot="card-icon">
  79. <IconCreditCard style={{ width: "32px", height: "32px" }} />
  80. </div>
  81. <div data-slot="card-details">
  82. <Show when={balanceInfo()?.paymentMethodLast4} fallback={<span data-slot="number">----</span>}>
  83. <span data-slot="secret">••••</span>
  84. <span data-slot="number">{balanceInfo()?.paymentMethodLast4}</span>
  85. </Show>
  86. </div>
  87. </div>
  88. <div data-slot="button-row">
  89. <Show
  90. when={balanceInfo()?.reload}
  91. fallback={
  92. <button
  93. data-color="primary"
  94. disabled={createCheckoutUrlSubmission.pending}
  95. onClick={async () => {
  96. const baseUrl = window.location.href
  97. const checkoutUrl = await createCheckoutUrlAction(params.id, baseUrl, baseUrl)
  98. if (checkoutUrl) {
  99. window.location.href = checkoutUrl
  100. }
  101. }}
  102. >
  103. {createCheckoutUrlSubmission.pending ? "Loading..." : "Enable Billing"}
  104. </button>
  105. }
  106. >
  107. <button
  108. data-color="primary"
  109. disabled={createSessionUrlSubmission.pending}
  110. onClick={async () => {
  111. const baseUrl = window.location.href
  112. const sessionUrl = await createSessionUrlAction(params.id, baseUrl)
  113. if (sessionUrl) {
  114. window.location.href = sessionUrl
  115. }
  116. }}
  117. >
  118. {createSessionUrlSubmission.pending ? "Loading..." : "Manage Payment Methods"}
  119. </button>
  120. <form action={disableReload} method="post" data-slot="create-form">
  121. <input type="hidden" name="workspaceID" value={params.id} />
  122. <button data-color="ghost" type="submit" disabled={disableReloadSubmission.pending}>
  123. {disableReloadSubmission.pending ? "Disabling..." : "Disable"}
  124. </button>
  125. </form>
  126. </Show>
  127. </div>
  128. </div>
  129. <div data-slot="usage">
  130. <Show when={!balanceInfo()?.reload && !(balanceAmount() === "0.00" || balanceAmount() === "-0.00")}>
  131. <p>
  132. You have <b data-slot="value">${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()}</b> remaining in
  133. your account. You can continue using the API with your remaining balance.
  134. </p>
  135. </Show>
  136. <Show when={balanceInfo()?.reload && !balanceInfo()?.reloadError}>
  137. <p>
  138. Your current balance is <b data-slot="value">${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()}</b>
  139. . We'll automatically reload <b>$20</b> (+$1.23 processing fee) when it reaches <b>$5</b>.
  140. </p>
  141. </Show>
  142. </div>
  143. </div>
  144. </section>
  145. )
  146. }