diff --git a/packages/console/app/src/component/go-referral.tsx b/packages/console/app/src/component/go-referral.tsx index fd4d5c8b1a..14ea8194d1 100644 --- a/packages/console/app/src/component/go-referral.tsx +++ b/packages/console/app/src/component/go-referral.tsx @@ -2,6 +2,7 @@ import { action, json, query, useAction, useSubmission } from "@solidjs/router" import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js" import { getRequestEvent } from "solid-js/web" 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 { Modal } from "~/component/modal" import { IconCheck, IconCopy } from "~/component/icon" @@ -9,6 +10,7 @@ import { useI18n } from "~/context/i18n" import { useLanguage } from "~/context/language" import { formatResetTime, liteResetTimeKeys } from "~/lib/format-reset-time" import { queryLiteSubscription } from "~/routes/workspace/[id]/go/lite-section" +import { clearReferralCookie, referralCodeFromCookieHeader } from "~/lib/referral-invite" import "./go-referral.css" type GoReferralSummary = Awaited> @@ -25,7 +27,21 @@ const emptyUsagePreview = { export const queryGoReferral = query(async (workspaceID: string) => { "use server" - return withActor(() => Referral.summary(), workspaceID) + return withActor(async () => { + const event = getRequestEvent() + 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() + }, workspaceID) }, "go.referral.get") export const queryGoReferralUsagePreview = query(async (workspaceID: string, referralID?: string) => { @@ -65,6 +81,7 @@ function rewardDescriptionKey(source: GoReferralReward["source"]) { function rewardActionKey(reward: GoReferralReward, hasActiveGo: boolean) { if (reward.status === "applied") return "workspace.referral.reward.action.applied" as const + if (reward.status === "pending" && reward.source === "inviter") return "workspace.referral.reward.source.pendingInviter" as const if (reward.status === "pending" || !hasActiveGo) return "workspace.referral.reward.action.subscribeUnlock" as const return "workspace.referral.reward.action.view" as const } diff --git a/packages/console/app/src/routes/auth/[...callback].ts b/packages/console/app/src/routes/auth/[...callback].ts index b1b942ee72..00bb89406f 100644 --- a/packages/console/app/src/routes/auth/[...callback].ts +++ b/packages/console/app/src/routes/auth/[...callback].ts @@ -1,11 +1,9 @@ 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 { clearReferralCookie, referralCodeFromCookieHeader } from "~/lib/referral-invite" export async function GET(input: APIEvent) { const url = new URL(input.request.url) @@ -19,7 +17,6 @@ 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 referralCode = referralCodeFromCookieHeader(input.request.headers.get("cookie")) const session = await useAuthSession() const id = decoded.subject.properties.accountID await session.update((value) => { @@ -35,15 +32,8 @@ export async function GET(input: APIEvent) { current: id, } }) - if (decoded.subject.properties.newAccount && referralCode) { - await Referral.createFromAccount({ accountID: id, referralCode }).catch((error) => { - console.error("Referral create failed", error) - }) - } const next = url.pathname === "/auth/callback" ? "/auth" : url.pathname.replace("/auth/callback", "") - const response = redirect(route(locale, next)) - if (referralCode) response.headers.append("set-cookie", clearReferralCookie()) - return response + return redirect(route(locale, next)) } catch (e: any) { return new Response( JSON.stringify({ diff --git a/packages/console/app/vite.config.ts b/packages/console/app/vite.config.ts index fb753b6be7..33455acc5e 100644 --- a/packages/console/app/vite.config.ts +++ b/packages/console/app/vite.config.ts @@ -17,6 +17,7 @@ export default defineConfig({ ], server: { allowedHosts: true, + port: 3001, }, build: { rollupOptions: { diff --git a/packages/console/core/src/referral.ts b/packages/console/core/src/referral.ts index 66b14dbe39..5686dd67ee 100644 --- a/packages/console/core/src/referral.ts +++ b/packages/console/core/src/referral.ts @@ -1,8 +1,8 @@ import { z } from "zod" -import { and, asc, eq, isNull, sql, Database } from "./drizzle" +import { and, asc, eq, inArray, isNull, sql, Database } from "./drizzle" import { Actor } from "./actor" import { Identifier } from "./identifier" -import { LiteTable } from "./schema/billing.sql" +import { LiteTable, PaymentTable } from "./schema/billing.sql" import { ReferralCodeTable, ReferralRewardTable, ReferralTable } from "./schema/referral.sql" import { AuthTable } from "./schema/auth.sql" import { UserTable } from "./schema/user.sql" @@ -318,6 +318,26 @@ export namespace Referral { .then((rows) => rows[0]) if (selfReferral) throw new Error("Self-referral is not allowed") + const workspaceIDs = await tx + .select({ workspaceID: UserTable.workspaceID }) + .from(UserTable) + .where(and(eq(UserTable.accountID, input.accountID), isNull(UserTable.timeDeleted))) + .then((rows) => rows.map((row) => row.workspaceID)) + if (workspaceIDs.length === 0) return + + const litePayment = await tx + .select({ id: PaymentTable.id }) + .from(PaymentTable) + .where( + and( + inArray(PaymentTable.workspaceID, workspaceIDs), + isNull(PaymentTable.timeDeleted), + sql`JSON_UNQUOTE(JSON_EXTRACT(${PaymentTable.enrichment}, '$.type')) = 'lite'`, + ), + ) + .then((rows) => rows[0]) + if (litePayment) return + const referralID = Identifier.create("referral") await tx.insert(ReferralTable).ignore().values({ workspaceID: code.workspaceID, @@ -355,7 +375,7 @@ export namespace Referral { .from(ReferralTable) .where(and(eq(ReferralTable.inviteeAccountID, invitee.accountID), isNull(ReferralTable.timeDeleted))) .then((rows) => rows[0]) - if (!referral) throw new Error("Referral not found") + if (!referral) return const result = await tx .insert(ReferralRewardTable) @@ -373,7 +393,7 @@ export namespace Referral { }, ]) - if (result.rowsAffected === 0) throw new Error("Referral already completed") + if (result.rowsAffected === 0) return }) } diff --git a/packages/enterprise/vite.config.ts b/packages/enterprise/vite.config.ts index 531732c2be..90f4665c59 100644 --- a/packages/enterprise/vite.config.ts +++ b/packages/enterprise/vite.config.ts @@ -29,6 +29,7 @@ export default defineConfig({ server: { host: "0.0.0.0", allowedHosts: true, + port: 3002, }, worker: { format: "es", diff --git a/packages/stats/app/tsconfig.json b/packages/stats/app/tsconfig.json index 0f96f182ce..e5d8ff3f6c 100644 --- a/packages/stats/app/tsconfig.json +++ b/packages/stats/app/tsconfig.json @@ -17,5 +17,6 @@ "paths": { "~/*": ["./src/*"] } - } + }, + "include": ["*.ts", "src", "../core/src/resource.d.ts"] } diff --git a/sst-env.d.ts b/sst-env.d.ts index 9f6a5db313..aa79ec87d3 100644 --- a/sst-env.d.ts +++ b/sst-env.d.ts @@ -26,6 +26,14 @@ declare module "sst" { "AuthApi": import("@cloudflare/workers-types").Service "AuthStorage": import("@cloudflare/workers-types").KVNamespace "Bucket": import("@cloudflare/workers-types").R2Bucket + "CLOUDFLARE_API_TOKEN": { + "type": "sst.sst.Secret" + "value": string + } + "CLOUDFLARE_DEFAULT_ACCOUNT_ID": { + "type": "sst.sst.Secret" + "value": string + } "Console": { "type": "sst.cloudflare.SolidStart" "url": string @@ -91,37 +99,6 @@ declare module "sst" { "type": "random.index/randomPassword.RandomPassword" "value": string } - "InferenceEvent": { - "catalog": string - "database": string - "region": string - "table": string - "tableBucket": string - "type": "sst.sst.Linkable" - "workgroup": string - } - "LakeIngest": { - "secret": string - "type": "sst.sst.Linkable" - "url": string - } - "LakeIngestConfig": { - "secret": string - "streamName": string - "type": "sst.sst.Linkable" - } - "LakeIngestSecret": { - "type": "random.index/randomPassword.RandomPassword" - "value": string - } - "LakeIngestService": { - "service": string - "type": "sst.aws.Service" - "url": string - } - "LakeVpc": { - "type": "sst.aws.Vpc" - } "LogProcessor": import("@cloudflare/workers-types").Service "R2AccessKey": { "type": "sst.sst.Secret" @@ -156,23 +133,6 @@ declare module "sst" { "value": string } "Stat": import("@cloudflare/workers-types").Service - "StatsDatabase": { - "database": string - "host": string - "password": string - "port": number - "type": "sst.sst.Linkable" - "url": string - "username": string - } - "StatsSyncConfig": { - "dataset": string - "type": "sst.sst.Linkable" - } - "StatsSyncService": { - "service": string - "type": "sst.aws.Service" - } "Teams": { "type": "sst.cloudflare.SolidStart" "url": string