diff --git a/packages/console/app/src/component/go-referral.css b/packages/console/app/src/component/go-referral.css
index 9ca3e25fae..4a05372da2 100644
--- a/packages/console/app/src/component/go-referral.css
+++ b/packages/console/app/src/component/go-referral.css
@@ -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;
}
diff --git a/packages/console/app/src/component/go-referral.tsx b/packages/console/app/src/component/go-referral.tsx
index 28515d82e6..d0eaab2185 100644
--- a/packages/console/app/src/component/go-referral.tsx
+++ b/packages/console/app/src/component/go-referral.tsx
@@ -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
{i18n.t("workspace.referral.rewards.subtitle", {
applied: appliedCount(),
- total: props.summary.rewards.length,
+ total: earnedCount(),
})}
@@ -309,32 +324,40 @@ export function GoReferralSection(props: { workspaceID: string; summary: GoRefer
| {i18n.t("workspace.referral.table.reward")} |
{i18n.t("workspace.referral.table.referral")} |
- {i18n.t("workspace.referral.table.earned")} |
+ {i18n.t("workspace.referral.table.date")} |
|
{(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 (
-
+
| {formatCurrency(reward.amount)} |
- {i18n.t(rewardSourceKey(reward.source))} |
+
+ {i18n.t(rewardSourceKey(reward))}
+
+ {i18n.t(rewardPendingKey(reward.source))}
+
+ |
{earnedAt()}
|
|
diff --git a/packages/console/app/src/i18n/en.ts b/packages/console/app/src/i18n/en.ts
index bdf63c0113..1f7fcf5d4a 100644
--- a/packages/console/app/src/i18n/en.ts
+++ b/packages/console/app/src/i18n/en.ts
@@ -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",
diff --git a/packages/console/app/src/i18n/zh.ts b/packages/console/app/src/i18n/zh.ts
index 946eafab9f..5f8ee3808a 100644
--- a/packages/console/app/src/i18n/zh.ts
+++ b/packages/console/app/src/i18n/zh.ts
@@ -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": "使用",
diff --git a/packages/console/app/src/lib/referral-invite.ts b/packages/console/app/src/lib/referral-invite.ts
new file mode 100644
index 0000000000..4453e39f66
--- /dev/null
+++ b/packages/console/app/src/lib/referral-invite.ts
@@ -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),
+ )
+}
diff --git a/packages/console/app/src/middleware.ts b/packages/console/app/src/middleware.ts
index 750b0d9674..9e2ceeaa4e 100644
--- a/packages/console/app/src/middleware.ts
+++ b/packages/console/app/src/middleware.ts
@@ -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))
},
})
diff --git a/packages/console/app/src/routes/auth/[...callback].ts b/packages/console/app/src/routes/auth/[...callback].ts
index 00bb89406f..c2f6263242 100644
--- a/packages/console/app/src/routes/auth/[...callback].ts
+++ b/packages/console/app/src/routes/auth/[...callback].ts
@@ -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({
diff --git a/packages/console/app/src/routes/go/index.tsx b/packages/console/app/src/routes/go/index.tsx
index 6757470f3e..83bab09097 100644
--- a/packages/console/app/src/routes/go/index.tsx
+++ b/packages/console/app/src/routes/go/index.tsx
@@ -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()
diff --git a/packages/console/app/src/routes/stripe/webhook.ts b/packages/console/app/src/routes/stripe/webhook.ts
index 798d7c0ca9..7b1db7dac2 100644
--- a/packages/console/app/src/routes/stripe/webhook.ts
+++ b/packages/console/app/src/routes/stripe/webhook.ts
@@ -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)
})
diff --git a/packages/console/app/src/routes/workspace/[id]/go/index.tsx b/packages/console/app/src/routes/workspace/[id]/go/index.tsx
index 5f75d50bc8..002ab9e910 100644
--- a/packages/console/app/src/routes/workspace/[id]/go/index.tsx
+++ b/packages/console/app/src/routes/workspace/[id]/go/index.tsx
@@ -30,7 +30,11 @@ export default function () {
{i18n.t("workspace.lite.loading")}}>
- {(summary) => }
+ {(summary) => (
+
+
+
+ )}
diff --git a/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx b/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx
index f80e34e8dd..054752a7a9 100644
--- a/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx
+++ b/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx
@@ -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 {
+ 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()
- 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
diff --git a/packages/console/core/src/billing.ts b/packages/console/core/src/billing.ts
index 5ce830acb9..ca3a40878e 100644
--- a/packages/console/core/src/billing.ts
+++ b/packages/console/core/src/billing.ts
@@ -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 } : {}),
},
},
})
diff --git a/packages/console/core/src/referral.ts b/packages/console/core/src/referral.ts
index f71acd1383..989eaad5db 100644
--- a/packages/console/core/src/referral.ts
+++ b/packages/console/core/src/referral.ts
@@ -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([
diff --git a/packages/console/function/src/auth.ts b/packages/console/function/src/auth.ts
index 28724ec157..613dccf997 100644
--- a/packages/console/function/src/auth.ts
+++ b/packages/console/function/src/auth.ts
@@ -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