core: credit referral invites on first Lite checkout

Applies saved invites during Lite checkout so users who skip the referral page still credit the inviter. Avoids crediting already-active Lite workspaces so rewards stay tied to the first upgrade.
This commit is contained in:
vimtor
2026-05-30 10:52:21 +02:00
parent 04c46117b9
commit 6a2cd816b7
4 changed files with 35 additions and 22 deletions
@@ -2,7 +2,6 @@ import { action, json, query, useAction, useSubmission } from "@solidjs/router"
import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js" import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js"
import { getRequestEvent } from "solid-js/web" import { getRequestEvent } from "solid-js/web"
import { Referral } from "@opencode-ai/console-core/referral.js" import { Referral } from "@opencode-ai/console-core/referral.js"
import { Actor } from "@opencode-ai/console-core/actor.js"
import { withActor } from "~/context/auth.withActor" import { withActor } from "~/context/auth.withActor"
import { Modal } from "~/component/modal" import { Modal } from "~/component/modal"
import { IconCheck, IconCopy } from "~/component/icon" import { IconCheck, IconCopy } from "~/component/icon"
@@ -10,7 +9,7 @@ import { useI18n } from "~/context/i18n"
import { useLanguage } from "~/context/language" import { useLanguage } from "~/context/language"
import { formatResetTime, liteResetTimeKeys } from "~/lib/format-reset-time" import { formatResetTime, liteResetTimeKeys } from "~/lib/format-reset-time"
import { queryLiteSubscription } from "~/routes/workspace/[id]/go/lite-section" import { queryLiteSubscription } from "~/routes/workspace/[id]/go/lite-section"
import { clearReferralCookie, referralCodeFromCookieHeader } from "~/lib/referral-invite" import { createReferralFromCookie } from "~/lib/referral-invite"
import "./go-referral.css" import "./go-referral.css"
type GoReferralSummary = Awaited<ReturnType<typeof Referral.summary>> type GoReferralSummary = Awaited<ReturnType<typeof Referral.summary>>
@@ -28,18 +27,7 @@ const emptyUsagePreview = {
export const queryGoReferral = query(async (workspaceID: string) => { export const queryGoReferral = query(async (workspaceID: string) => {
"use server" "use server"
return withActor(async () => { return withActor(async () => {
const event = getRequestEvent() await createReferralFromCookie()
const referralCode = referralCodeFromCookieHeader(event?.request.headers.get("cookie") ?? null)
if (referralCode) {
await Referral.createFromAccount({
accountID: Actor.account(),
referralCode,
}).catch((error) => {
console.error("Referral create failed", error)
})
event?.response.headers.append("set-cookie", clearReferralCookie())
}
return Referral.summary() return Referral.summary()
}, workspaceID) }, workspaceID)
}, "go.referral.get") }, "go.referral.get")
@@ -1,4 +1,6 @@
import { Actor } from "@opencode-ai/console-core/actor.js"
import { Referral } from "@opencode-ai/console-core/referral.js" import { Referral } from "@opencode-ai/console-core/referral.js"
import { getRequestEvent } from "solid-js/web"
const REFERRAL_COOKIE = "oc_referral" const REFERRAL_COOKIE = "oc_referral"
const REFERRAL_MAX_AGE = 60 * 60 * 24 * 30 const REFERRAL_MAX_AGE = 60 * 60 * 24 * 30
@@ -26,3 +28,17 @@ export function referralCodeFromCookieHeader(header: string | null) {
?.slice(`${REFERRAL_COOKIE}=`.length), ?.slice(`${REFERRAL_COOKIE}=`.length),
) )
} }
export async function createReferralFromCookie() {
const event = getRequestEvent()
const referralCode = referralCodeFromCookieHeader(event?.request.headers.get("cookie") ?? null)
if (!referralCode) return
await Referral.createFromAccount({
accountID: Actor.account(),
referralCode,
}).catch((error) => {
console.error("Referral create failed", error)
})
event?.response.headers.append("set-cookie", clearReferralCookie())
}
@@ -15,6 +15,7 @@ import { useI18n } from "~/context/i18n"
import { useLanguage } from "~/context/language" import { useLanguage } from "~/context/language"
import { formError } from "~/lib/form-error" import { formError } from "~/lib/form-error"
import { formatResetTime, liteResetTimeKeys } from "~/lib/format-reset-time" import { formatResetTime, liteResetTimeKeys } from "~/lib/format-reset-time"
import { createReferralFromCookie } from "~/lib/referral-invite"
import { IconAlipay, IconUpi } from "~/component/icon" import { IconAlipay, IconUpi } from "~/component/icon"
@@ -75,15 +76,16 @@ const createLiteCheckoutUrl = action(
"use server" "use server"
return json( return json(
await withActor( await withActor(
() => async () => {
Billing.generateLiteCheckoutUrl({ successUrl, cancelUrl, method }) const data = await Billing.generateLiteCheckoutUrl({ successUrl, cancelUrl, method })
.then((data) => ({ error: undefined, data })) await createReferralFromCookie()
.catch((e) => ({ return { error: undefined, data }
error: e.message as string, },
data: undefined,
})),
workspaceID, workspaceID,
), ).catch((e) => ({
error: e.message as string,
data: undefined,
})),
{ revalidate: [queryBillingInfo.key, queryLiteSubscription.key] }, { revalidate: [queryBillingInfo.key, queryLiteSubscription.key] },
) )
}, },
+7
View File
@@ -325,6 +325,13 @@ export namespace Referral {
.then((rows) => rows.map((row) => row.workspaceID)) .then((rows) => rows.map((row) => row.workspaceID))
if (workspaceIDs.length === 0) return if (workspaceIDs.length === 0) return
const activeLite = await tx
.select({ id: LiteTable.id })
.from(LiteTable)
.where(and(inArray(LiteTable.workspaceID, workspaceIDs), isNull(LiteTable.timeDeleted)))
.then((rows) => rows[0])
if (activeLite) return
const litePayment = await tx const litePayment = await tx
.select({ id: PaymentTable.id }) .select({ id: PaymentTable.id })
.from(PaymentTable) .from(PaymentTable)