mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-13 15:44:56 +00:00
feat(console): add Go referral UI
This commit is contained in:
330
packages/console/app/src/component/go-referral.css
Normal file
330
packages/console/app/src/component/go-referral.css
Normal file
@@ -0,0 +1,330 @@
|
||||
[data-component="go-referral-banner"],
|
||||
[data-component="go-credit-apply-card"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-4);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background-color: var(--color-bg-surface);
|
||||
|
||||
@media (max-width: 40rem) {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
strong {
|
||||
display: block;
|
||||
margin-bottom: var(--space-1);
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="go-referral-banner"],
|
||||
[data-component="go-credit-apply-card"],
|
||||
[data-component="go-referral-modal"],
|
||||
[data-component="go-credit-confirm"],
|
||||
[data-component="go-referral-section"] {
|
||||
a[data-color],
|
||||
button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--color-surface-hover);
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
&[data-color="primary"] {
|
||||
background-color: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary-text);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--color-primary-hover);
|
||||
border-color: var(--color-primary-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="go-referral-modal"],
|
||||
[data-component="go-credit-confirm"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
min-width: min(34rem, calc(100vw - var(--space-8)));
|
||||
|
||||
[data-slot="loading"] {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
[data-slot="modal-actions"] {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="invite-link-box"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
|
||||
label {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
|
||||
@media (max-width: 40rem) {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: var(--space-3);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="referral-progress"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
[data-slot="progress-header"] {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-sm);
|
||||
|
||||
span {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="progress-track"] {
|
||||
height: 8px;
|
||||
overflow: hidden;
|
||||
border-radius: var(--border-radius-sm);
|
||||
background-color: var(--color-bg-surface);
|
||||
}
|
||||
|
||||
[data-slot="progress-fill"] {
|
||||
height: 100%;
|
||||
border-radius: var(--border-radius-sm);
|
||||
background-color: var(--color-accent);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
[data-slot="milestones"] {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--space-2);
|
||||
|
||||
@media (max-width: 40rem) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="milestone"] {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-3);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
|
||||
&[data-status="unlocked"] {
|
||||
border-color: var(--color-accent);
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-bg);
|
||||
}
|
||||
|
||||
strong {
|
||||
color: var(--color-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="next-reward"] {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
[data-slot="instructions"] {
|
||||
padding: var(--space-4);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background-color: var(--color-bg);
|
||||
|
||||
strong {
|
||||
display: block;
|
||||
margin-bottom: var(--space-3);
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
ol {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
margin: 0;
|
||||
padding: 0 0 0 1.35rem;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
li::marker {
|
||||
color: var(--color-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="go-referral-section"] {
|
||||
[data-slot="credit-summary"] {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
|
||||
@media (max-width: 40rem) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-4);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background-color: var(--color-bg-surface);
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
strong {
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="credit-list"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
[data-slot="credit-row"] {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(9rem, 1fr) minmax(14rem, 2fr) auto;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
grid-template-columns: 1fr;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="credit-main"],
|
||||
[data-slot="credit-meta"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
[data-slot="credit-main"] {
|
||||
strong {
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="credit-meta"] {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="go-credit-confirm"] {
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
420
packages/console/app/src/component/go-referral.tsx
Normal file
420
packages/console/app/src/component/go-referral.tsx
Normal file
@@ -0,0 +1,420 @@
|
||||
import { A, action, json, query, useAction, useSubmission } from "@solidjs/router"
|
||||
import { createMemo, createSignal, For, Show } from "solid-js"
|
||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||
import { withActor } from "~/context/auth.withActor"
|
||||
import { config } from "~/config"
|
||||
import { Modal } from "~/component/modal"
|
||||
import { useI18n } from "~/context/i18n"
|
||||
import { useLanguage } from "~/context/language"
|
||||
import "./go-referral.css"
|
||||
|
||||
type CreditStatus = "pending" | "available" | "used" | "expired" | "revoked"
|
||||
|
||||
export type GoReferralCredit = {
|
||||
id: string
|
||||
amount: number
|
||||
remaining: number
|
||||
status: CreditStatus
|
||||
source: "referral" | "invitee"
|
||||
issuedAt: string
|
||||
availableAt: string
|
||||
expiresAt: string
|
||||
usedAt?: string
|
||||
}
|
||||
|
||||
export type GoReferralMilestone = {
|
||||
inviteCount: number
|
||||
amount: number
|
||||
status: "unlocked" | "locked"
|
||||
}
|
||||
|
||||
export type GoReferralSummary = {
|
||||
inviteCode: string
|
||||
inviteUrl: string
|
||||
validInviteCount: number
|
||||
maxInviteCount: number
|
||||
nextInviteCount?: number
|
||||
nextReward?: number
|
||||
invitedUserReward: number
|
||||
maxReward: number
|
||||
creditAvailable: number
|
||||
creditPending: number
|
||||
creditUsed: number
|
||||
workspaceMonthlyUsage: number
|
||||
milestones: GoReferralMilestone[]
|
||||
credits: GoReferralCredit[]
|
||||
}
|
||||
|
||||
export const queryGoReferral = query(async (workspaceID: string) => {
|
||||
"use server"
|
||||
return withActor(() => {
|
||||
const account = Actor.account()
|
||||
const code = account.replace(/[^a-zA-Z0-9]/g, "").slice(-8).toUpperCase()
|
||||
const now = Date.now()
|
||||
const day = 24 * 60 * 60 * 1000
|
||||
const credits: GoReferralCredit[] = [
|
||||
{
|
||||
id: "demo-referral-1",
|
||||
amount: 1000,
|
||||
remaining: 1000,
|
||||
status: "available",
|
||||
source: "referral",
|
||||
issuedAt: new Date(now - 8 * day).toISOString(),
|
||||
availableAt: new Date(now - 7 * day).toISOString(),
|
||||
expiresAt: new Date(now + 82 * day).toISOString(),
|
||||
},
|
||||
{
|
||||
id: "demo-invitee-1",
|
||||
amount: 1000,
|
||||
remaining: 1000,
|
||||
status: "pending",
|
||||
source: "invitee",
|
||||
issuedAt: new Date(now - 6 * 60 * 60 * 1000).toISOString(),
|
||||
availableAt: new Date(now + 18 * 60 * 60 * 1000).toISOString(),
|
||||
expiresAt: new Date(now + 90 * day).toISOString(),
|
||||
},
|
||||
{
|
||||
id: "demo-referral-used",
|
||||
amount: 500,
|
||||
remaining: 0,
|
||||
status: "used",
|
||||
source: "referral",
|
||||
issuedAt: new Date(now - 30 * day).toISOString(),
|
||||
availableAt: new Date(now - 29 * day).toISOString(),
|
||||
expiresAt: new Date(now + 60 * day).toISOString(),
|
||||
usedAt: new Date(now - 3 * day).toISOString(),
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
inviteCode: code,
|
||||
inviteUrl: `${config.baseUrl}/go?invite=${code}`,
|
||||
validInviteCount: 2,
|
||||
maxInviteCount: 5,
|
||||
nextInviteCount: 3,
|
||||
nextReward: 2000,
|
||||
invitedUserReward: 1000,
|
||||
maxReward: 6000,
|
||||
creditAvailable: credits
|
||||
.filter((credit) => credit.status === "available")
|
||||
.reduce((total, credit) => total + credit.remaining, 0),
|
||||
creditPending: credits
|
||||
.filter((credit) => credit.status === "pending")
|
||||
.reduce((total, credit) => total + credit.remaining, 0),
|
||||
creditUsed: credits.filter((credit) => credit.status === "used").reduce((total, credit) => total + credit.amount, 0),
|
||||
workspaceMonthlyUsage: 1240,
|
||||
milestones: [
|
||||
{ inviteCount: 1, amount: 1000, status: "unlocked" },
|
||||
{ inviteCount: 3, amount: 2000, status: "locked" },
|
||||
{ inviteCount: 5, amount: 3000, status: "locked" },
|
||||
],
|
||||
credits,
|
||||
} satisfies GoReferralSummary
|
||||
}, workspaceID)
|
||||
}, "go.referral.get")
|
||||
|
||||
export const applyGoReferralCredit = action(async (workspaceID: string, amount: number, creditID?: string) => {
|
||||
"use server"
|
||||
return json(
|
||||
await withActor(
|
||||
() => ({
|
||||
error: undefined,
|
||||
data: { amount, creditID },
|
||||
}),
|
||||
workspaceID,
|
||||
),
|
||||
{ revalidate: [queryGoReferral.key] },
|
||||
)
|
||||
}, "go.referral.credit.apply")
|
||||
|
||||
function formatCurrency(amount: number) {
|
||||
if (amount % 100 === 0) return `$${amount / 100}`
|
||||
return `$${(amount / 100).toFixed(2)}`
|
||||
}
|
||||
|
||||
function formatDate(value: string, locale: string) {
|
||||
return new Intl.DateTimeFormat(locale, { month: "short", day: "numeric", year: "numeric" }).format(new Date(value))
|
||||
}
|
||||
|
||||
function creditStatusKey(status: CreditStatus) {
|
||||
if (status === "pending") return "workspace.referral.credit.status.pending" as const
|
||||
if (status === "available") return "workspace.referral.credit.status.available" as const
|
||||
if (status === "used") return "workspace.referral.credit.status.used" as const
|
||||
if (status === "expired") return "workspace.referral.credit.status.expired" as const
|
||||
return "workspace.referral.credit.status.revoked" as const
|
||||
}
|
||||
|
||||
function creditSourceKey(source: GoReferralCredit["source"]) {
|
||||
if (source === "invitee") return "workspace.referral.credit.source.invitee" as const
|
||||
return "workspace.referral.credit.source.referral" as const
|
||||
}
|
||||
|
||||
function milestoneLabel(inviteCount: number, i18n: ReturnType<typeof useI18n>) {
|
||||
if (inviteCount === 1) return i18n.t("workspace.referral.milestone.one")
|
||||
if (inviteCount === 3) return i18n.t("workspace.referral.milestone.three")
|
||||
return i18n.t("workspace.referral.milestone.five")
|
||||
}
|
||||
|
||||
function CopyInviteLink(props: { summary: GoReferralSummary }) {
|
||||
const i18n = useI18n()
|
||||
const [copied, setCopied] = createSignal(false)
|
||||
|
||||
async function copy() {
|
||||
if (typeof navigator !== "object") return
|
||||
await navigator.clipboard.writeText(props.summary.inviteUrl)
|
||||
setCopied(true)
|
||||
window.setTimeout(() => setCopied(false), 1600)
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-slot="invite-link-box">
|
||||
<label>{i18n.t("workspace.referral.inviteLink")}</label>
|
||||
<div>
|
||||
<code>{props.summary.inviteUrl}</code>
|
||||
<button type="button" data-color="primary" onClick={copy}>
|
||||
{copied() ? i18n.t("workspace.referral.copied") : i18n.t("workspace.referral.copyLink")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ReferralProgress(props: { summary: GoReferralSummary }) {
|
||||
const i18n = useI18n()
|
||||
const progress = createMemo(() => Math.min(100, (props.summary.validInviteCount / props.summary.maxInviteCount) * 100))
|
||||
|
||||
return (
|
||||
<div data-slot="referral-progress">
|
||||
<div data-slot="progress-header">
|
||||
<strong>{i18n.t("workspace.referral.progress.title")}</strong>
|
||||
<span>
|
||||
{i18n.t("workspace.referral.progress.value", {
|
||||
count: props.summary.validInviteCount,
|
||||
total: props.summary.maxInviteCount,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div data-slot="progress-track">
|
||||
<div data-slot="progress-fill" style={{ width: `${progress()}%` }} />
|
||||
</div>
|
||||
<div data-slot="milestones">
|
||||
<For each={props.summary.milestones}>
|
||||
{(milestone) => (
|
||||
<div data-slot="milestone" data-status={milestone.status}>
|
||||
<span>{milestoneLabel(milestone.inviteCount, i18n)}</span>
|
||||
<strong>{formatCurrency(milestone.amount)}</strong>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<p data-slot="next-reward">
|
||||
<Show
|
||||
when={props.summary.nextInviteCount && props.summary.nextReward}
|
||||
fallback={i18n.t("workspace.referral.progress.complete")}
|
||||
>
|
||||
{i18n.t("workspace.referral.progress.next", {
|
||||
count: Math.max(0, props.summary.nextInviteCount! - props.summary.validInviteCount),
|
||||
amount: formatCurrency(props.summary.nextReward!),
|
||||
})}
|
||||
</Show>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function GoReferralBanner(props: { href: string }) {
|
||||
const i18n = useI18n()
|
||||
|
||||
return (
|
||||
<div data-component="go-referral-banner">
|
||||
<div>
|
||||
<strong>{i18n.t("workspace.referral.banner.title")}</strong>
|
||||
<p>{i18n.t("workspace.referral.banner.body")}</p>
|
||||
</div>
|
||||
<A href={props.href} data-color="primary">
|
||||
{i18n.t("workspace.referral.banner.action")}
|
||||
</A>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function GoCreditApplyCard(props: { workspaceID: string; summary?: GoReferralSummary }) {
|
||||
const i18n = useI18n()
|
||||
const apply = useAction(applyGoReferralCredit)
|
||||
const submission = useSubmission(applyGoReferralCredit)
|
||||
const [confirming, setConfirming] = createSignal(false)
|
||||
const [applied, setApplied] = createSignal(0)
|
||||
const available = createMemo(() => Math.max(0, (props.summary?.creditAvailable ?? 0) - applied()))
|
||||
const applyAmount = createMemo(() => Math.min(available(), props.summary?.workspaceMonthlyUsage ?? 0))
|
||||
|
||||
async function onApply() {
|
||||
const amount = applyAmount()
|
||||
if (!amount) return
|
||||
const result = await apply(props.workspaceID, amount)
|
||||
if (result.data) {
|
||||
setApplied((value) => value + amount)
|
||||
setConfirming(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-component="go-credit-apply-card">
|
||||
<div>
|
||||
<strong>
|
||||
{i18n.t("workspace.referral.apply.title", { amount: formatCurrency(available()) })}
|
||||
</strong>
|
||||
<p>
|
||||
<Show when={applyAmount() > 0} fallback={i18n.t("workspace.referral.apply.noUsage")}>
|
||||
{i18n.t("workspace.referral.apply.body")}
|
||||
</Show>
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" disabled={!applyAmount()} onClick={() => setConfirming(true)}>
|
||||
{i18n.t("workspace.referral.apply.action")}
|
||||
</button>
|
||||
<Modal open={confirming()} onClose={() => setConfirming(false)} title={i18n.t("workspace.referral.apply.confirmTitle")}>
|
||||
<div data-component="go-credit-confirm">
|
||||
<p>
|
||||
{i18n.t("workspace.referral.apply.confirmBody", {
|
||||
amount: formatCurrency(applyAmount()),
|
||||
usage: formatCurrency(props.summary?.workspaceMonthlyUsage ?? 0),
|
||||
usageAfter: formatCurrency(Math.max(0, (props.summary?.workspaceMonthlyUsage ?? 0) - applyAmount())),
|
||||
})}
|
||||
</p>
|
||||
<div data-slot="modal-actions">
|
||||
<button type="button" onClick={() => setConfirming(false)}>
|
||||
{i18n.t("common.cancel")}
|
||||
</button>
|
||||
<button type="button" data-color="primary" disabled={submission.pending} onClick={onApply}>
|
||||
{submission.pending ? i18n.t("workspace.lite.loading") : i18n.t("workspace.referral.apply.confirmAction")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function GoReferralCredits(props: { workspaceID: string; summary: GoReferralSummary }) {
|
||||
const i18n = useI18n()
|
||||
const language = useLanguage()
|
||||
const apply = useAction(applyGoReferralCredit)
|
||||
const submission = useSubmission(applyGoReferralCredit)
|
||||
const [selected, setSelected] = createSignal<GoReferralCredit>()
|
||||
const [appliedCredits, setAppliedCredits] = createSignal<Record<string, number>>({})
|
||||
|
||||
const creditRemaining = (credit: GoReferralCredit) => Math.max(0, credit.remaining - (appliedCredits()[credit.id] ?? 0))
|
||||
const selectedAmount = createMemo(() => {
|
||||
const credit = selected()
|
||||
if (!credit) return 0
|
||||
return Math.min(creditRemaining(credit), props.summary.workspaceMonthlyUsage)
|
||||
})
|
||||
|
||||
async function onApply() {
|
||||
const credit = selected()
|
||||
const amount = selectedAmount()
|
||||
if (!credit || !amount) return
|
||||
const result = await apply(props.workspaceID, amount, credit.id)
|
||||
if (result.data) {
|
||||
setAppliedCredits((value) => ({ ...value, [credit.id]: (value[credit.id] ?? 0) + amount }))
|
||||
setSelected(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section data-component="go-referral-section">
|
||||
<div data-slot="section-title">
|
||||
<h2>{i18n.t("workspace.referral.credits.title")}</h2>
|
||||
<p>{i18n.t("workspace.referral.credits.subtitle")}</p>
|
||||
</div>
|
||||
<div data-slot="credit-list">
|
||||
<For each={props.summary.credits}>
|
||||
{(credit) => (
|
||||
<div data-slot="credit-row" data-status={credit.status}>
|
||||
<div data-slot="credit-main">
|
||||
<strong>{formatCurrency(creditRemaining(credit) || credit.amount)}</strong>
|
||||
<span>{i18n.t(creditSourceKey(credit.source))}</span>
|
||||
</div>
|
||||
<div data-slot="credit-meta">
|
||||
<span>{i18n.t(creditStatusKey(credit.status))}</span>
|
||||
<span>
|
||||
{credit.status === "pending"
|
||||
? i18n.t("workspace.referral.credit.availableOn", {
|
||||
date: formatDate(credit.availableAt, language.tag(language.locale())),
|
||||
})
|
||||
: i18n.t("workspace.referral.credit.expiresOn", {
|
||||
date: formatDate(credit.expiresAt, language.tag(language.locale())),
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={credit.status !== "available" || creditRemaining(credit) <= 0 || props.summary.workspaceMonthlyUsage <= 0}
|
||||
onClick={() => setSelected(credit)}
|
||||
>
|
||||
{i18n.t("workspace.referral.apply.action")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<Modal open={!!selected()} onClose={() => setSelected(undefined)} title={i18n.t("workspace.referral.apply.confirmTitle")}>
|
||||
<div data-component="go-credit-confirm">
|
||||
<p>
|
||||
<Show when={selectedAmount() > 0} fallback={i18n.t("workspace.referral.apply.noUsage")}>
|
||||
{i18n.t("workspace.referral.apply.confirmBody", {
|
||||
amount: formatCurrency(selectedAmount()),
|
||||
usage: formatCurrency(props.summary.workspaceMonthlyUsage),
|
||||
usageAfter: formatCurrency(Math.max(0, props.summary.workspaceMonthlyUsage - selectedAmount())),
|
||||
})}
|
||||
</Show>
|
||||
</p>
|
||||
<div data-slot="modal-actions">
|
||||
<button type="button" onClick={() => setSelected(undefined)}>
|
||||
{i18n.t("common.cancel")}
|
||||
</button>
|
||||
<button type="button" data-color="primary" disabled={submission.pending || !selectedAmount()} onClick={onApply}>
|
||||
{submission.pending ? i18n.t("workspace.lite.loading") : i18n.t("workspace.referral.apply.confirmAction")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export function GoReferralOverview(props: { summary: GoReferralSummary }) {
|
||||
const i18n = useI18n()
|
||||
|
||||
return (
|
||||
<section data-component="go-referral-section">
|
||||
<div data-slot="section-title">
|
||||
<h2>{i18n.t("workspace.referral.overview.title")}</h2>
|
||||
<p>
|
||||
{i18n.t("workspace.referral.overview.subtitle", {
|
||||
max: formatCurrency(props.summary.maxReward),
|
||||
reward: formatCurrency(props.summary.invitedUserReward),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<InvitationInstructions />
|
||||
<CopyInviteLink summary={props.summary} />
|
||||
<ReferralProgress summary={props.summary} />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export function InvitationInstructions() {
|
||||
const i18n = useI18n()
|
||||
|
||||
return (
|
||||
<div data-slot="instructions">
|
||||
<strong>{i18n.t("workspace.referral.instructions.title")}</strong>
|
||||
<ol>
|
||||
<li>{i18n.t("workspace.referral.instructions.share")}</li>
|
||||
<li>{i18n.t("workspace.referral.instructions.subscribe")}</li>
|
||||
<li>{i18n.t("workspace.referral.instructions.unlock")}</li>
|
||||
</ol>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -258,6 +258,9 @@ export const dict = {
|
||||
"go.cta.text": "Subscribe to Go",
|
||||
"go.cta.price": "$10/month",
|
||||
"go.cta.promo": "$5 first month",
|
||||
"go.referral.title": "Invite friends",
|
||||
"go.referral.link": "Invite",
|
||||
"go.referral.note": "Friends receive $10 Go credit after subscribing. You can earn up to $60 in Go credit.",
|
||||
"go.pricing.body": "Use with any agent. $5 first month, then $10/month. Top up credit if needed. Cancel any time.",
|
||||
"go.graph.free": "Free",
|
||||
"go.graph.freePill": "Big Pickle and free models",
|
||||
@@ -420,6 +423,7 @@ export const dict = {
|
||||
|
||||
"workspace.nav.zen": "Zen",
|
||||
"workspace.nav.go": "Go",
|
||||
"workspace.nav.invite": "Invite",
|
||||
"workspace.nav.usage": "Usage",
|
||||
"workspace.nav.apiKeys": "API Keys",
|
||||
"workspace.nav.members": "Members",
|
||||
@@ -662,6 +666,49 @@ export const dict = {
|
||||
"workspace.lite.promo.otherMethods": "Other payment methods",
|
||||
"workspace.lite.promo.selectMethod": "Select payment method",
|
||||
|
||||
"workspace.referral.page.subtitle": "Invite friends to Go and manage your usage credits.",
|
||||
"workspace.referral.inviteApplied": "Invite applied. You will receive Go credit after subscribing.",
|
||||
"workspace.referral.banner.title": "Invite friends to Go",
|
||||
"workspace.referral.banner.body": "Earn up to $60 in Go usage credits when friends subscribe.",
|
||||
"workspace.referral.banner.action": "Invite",
|
||||
"workspace.referral.inviteLink": "Invite link",
|
||||
"workspace.referral.copyLink": "Copy link",
|
||||
"workspace.referral.copied": "Copied",
|
||||
"workspace.referral.overview.title": "Invite friends to Go",
|
||||
"workspace.referral.overview.subtitle": "Friends who subscribe with your link receive {{reward}} Go credit. You can earn up to {{max}}.",
|
||||
"workspace.referral.instructions.title": "Invitation instructions",
|
||||
"workspace.referral.instructions.share": "Share your invite link.",
|
||||
"workspace.referral.instructions.subscribe": "Your friend subscribes to Go and receives $10 Go credit.",
|
||||
"workspace.referral.instructions.unlock": "Unlock up to $60 in Go credit at 1, 3, and 5 valid invites.",
|
||||
"workspace.referral.progress.title": "Progress",
|
||||
"workspace.referral.progress.value": "{{count}} / {{total}} valid invites",
|
||||
"workspace.referral.progress.next": "{{count}} more valid invite(s) to unlock {{amount}}.",
|
||||
"workspace.referral.progress.complete": "All invite rewards unlocked.",
|
||||
"workspace.referral.milestone.one": "1 valid invite",
|
||||
"workspace.referral.milestone.three": "3 valid invites",
|
||||
"workspace.referral.milestone.five": "5 valid invites",
|
||||
"workspace.referral.credits.title": "Go credits",
|
||||
"workspace.referral.credits.subtitle": "Use credits to reduce Go usage.",
|
||||
"workspace.referral.credits.available": "Available",
|
||||
"workspace.referral.credits.pending": "Pending",
|
||||
"workspace.referral.credits.used": "Used",
|
||||
"workspace.referral.credit.source.referral": "Earned by inviting a friend",
|
||||
"workspace.referral.credit.source.invitee": "Received from an invite",
|
||||
"workspace.referral.credit.status.pending": "Pending",
|
||||
"workspace.referral.credit.status.available": "Available",
|
||||
"workspace.referral.credit.status.used": "Used",
|
||||
"workspace.referral.credit.status.expired": "Expired",
|
||||
"workspace.referral.credit.status.revoked": "Revoked",
|
||||
"workspace.referral.credit.availableOn": "Available on {{date}}",
|
||||
"workspace.referral.credit.expiresOn": "Expires on {{date}}",
|
||||
"workspace.referral.apply.title": "{{amount}} Go credit available",
|
||||
"workspace.referral.apply.body": "Apply credits to reduce this workspace's current Go usage.",
|
||||
"workspace.referral.apply.noUsage": "Use Go first, then apply credits to reduce your monthly usage.",
|
||||
"workspace.referral.apply.action": "Apply credit",
|
||||
"workspace.referral.apply.confirmTitle": "Apply Go credit",
|
||||
"workspace.referral.apply.confirmBody": "Apply {{amount}} to current Go usage of {{usage}}. Usage after credit: {{usageAfter}}.",
|
||||
"workspace.referral.apply.confirmAction": "Apply",
|
||||
|
||||
"download.title": "OpenCode | Download",
|
||||
"download.meta.description": "Download OpenCode for macOS, Windows, and Linux",
|
||||
"download.hero.title": "Download OpenCode",
|
||||
|
||||
@@ -251,6 +251,9 @@ export const dict = {
|
||||
"go.cta.text": "订阅 Go",
|
||||
"go.cta.price": "$10/月",
|
||||
"go.cta.promo": "首月 $5",
|
||||
"go.referral.title": "邀请好友",
|
||||
"go.referral.link": "邀请",
|
||||
"go.referral.note": "好友订阅后可获得 $10 Go 用量抵扣。您最多可获得 $60 Go 用量抵扣。",
|
||||
"go.pricing.body": "可配合任何代理使用。首月 $5,之后 $10/月。如有需要可充值。随时取消。",
|
||||
"go.graph.free": "免费",
|
||||
"go.graph.freePill": "Big Pickle 和免费模型",
|
||||
@@ -406,6 +409,7 @@ export const dict = {
|
||||
|
||||
"workspace.nav.zen": "Zen",
|
||||
"workspace.nav.go": "Go",
|
||||
"workspace.nav.invite": "邀请",
|
||||
"workspace.nav.usage": "使用量",
|
||||
"workspace.nav.apiKeys": "API 密钥",
|
||||
"workspace.nav.members": "成员",
|
||||
@@ -643,6 +647,49 @@ export const dict = {
|
||||
"workspace.lite.promo.otherMethods": "其他付款方式",
|
||||
"workspace.lite.promo.selectMethod": "选择付款方式",
|
||||
|
||||
"workspace.referral.page.subtitle": "邀请好友使用 Go,并管理您的用量抵扣。",
|
||||
"workspace.referral.inviteApplied": "已应用邀请。订阅后将获得 Go 用量抵扣。",
|
||||
"workspace.referral.banner.title": "邀请好友使用 Go",
|
||||
"workspace.referral.banner.body": "好友订阅后,您最多可获得 $60 Go 用量抵扣。",
|
||||
"workspace.referral.banner.action": "邀请",
|
||||
"workspace.referral.inviteLink": "邀请链接",
|
||||
"workspace.referral.copyLink": "复制链接",
|
||||
"workspace.referral.copied": "已复制",
|
||||
"workspace.referral.overview.title": "邀请好友使用 Go",
|
||||
"workspace.referral.overview.subtitle": "通过您的链接订阅的好友可获得 {{reward}} Go 用量抵扣。您最多可获得 {{max}}。",
|
||||
"workspace.referral.instructions.title": "邀请说明",
|
||||
"workspace.referral.instructions.share": "分享您的邀请链接。",
|
||||
"workspace.referral.instructions.subscribe": "好友订阅 Go 后获得 $10 Go 用量抵扣。",
|
||||
"workspace.referral.instructions.unlock": "在达到 1、3、5 个有效邀请时,最多解锁 $60 Go 用量抵扣。",
|
||||
"workspace.referral.progress.title": "进度",
|
||||
"workspace.referral.progress.value": "{{count}} / {{total}} 个有效邀请",
|
||||
"workspace.referral.progress.next": "再完成 {{count}} 个有效邀请可解锁 {{amount}}。",
|
||||
"workspace.referral.progress.complete": "已解锁全部邀请奖励。",
|
||||
"workspace.referral.milestone.one": "1 个有效邀请",
|
||||
"workspace.referral.milestone.three": "3 个有效邀请",
|
||||
"workspace.referral.milestone.five": "5 个有效邀请",
|
||||
"workspace.referral.credits.title": "Go 抵扣",
|
||||
"workspace.referral.credits.subtitle": "使用抵扣减少 Go 用量。",
|
||||
"workspace.referral.credits.available": "可用",
|
||||
"workspace.referral.credits.pending": "待生效",
|
||||
"workspace.referral.credits.used": "已使用",
|
||||
"workspace.referral.credit.source.referral": "邀请好友获得",
|
||||
"workspace.referral.credit.source.invitee": "通过邀请获得",
|
||||
"workspace.referral.credit.status.pending": "待生效",
|
||||
"workspace.referral.credit.status.available": "可用",
|
||||
"workspace.referral.credit.status.used": "已使用",
|
||||
"workspace.referral.credit.status.expired": "已过期",
|
||||
"workspace.referral.credit.status.revoked": "已撤销",
|
||||
"workspace.referral.credit.availableOn": "{{date}} 可用",
|
||||
"workspace.referral.credit.expiresOn": "{{date}} 过期",
|
||||
"workspace.referral.apply.title": "{{amount}} Go 抵扣可用",
|
||||
"workspace.referral.apply.body": "应用抵扣以减少当前工作区的 Go 用量。",
|
||||
"workspace.referral.apply.noUsage": "先使用 Go,再应用抵扣减少本月用量。",
|
||||
"workspace.referral.apply.action": "使用抵扣",
|
||||
"workspace.referral.apply.confirmTitle": "使用 Go 抵扣",
|
||||
"workspace.referral.apply.confirmBody": "将 {{amount}} 抵扣当前 Go 用量 {{usage}}。抵扣后用量为 {{usageAfter}}。",
|
||||
"workspace.referral.apply.confirmAction": "使用",
|
||||
|
||||
"download.title": "OpenCode | 下载",
|
||||
"download.meta.description": "下载适用于 macOS, Windows, 和 Linux 的 OpenCode",
|
||||
"download.hero.title": "下载 OpenCode",
|
||||
|
||||
@@ -807,6 +807,19 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="referral"] {
|
||||
border-top: 1px solid var(--color-border-weak);
|
||||
padding: var(--vertical-padding) var(--padding);
|
||||
background: var(--color-background);
|
||||
|
||||
a {
|
||||
color: var(--color-text-strong);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: var(--space-1);
|
||||
text-decoration-thickness: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="problem"] {
|
||||
border-top: 1px solid var(--color-border-weak);
|
||||
padding: var(--vertical-padding) var(--padding);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import "./index.css"
|
||||
import { createAsync, query } from "@solidjs/router"
|
||||
import { createAsync, query, useLocation } from "@solidjs/router"
|
||||
import { Title, Meta } from "@solidjs/meta"
|
||||
import { For, createMemo, createSignal, onCleanup, onMount } from "solid-js"
|
||||
//import { HttpHeader } from "@solidjs/start"
|
||||
@@ -224,8 +224,15 @@ function LimitsGraph(props: { href: string }) {
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const location = useLocation()
|
||||
const workspaceID = createAsync(() => checkLoggedIn())
|
||||
const subscribeUrl = createMemo(() => (workspaceID() ? `/workspace/${workspaceID()}/go` : "/auth"))
|
||||
const inviteCode = createMemo(() => new URLSearchParams(location.search).get("invite") ?? undefined)
|
||||
const subscribeUrl = createMemo(() => {
|
||||
const invite = inviteCode() ? `?invite=${encodeURIComponent(inviteCode()!)}` : ""
|
||||
if (workspaceID()) return `/workspace/${workspaceID()}/go${invite}`
|
||||
return "/auth"
|
||||
})
|
||||
const referralUrl = createMemo(() => (workspaceID() ? `/workspace/${workspaceID()}/go/invite` : "/auth"))
|
||||
const i18n = useI18n()
|
||||
const language = useLanguage()
|
||||
return (
|
||||
@@ -380,6 +387,15 @@ export default function Home() {
|
||||
<LimitsGraph href={language.route("/docs/go/#usage-limits")} />
|
||||
</section>
|
||||
|
||||
<section data-component="referral">
|
||||
<div data-slot="section-title">
|
||||
<h3>{i18n.t("go.referral.title")}</h3>
|
||||
<p>
|
||||
{i18n.t("go.referral.note")} <a href={referralUrl()}>{i18n.t("go.referral.link")}</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section data-component="problem">
|
||||
<div data-slot="section-title">
|
||||
<h3>{i18n.t("go.problem.title")}</h3>
|
||||
|
||||
@@ -58,6 +58,18 @@
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-nav-child] {
|
||||
margin-top: calc(-1 * var(--space-2));
|
||||
margin-left: var(--space-5);
|
||||
padding-top: var(--space-2);
|
||||
padding-bottom: var(--space-2);
|
||||
font-size: var(--font-size-xs);
|
||||
|
||||
&.active::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,6 +133,12 @@
|
||||
border-radius: 2px 2px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-nav-child] {
|
||||
padding-left: var(--space-2);
|
||||
padding-right: var(--space-2);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ export default function WorkspaceLayout(props: RouteSectionProps) {
|
||||
<A href={`/workspace/${params.id}/go`} activeClass="active" data-nav-button>
|
||||
{i18n.t("workspace.nav.go")}
|
||||
</A>
|
||||
<A href={`/workspace/${params.id}/go/invite`} activeClass="active" data-nav-button data-nav-child>
|
||||
{i18n.t("workspace.nav.invite")}
|
||||
</A>
|
||||
<A href={`/workspace/${params.id}/usage`} activeClass="active" data-nav-button>
|
||||
{i18n.t("workspace.nav.usage")}
|
||||
</A>
|
||||
@@ -50,6 +53,9 @@ export default function WorkspaceLayout(props: RouteSectionProps) {
|
||||
<A href={`/workspace/${params.id}/go`} activeClass="active" data-nav-button>
|
||||
{i18n.t("workspace.nav.go")}
|
||||
</A>
|
||||
<A href={`/workspace/${params.id}/go/invite`} activeClass="active" data-nav-button data-nav-child>
|
||||
{i18n.t("workspace.nav.invite")}
|
||||
</A>
|
||||
<A href={`/workspace/${params.id}/usage`} activeClass="active" data-nav-button>
|
||||
{i18n.t("workspace.nav.usage")}
|
||||
</A>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { createAsync, useParams } from "@solidjs/router"
|
||||
import { Show } from "solid-js"
|
||||
import { IconGo } from "~/component/icon"
|
||||
import { GoReferralCredits, GoReferralOverview, queryGoReferral } from "~/component/go-referral"
|
||||
import { useI18n } from "~/context/i18n"
|
||||
|
||||
export default function () {
|
||||
const params = useParams()
|
||||
const i18n = useI18n()
|
||||
const referral = createAsync(() => queryGoReferral(params.id!))
|
||||
|
||||
return (
|
||||
<div data-page="workspace-[id]">
|
||||
<section data-component="header-section">
|
||||
<IconGo />
|
||||
<p>{i18n.t("workspace.referral.page.subtitle")}</p>
|
||||
</section>
|
||||
|
||||
<div data-slot="sections">
|
||||
<Show when={referral()} fallback={<section>{i18n.t("workspace.lite.loading")}</section>}>
|
||||
{(summary) => (
|
||||
<>
|
||||
<GoReferralOverview summary={summary()} />
|
||||
<GoReferralCredits workspaceID={params.id!} summary={summary()} />
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -174,6 +174,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="invite-applied"] {
|
||||
width: fit-content;
|
||||
margin-top: var(--space-4);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background-color: var(--color-bg-surface);
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
[data-slot="promo-models-title"] {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 600;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { action, useParams, useAction, useSubmission, json, query, createAsync } from "@solidjs/router"
|
||||
import { action, useParams, useAction, useSubmission, json, query, createAsync, useLocation } from "@solidjs/router"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createMemo, For, Show } from "solid-js"
|
||||
import { Modal } from "~/component/modal"
|
||||
@@ -14,6 +14,7 @@ import styles from "./lite-section.module.css"
|
||||
import { useI18n } from "~/context/i18n"
|
||||
import { useLanguage } from "~/context/language"
|
||||
import { formError } from "~/lib/form-error"
|
||||
import { GoCreditApplyCard, GoReferralBanner, queryGoReferral } from "~/component/go-referral"
|
||||
|
||||
import { IconAlipay, IconUpi } from "~/component/icon"
|
||||
|
||||
@@ -82,8 +83,9 @@ function formatResetTime(seconds: number, i18n: ReturnType<typeof useI18n>) {
|
||||
}
|
||||
|
||||
const createLiteCheckoutUrl = action(
|
||||
async (workspaceID: string, successUrl: string, cancelUrl: string, method?: "alipay" | "upi") => {
|
||||
async (workspaceID: string, successUrl: string, cancelUrl: string, method?: "alipay" | "upi", inviteCode?: string) => {
|
||||
"use server"
|
||||
void inviteCode
|
||||
return json(
|
||||
await withActor(
|
||||
() =>
|
||||
@@ -142,11 +144,14 @@ const setLiteUseBalance = action(async (form: FormData) => {
|
||||
|
||||
export function LiteSection() {
|
||||
const params = useParams()
|
||||
const location = useLocation()
|
||||
const i18n = useI18n()
|
||||
const language = useLanguage()
|
||||
const billingInfo = createAsync(() => queryBillingInfo(params.id!))
|
||||
const isBlack = createMemo(() => billingInfo()?.subscriptionID || billingInfo()?.timeSubscriptionBooked)
|
||||
const lite = createAsync(() => queryLiteSubscription(params.id!))
|
||||
const referral = createAsync(() => queryGoReferral(params.id!))
|
||||
const inviteCode = createMemo(() => new URLSearchParams(location.search).get("invite") ?? undefined)
|
||||
const sessionAction = useAction(createSessionUrl)
|
||||
const sessionSubmission = useSubmission(createSessionUrl)
|
||||
const checkoutAction = useAction(createLiteCheckoutUrl)
|
||||
@@ -171,7 +176,7 @@ export function LiteSection() {
|
||||
|
||||
async function onClickSubscribe(method?: "alipay" | "upi") {
|
||||
setStore("loading", method ?? "checkout")
|
||||
const result = await checkoutAction(params.id!, window.location.href, window.location.href, method)
|
||||
const result = await checkoutAction(params.id!, window.location.href, window.location.href, method, inviteCode())
|
||||
if (result.data) {
|
||||
window.location.href = result.data
|
||||
return
|
||||
@@ -206,6 +211,7 @@ export function LiteSection() {
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
<GoReferralBanner href={`/workspace/${params.id}/go/invite`} />
|
||||
<div data-slot="usage">
|
||||
<div data-slot="usage-item">
|
||||
<div data-slot="usage-header">
|
||||
@@ -246,6 +252,7 @@ export function LiteSection() {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<GoCreditApplyCard workspaceID={params.id!} summary={referral()} />
|
||||
<form action={setLiteUseBalance} method="post" data-slot="setting-row">
|
||||
<p>{i18n.t("workspace.lite.subscription.useBalance")}</p>
|
||||
<input type="hidden" name="workspaceID" value={params.id} />
|
||||
@@ -299,6 +306,9 @@ export function LiteSection() {
|
||||
<li>DeepSeek V4 Flash</li>
|
||||
</ul>
|
||||
<p data-slot="promo-description">{i18n.t("workspace.lite.promo.footer")}</p>
|
||||
<Show when={inviteCode()}>
|
||||
<p data-slot="invite-applied">{i18n.t("workspace.referral.inviteApplied")}</p>
|
||||
</Show>
|
||||
<div data-slot="subscribe-actions">
|
||||
<button
|
||||
data-slot="subscribe-button"
|
||||
|
||||
Reference in New Issue
Block a user