core: keep Go referral credit visible until subscription unlock

New invitees keep referral credit through sign-in and both sides see pending rewards until the invitee subscribes to Go.
This commit is contained in:
vimtor
2026-05-16 12:42:36 +02:00
parent 0af386a433
commit 71535a25c7
14 changed files with 223 additions and 82 deletions

View File

@@ -336,6 +336,18 @@
&[data-slot="referral-source"] {
color: var(--color-text-secondary);
font-family: var(--font-sans);
span,
[data-slot="pending-note"] {
display: block;
}
[data-slot="pending-note"] {
margin-top: var(--space-1);
color: var(--color-text-muted);
line-height: 1.4;
white-space: normal;
}
}
&[data-slot="referral-action"] {
@@ -356,6 +368,17 @@
}
}
&[data-status="pending"] {
td[data-slot="referral-amount"],
td[data-slot="referral-date"] {
color: var(--color-text-muted);
}
td[data-slot="referral-source"] {
color: var(--color-text);
}
}
&:last-child td {
border-bottom: none;
}

View File

@@ -19,6 +19,7 @@ export type GoReferralReward = {
id: string
amount: number
source: "inviter" | "invitee"
status: "pending" | "available" | "applied"
timeCreated: string | Date
timeApplied: string | Date | null
}
@@ -179,11 +180,24 @@ function formatDate(value: string | Date, locale: string) {
return new Intl.DateTimeFormat(locale, { month: "short", day: "numeric", year: "numeric" }).format(new Date(value))
}
function rewardSourceKey(source: GoReferralReward["source"]) {
if (source === "invitee") return "workspace.referral.reward.source.invitee" as const
function rewardSourceKey(reward: GoReferralReward) {
if (reward.status === "pending" && reward.source === "invitee")
return "workspace.referral.reward.source.pendingInvitee" as const
if (reward.status === "pending") return "workspace.referral.reward.source.pendingInviter" as const
if (reward.source === "invitee") return "workspace.referral.reward.source.invitee" as const
return "workspace.referral.reward.source.inviter" as const
}
function rewardPendingKey(source: GoReferralReward["source"]) {
if (source === "invitee") return "workspace.referral.reward.pending.invitee" as const
return "workspace.referral.reward.pending.inviter" as const
}
function rewardPendingStatusKey(source: GoReferralReward["source"]) {
if (source === "invitee") return "workspace.referral.reward.status.pendingInvitee" as const
return "workspace.referral.reward.status.pendingInviter" as const
}
function CopyInviteLink(props: { summary: GoReferralSummary }) {
const i18n = useI18n()
const [copied, setCopied] = createSignal(false)
@@ -236,6 +250,7 @@ export function GoReferralSection(props: { workspaceID: string; summary: GoRefer
} satisfies GoReferralUsagePreview
})
const appliedCount = createMemo(() => props.summary.rewards.filter((reward) => reward.timeApplied).length)
const earnedCount = createMemo(() => props.summary.rewards.filter((reward) => reward.status !== "pending").length)
createEffect(() => {
const reward = selected()
@@ -295,7 +310,7 @@ export function GoReferralSection(props: { workspaceID: string; summary: GoRefer
<p>
{i18n.t("workspace.referral.rewards.subtitle", {
applied: appliedCount(),
total: props.summary.rewards.length,
total: earnedCount(),
})}
</p>
</div>
@@ -309,32 +324,40 @@ export function GoReferralSection(props: { workspaceID: string; summary: GoRefer
<tr>
<th>{i18n.t("workspace.referral.table.reward")}</th>
<th>{i18n.t("workspace.referral.table.referral")}</th>
<th>{i18n.t("workspace.referral.table.earned")}</th>
<th>{i18n.t("workspace.referral.table.date")}</th>
<th></th>
</tr>
</thead>
<tbody>
<For each={props.summary.rewards}>
{(reward) => {
const applied = createMemo(() => !!reward.timeApplied)
const applied = createMemo(() => reward.status === "applied")
const pending = createMemo(() => reward.status === "pending")
const earnedAt = createMemo(() => formatDate(reward.timeCreated, language.tag(language.locale())))
return (
<tr data-status={applied() ? "applied" : "available"}>
<tr data-status={reward.status} data-source={reward.source}>
<td data-slot="referral-amount">{formatCurrency(reward.amount)}</td>
<td data-slot="referral-source">{i18n.t(rewardSourceKey(reward.source))}</td>
<td data-slot="referral-source">
<span>{i18n.t(rewardSourceKey(reward))}</span>
<Show when={pending()}>
<span data-slot="pending-note">{i18n.t(rewardPendingKey(reward.source))}</span>
</Show>
</td>
<td data-slot="referral-date" title={earnedAt()}>
{earnedAt()}
</td>
<td data-slot="referral-action">
<button
type="button"
disabled={applied() || !props.summary.hasActiveGo || submission.pending}
disabled={reward.status !== "available" || !props.summary.hasActiveGo || submission.pending}
onClick={() => setSelected(reward)}
>
<Show when={!applied()} fallback={i18n.t("workspace.referral.reward.status.applied")}>
{props.summary.hasActiveGo
? i18n.t("workspace.referral.apply.action")
: i18n.t("workspace.referral.apply.noGo")}
{pending()
? i18n.t(rewardPendingStatusKey(reward.source))
: props.summary.hasActiveGo
? i18n.t("workspace.referral.apply.action")
: i18n.t("workspace.referral.apply.noGo")}
</Show>
</button>
</td>

View File

@@ -678,11 +678,17 @@ export const dict = {
"workspace.referral.rewards.subtitle": "{{applied}} / {{total}} rewards applied.",
"workspace.referral.rewards.empty": "No referral rewards yet.",
"workspace.referral.table.reward": "Reward",
"workspace.referral.table.referral": "Referral",
"workspace.referral.table.earned": "Earned",
"workspace.referral.table.referral": "Description",
"workspace.referral.table.date": "Date",
"workspace.referral.reward.source.inviter": "Earned by inviting a friend",
"workspace.referral.reward.source.invitee": "Received from an invite",
"workspace.referral.reward.source.pendingInviter": "Friend account created",
"workspace.referral.reward.source.pendingInvitee": "Invite accepted",
"workspace.referral.reward.status.applied": "Applied",
"workspace.referral.reward.status.pendingInviter": "Waiting for subscription",
"workspace.referral.reward.status.pendingInvitee": "Subscribe to unlock",
"workspace.referral.reward.pending.inviter": "Reward unlocks when they subscribe to Go.",
"workspace.referral.reward.pending.invitee": "Subscribe to Go to unlock this reward.",
"workspace.referral.reward.earnedOn": "{{date}}",
"workspace.referral.apply.noGo": "Subscribe to Go",
"workspace.referral.apply.action": "Apply",

View File

@@ -659,11 +659,17 @@ export const dict = {
"workspace.referral.rewards.subtitle": "已使用 {{applied}} / {{total}} 个奖励。",
"workspace.referral.rewards.empty": "暂无邀请奖励。",
"workspace.referral.table.reward": "奖励",
"workspace.referral.table.referral": "邀请",
"workspace.referral.table.earned": "获得时间",
"workspace.referral.table.referral": "描述",
"workspace.referral.table.date": "日期",
"workspace.referral.reward.source.inviter": "邀请好友获得",
"workspace.referral.reward.source.invitee": "通过邀请获得",
"workspace.referral.reward.source.pendingInviter": "好友已创建账号",
"workspace.referral.reward.source.pendingInvitee": "已接受邀请",
"workspace.referral.reward.status.applied": "已使用",
"workspace.referral.reward.status.pendingInviter": "等待订阅",
"workspace.referral.reward.status.pendingInvitee": "订阅后解锁",
"workspace.referral.reward.pending.inviter": "好友订阅 Go 后即可解锁奖励。",
"workspace.referral.reward.pending.invitee": "订阅 Go 后即可解锁此奖励。",
"workspace.referral.reward.earnedOn": "{{date}}",
"workspace.referral.apply.noGo": "订阅 Go",
"workspace.referral.apply.action": "使用",

View File

@@ -0,0 +1,26 @@
const INVITE_COOKIE = "opencode.go.invite"
const INVITE_MAX_AGE = 60 * 60 * 24 * 30
export function normalizeInviteCode(code?: string | null) {
return code?.toUpperCase().replace(/[^A-Z0-9]/g, "").slice(0, 16)
}
export function inviteCookie(code: string) {
return `${INVITE_COOKIE}=${encodeURIComponent(code)}; Path=/; Max-Age=${INVITE_MAX_AGE}; SameSite=Lax; HttpOnly`
}
export function clearInviteCookie() {
return `${INVITE_COOKIE}=; Path=/; Max-Age=0; SameSite=Lax; HttpOnly`
}
export function inviteFromCookieHeader(header: string | null) {
if (!header) return undefined
return normalizeInviteCode(
header
.split(";")
.map((x) => x.trim())
.find((x) => x.startsWith(`${INVITE_COOKIE}=`))
?.slice(`${INVITE_COOKIE}=`.length),
)
}

View File

@@ -1,16 +1,20 @@
import { createMiddleware } from "@solidjs/start/middleware"
import { LOCALE_HEADER, cookie, fromPathname, strip } from "~/lib/language"
import { inviteCookie, normalizeInviteCode } from "~/lib/referral-invite"
export default createMiddleware({
onRequest(event) {
const url = new URL(event.request.url)
const locale = fromPathname(url.pathname)
if (!locale) return
if (locale) {
url.pathname = strip(url.pathname)
const request = new Request(url, event.request)
request.headers.set(LOCALE_HEADER, locale)
event.request = request
event.response.headers.append("set-cookie", cookie(locale))
}
url.pathname = strip(url.pathname)
const request = new Request(url, event.request)
request.headers.set(LOCALE_HEADER, locale)
event.request = request
event.response.headers.append("set-cookie", cookie(locale))
const inviteCode = normalizeInviteCode(url.searchParams.get("invite"))
if (inviteCode) event.response.headers.append("set-cookie", inviteCookie(inviteCode))
},
})

View File

@@ -1,9 +1,11 @@
import { redirect } from "@solidjs/router"
import type { APIEvent } from "@solidjs/start/server"
import { Referral } from "@opencode-ai/console-core/referral.js"
import { AuthClient } from "~/context/auth"
import { useAuthSession } from "~/context/auth"
import { i18n } from "~/i18n"
import { localeFromRequest, route } from "~/lib/language"
import { clearInviteCookie, inviteFromCookieHeader } from "~/lib/referral-invite"
export async function GET(input: APIEvent) {
const url = new URL(input.request.url)
@@ -17,6 +19,7 @@ export async function GET(input: APIEvent) {
if (result.err) throw new Error(result.err.message)
const decoded = AuthClient.decode(result.tokens.access, {} as any)
if (decoded.err) throw new Error(decoded.err.message)
const inviteCode = inviteFromCookieHeader(input.request.headers.get("cookie"))
const session = await useAuthSession()
const id = decoded.subject.properties.accountID
await session.update((value) => {
@@ -32,8 +35,15 @@ export async function GET(input: APIEvent) {
current: id,
}
})
if (decoded.subject.properties.newAccount && inviteCode) {
await Referral.createFromAccount({ accountID: id, inviteCode }).catch((error) => {
console.error("Referral create failed", error)
})
}
const next = url.pathname === "/auth/callback" ? "/auth" : url.pathname.replace("/auth/callback", "")
return redirect(route(locale, next))
const response = redirect(route(locale, next))
if (inviteCode) response.headers.append("set-cookie", clearInviteCookie())
return response
} catch (e: any) {
return new Response(
JSON.stringify({

View File

@@ -37,8 +37,6 @@ const models = [
{ name: "DeepSeek V4 Flash", provider: "DeepSeek" },
]
const INVITE_STORAGE_KEY = "opencode.go.invite"
function LimitsGraph(props: { href: string }) {
let root!: HTMLElement
const [visible, setVisible] = createSignal(false)
@@ -229,15 +227,10 @@ export default function Home() {
const location = useLocation()
const workspaceID = createAsync(() => checkLoggedIn())
const inviteCode = createMemo(() => new URLSearchParams(location.search).get("invite") ?? undefined)
onMount(() => {
const code = inviteCode()
if (!code) return
window.localStorage.setItem(INVITE_STORAGE_KEY, code)
})
const subscribeUrl = createMemo(() => {
const invite = inviteCode() ? `?invite=${encodeURIComponent(inviteCode()!)}` : ""
if (workspaceID()) return `/workspace/${workspaceID()}/go${invite}`
return "/auth"
return `/auth${invite}`
})
const i18n = useI18n()
const language = useLanguage()

View File

@@ -112,7 +112,6 @@ export async function POST(input: APIEvent) {
const userID = body.data.object.metadata?.userID
const userEmail = body.data.object.metadata?.userEmail
const coupon = body.data.object.metadata?.coupon
const inviteCode = body.data.object.metadata?.inviteCode
const customerID = body.data.object.customer as string
const invoiceID = body.data.object.latest_invoice as string
const subscriptionID = body.data.object.id as string
@@ -174,10 +173,9 @@ export async function POST(input: APIEvent) {
}
})
await Referral.createFromLiteSubscription({
await Referral.completeFromLiteSubscription({
workspaceID,
userID,
inviteCode,
}).catch((error) => {
console.error("Referral sync failed", error)
})

View File

@@ -30,7 +30,11 @@ export default function () {
<div data-slot="sections">
<LiteSection />
<Show when={referral()} fallback={<section>{i18n.t("workspace.lite.loading")}</section>}>
{(summary) => <GoReferralSection workspaceID={params.id!} summary={summary()} />}
{(summary) => (
<Show when={summary().hasActiveGo}>
<GoReferralSection workspaceID={params.id!} summary={summary()} />
</Show>
)}
</Show>
</div>
</div>

View File

@@ -1,6 +1,6 @@
import { action, useParams, useAction, useSubmission, json, query, createAsync, useLocation } from "@solidjs/router"
import { action, useParams, useAction, useSubmission, json, query, createAsync } from "@solidjs/router"
import { createStore } from "solid-js/store"
import { createMemo, createSignal, For, onMount, Show } from "solid-js"
import { createMemo, For, Show } from "solid-js"
import { Modal } from "~/component/modal"
import { Billing } from "@opencode-ai/console-core/billing.js"
import { Database, eq, and, isNull } from "@opencode-ai/console-core/drizzle/index.js"
@@ -17,8 +17,6 @@ import { formError } from "~/lib/form-error"
import { IconAlipay, IconUpi } from "~/component/icon"
const INVITE_STORAGE_KEY = "opencode.go.invite"
export const queryLiteSubscription = query(async (workspaceID: string) => {
"use server"
return withActor(async () => {
@@ -84,12 +82,12 @@ export function formatResetTime(seconds: number, i18n: ReturnType<typeof useI18n
}
const createLiteCheckoutUrl = action(
async (workspaceID: string, successUrl: string, cancelUrl: string, method?: "alipay" | "upi", inviteCode?: string) => {
async (workspaceID: string, successUrl: string, cancelUrl: string, method?: "alipay" | "upi") => {
"use server"
return json(
await withActor(
() =>
Billing.generateLiteCheckoutUrl({ successUrl, cancelUrl, method, inviteCode })
Billing.generateLiteCheckoutUrl({ successUrl, cancelUrl, method })
.then((data) => ({ error: undefined, data }))
.catch((e) => ({
error: e.message as string,
@@ -144,14 +142,11 @@ 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 [storedInviteCode, setStoredInviteCode] = createSignal<string>()
const inviteCode = createMemo(() => new URLSearchParams(location.search).get("invite") ?? storedInviteCode())
const sessionAction = useAction(createSessionUrl)
const sessionSubmission = useSubmission(createSessionUrl)
const checkoutAction = useAction(createLiteCheckoutUrl)
@@ -164,13 +159,6 @@ export function LiteSection() {
const busy = createMemo(() => !!store.loading)
onMount(() => {
const code = inviteCode() ?? window.localStorage.getItem(INVITE_STORAGE_KEY) ?? undefined
if (!code) return
window.localStorage.setItem(INVITE_STORAGE_KEY, code)
setStoredInviteCode(code)
})
async function onClickSession() {
setStore("loading", "session")
const result = await sessionAction(params.id!, window.location.href)
@@ -183,7 +171,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, inviteCode())
const result = await checkoutAction(params.id!, window.location.href, window.location.href, method)
if (result.data) {
window.location.href = result.data
return

View File

@@ -279,11 +279,10 @@ export namespace Billing {
successUrl: z.string(),
cancelUrl: z.string(),
method: z.enum(["alipay", "upi"]).optional(),
inviteCode: z.string().optional(),
}),
async (input) => {
const user = Actor.assert("user")
const { successUrl, cancelUrl, method, inviteCode } = input
const { successUrl, cancelUrl, method } = input
const email = (await User.getAuthEmail(user.properties.userID))!
const billing = await Billing.get()
@@ -365,7 +364,6 @@ export namespace Billing {
userEmail: email,
coupon,
type: "lite",
...(inviteCode ? { inviteCode } : {}),
},
},
})

View File

@@ -56,11 +56,13 @@ export namespace Referral {
export const summary = fn(z.void(), async () => {
const workspaceID = Actor.workspace()
const accountID = Actor.account()
const code = await ensureCode(workspaceID)
const rows = await Database.use(async (tx) => {
const rewards = await tx
.select({
id: ReferralRewardTable.id,
referralID: ReferralRewardTable.referralID,
workspaceID: ReferralRewardTable.workspaceID,
referralWorkspaceID: ReferralTable.workspaceID,
amount: ReferralRewardTable.amount,
@@ -79,27 +81,68 @@ export namespace Referral {
.orderBy(desc(ReferralRewardTable.timeCreated))
const invites = await tx
.select({ id: ReferralTable.id })
.select({ id: ReferralTable.id, timeCreated: ReferralTable.timeCreated })
.from(ReferralTable)
.where(and(eq(ReferralTable.workspaceID, workspaceID), isNull(ReferralTable.timeDeleted)))
const inviteeReferrals = await tx
.select({ id: ReferralTable.id, timeCreated: ReferralTable.timeCreated })
.from(ReferralTable)
.where(and(eq(ReferralTable.inviteeAccountID, accountID), isNull(ReferralTable.timeDeleted)))
const inviteeRewards = await tx
.select({ referralID: ReferralRewardTable.referralID })
.from(ReferralRewardTable)
.innerJoin(ReferralTable, eq(ReferralTable.id, ReferralRewardTable.referralID))
.where(
and(
eq(ReferralTable.inviteeAccountID, accountID),
isNull(ReferralRewardTable.timeDeleted),
isNull(ReferralTable.timeDeleted),
),
)
const lite = await tx
.select({ id: LiteTable.id })
.from(LiteTable)
.where(and(eq(LiteTable.workspaceID, workspaceID), isNull(LiteTable.timeDeleted)))
.then((result) => result[0])
return { invites, lite, rewards }
return { inviteeReferrals, inviteeRewards, invites, lite, rewards }
})
const rewardReferralIDs = new Set(rows.rewards.map((reward) => reward.referralID))
const inviteeRewardReferralIDs = new Set(rows.inviteeRewards.map((reward) => reward.referralID))
const rewards = rows.rewards.map((reward) => ({
id: reward.id,
source: reward.workspaceID === reward.referralWorkspaceID ? ("inviter" as const) : ("invitee" as const),
status: reward.timeApplied ? ("applied" as const) : ("available" as const),
amount: microCentsToCents(reward.amount),
timeCreated: reward.timeCreated,
timeApplied: reward.timeApplied,
}))
const pending = [
...rows.invites
.filter((referral) => !rewardReferralIDs.has(referral.id))
.map((referral) => ({
id: `${referral.id}:inviter`,
source: "inviter" as const,
status: "pending" as const,
amount: microCentsToCents(REWARD_AMOUNT),
timeCreated: referral.timeCreated,
timeApplied: null,
})),
...rows.inviteeReferrals
.filter((referral) => !inviteeRewardReferralIDs.has(referral.id))
.map((referral) => ({
id: `${referral.id}:invitee`,
source: "invitee" as const,
status: "pending" as const,
amount: microCentsToCents(REWARD_AMOUNT),
timeCreated: referral.timeCreated,
timeApplied: null,
})),
].sort((a, b) => new Date(b.timeCreated).getTime() - new Date(a.timeCreated).getTime())
return {
inviteCode: code.code,
validInviteCount: rows.invites.length,
@@ -109,7 +152,9 @@ export namespace Referral {
totalApplied: rewards
.filter((reward) => reward.timeApplied)
.reduce((total, reward) => total + reward.amount, 0),
rewards,
rewards: [...pending, ...rewards].sort(
(a, b) => new Date(b.timeCreated).getTime() - new Date(a.timeCreated).getTime(),
),
}
})
@@ -184,9 +229,8 @@ export namespace Referral {
})
})
export async function createFromLiteSubscription(input: {
workspaceID: string
userID: string
export async function createFromAccount(input: {
accountID: string
inviteCode?: string
}) {
const inviteCode = normalizeCode(input.inviteCode)
@@ -200,19 +244,10 @@ export namespace Referral {
.then((rows) => rows[0])
if (!code) return { status: "invalid-code" as const }
const invitee = await tx
.select({ accountID: UserTable.accountID })
.from(UserTable)
.where(
and(eq(UserTable.workspaceID, input.workspaceID), eq(UserTable.id, input.userID), isNull(UserTable.timeDeleted)),
)
.then((rows) => rows[0])
if (!invitee?.accountID) return { status: "missing-account" as const }
const existingReferral = await tx
.select({ id: ReferralTable.id })
.from(ReferralTable)
.where(and(eq(ReferralTable.inviteeAccountID, invitee.accountID), isNull(ReferralTable.timeDeleted)))
.where(and(eq(ReferralTable.inviteeAccountID, input.accountID), isNull(ReferralTable.timeDeleted)))
.then((rows) => rows[0])
if (existingReferral) return { status: "already-redeemed" as const }
@@ -222,27 +257,20 @@ export namespace Referral {
.where(
and(
eq(UserTable.workspaceID, code.workspaceID),
eq(UserTable.accountID, invitee.accountID),
eq(UserTable.accountID, input.accountID),
isNull(UserTable.timeDeleted),
),
)
.then((rows) => rows[0])
if (selfReferral) return { status: "self-referral" as const }
const existingGo = await tx
.select({ workspaceID: LiteTable.workspaceID })
.from(LiteTable)
.innerJoin(UserTable, and(eq(UserTable.workspaceID, LiteTable.workspaceID), eq(UserTable.id, LiteTable.userID)))
.where(and(eq(UserTable.accountID, invitee.accountID), isNull(UserTable.timeDeleted), isNull(LiteTable.timeDeleted)))
if (existingGo.some((row) => row.workspaceID !== input.workspaceID)) return { status: "already-subscribed" as const }
const referralID = Identifier.create("referral")
await tx
.insert(ReferralTable)
.values({
workspaceID: code.workspaceID,
id: referralID,
inviteeAccountID: invitee.accountID,
inviteeAccountID: input.accountID,
})
.onDuplicateKeyUpdate({
set: {
@@ -253,11 +281,42 @@ export namespace Referral {
const referral = await tx
.select({ id: ReferralTable.id, workspaceID: ReferralTable.workspaceID })
.from(ReferralTable)
.where(and(eq(ReferralTable.inviteeAccountID, invitee.accountID), isNull(ReferralTable.timeDeleted)))
.where(and(eq(ReferralTable.inviteeAccountID, input.accountID), isNull(ReferralTable.timeDeleted)))
.then((rows) => rows[0])
if (!referral) return { status: "duplicate" as const }
if (referral.id !== referralID) return { status: "already-redeemed" as const }
return { status: "created" as const }
})
}
export async function completeFromLiteSubscription(input: {
workspaceID: string
userID: string
}) {
return Database.transaction(async (tx) => {
const invitee = await tx
.select({ accountID: UserTable.accountID })
.from(UserTable)
.where(
and(eq(UserTable.workspaceID, input.workspaceID), eq(UserTable.id, input.userID), isNull(UserTable.timeDeleted)),
)
.then((rows) => rows[0])
if (!invitee?.accountID) return { status: "missing-account" as const }
const referral = await tx
.select({ id: ReferralTable.id, workspaceID: ReferralTable.workspaceID })
.from(ReferralTable)
.where(and(eq(ReferralTable.inviteeAccountID, invitee.accountID), isNull(ReferralTable.timeDeleted)))
.then((rows) => rows[0])
if (!referral) return { status: "missing-referral" as const }
const existingRewards = await tx
.select({ id: ReferralRewardTable.id })
.from(ReferralRewardTable)
.where(and(eq(ReferralRewardTable.referralID, referral.id), isNull(ReferralRewardTable.timeDeleted)))
if (existingRewards.length > 0) return { status: "already-completed" as const }
await tx
.insert(ReferralRewardTable)
.values([

View File

@@ -26,6 +26,7 @@ export const subjects = createSubjects({
account: z.object({
accountID: z.string(),
email: z.string(),
newAccount: z.boolean().optional(),
}),
user: z.object({
userID: z.string(),
@@ -142,6 +143,7 @@ export default {
}
// Get account
let newAccount = false
const accountID = await (async () => {
const matches = await Database.use(async (tx) =>
tx
@@ -166,6 +168,7 @@ export default {
if (!accountID) {
console.log("creating account for", email)
accountID = await Account.create({})
newAccount = true
}
await Database.use(async (tx) =>
@@ -215,7 +218,7 @@ export default {
await Workspace.create({ name: "Default" })
}
})
return ctx.subject("account", accountID, { accountID, email })
return ctx.subject("account", accountID, { accountID, email, newAccount })
},
}).fetch(request, env, ctx)
return result