mirror of
https://github.com/anomalyco/opencode.git
synced 2026-06-01 22:10:06 +02:00
feat: improve referral system (#29720)
This commit is contained in:
@@ -2,6 +2,7 @@ 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"
|
||||||
@@ -9,6 +10,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 "./go-referral.css"
|
import "./go-referral.css"
|
||||||
|
|
||||||
type GoReferralSummary = Awaited<ReturnType<typeof Referral.summary>>
|
type GoReferralSummary = Awaited<ReturnType<typeof Referral.summary>>
|
||||||
@@ -25,7 +27,21 @@ const emptyUsagePreview = {
|
|||||||
|
|
||||||
export const queryGoReferral = query(async (workspaceID: string) => {
|
export const queryGoReferral = query(async (workspaceID: string) => {
|
||||||
"use server"
|
"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")
|
}, "go.referral.get")
|
||||||
|
|
||||||
export const queryGoReferralUsagePreview = query(async (workspaceID: string, referralID?: string) => {
|
export const queryGoReferralUsagePreview = query(async (workspaceID: string, referralID?: string) => {
|
||||||
@@ -65,6 +81,7 @@ function rewardDescriptionKey(source: GoReferralReward["source"]) {
|
|||||||
|
|
||||||
function rewardActionKey(reward: GoReferralReward, hasActiveGo: boolean) {
|
function rewardActionKey(reward: GoReferralReward, hasActiveGo: boolean) {
|
||||||
if (reward.status === "applied") return "workspace.referral.reward.action.applied" as const
|
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
|
if (reward.status === "pending" || !hasActiveGo) return "workspace.referral.reward.action.subscribeUnlock" as const
|
||||||
return "workspace.referral.reward.action.view" as const
|
return "workspace.referral.reward.action.view" as const
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { redirect } from "@solidjs/router"
|
import { redirect } from "@solidjs/router"
|
||||||
import type { APIEvent } from "@solidjs/start/server"
|
import type { APIEvent } from "@solidjs/start/server"
|
||||||
import { Referral } from "@opencode-ai/console-core/referral.js"
|
|
||||||
import { AuthClient } from "~/context/auth"
|
import { AuthClient } from "~/context/auth"
|
||||||
import { useAuthSession } from "~/context/auth"
|
import { useAuthSession } from "~/context/auth"
|
||||||
import { i18n } from "~/i18n"
|
import { i18n } from "~/i18n"
|
||||||
import { localeFromRequest, route } from "~/lib/language"
|
import { localeFromRequest, route } from "~/lib/language"
|
||||||
import { clearReferralCookie, referralCodeFromCookieHeader } from "~/lib/referral-invite"
|
|
||||||
|
|
||||||
export async function GET(input: APIEvent) {
|
export async function GET(input: APIEvent) {
|
||||||
const url = new URL(input.request.url)
|
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)
|
if (result.err) throw new Error(result.err.message)
|
||||||
const decoded = AuthClient.decode(result.tokens.access, {} as any)
|
const decoded = AuthClient.decode(result.tokens.access, {} as any)
|
||||||
if (decoded.err) throw new Error(decoded.err.message)
|
if (decoded.err) throw new Error(decoded.err.message)
|
||||||
const referralCode = referralCodeFromCookieHeader(input.request.headers.get("cookie"))
|
|
||||||
const session = await useAuthSession()
|
const session = await useAuthSession()
|
||||||
const id = decoded.subject.properties.accountID
|
const id = decoded.subject.properties.accountID
|
||||||
await session.update((value) => {
|
await session.update((value) => {
|
||||||
@@ -35,15 +32,8 @@ export async function GET(input: APIEvent) {
|
|||||||
current: id,
|
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 next = url.pathname === "/auth/callback" ? "/auth" : url.pathname.replace("/auth/callback", "")
|
||||||
const response = redirect(route(locale, next))
|
return redirect(route(locale, next))
|
||||||
if (referralCode) response.headers.append("set-cookie", clearReferralCookie())
|
|
||||||
return response
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
server: {
|
server: {
|
||||||
allowedHosts: true,
|
allowedHosts: true,
|
||||||
|
port: 3001,
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { z } from "zod"
|
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 { Actor } from "./actor"
|
||||||
import { Identifier } from "./identifier"
|
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 { ReferralCodeTable, ReferralRewardTable, ReferralTable } from "./schema/referral.sql"
|
||||||
import { AuthTable } from "./schema/auth.sql"
|
import { AuthTable } from "./schema/auth.sql"
|
||||||
import { UserTable } from "./schema/user.sql"
|
import { UserTable } from "./schema/user.sql"
|
||||||
@@ -318,6 +318,26 @@ export namespace Referral {
|
|||||||
.then((rows) => rows[0])
|
.then((rows) => rows[0])
|
||||||
if (selfReferral) throw new Error("Self-referral is not allowed")
|
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")
|
const referralID = Identifier.create("referral")
|
||||||
await tx.insert(ReferralTable).ignore().values({
|
await tx.insert(ReferralTable).ignore().values({
|
||||||
workspaceID: code.workspaceID,
|
workspaceID: code.workspaceID,
|
||||||
@@ -355,7 +375,7 @@ export namespace Referral {
|
|||||||
.from(ReferralTable)
|
.from(ReferralTable)
|
||||||
.where(and(eq(ReferralTable.inviteeAccountID, invitee.accountID), isNull(ReferralTable.timeDeleted)))
|
.where(and(eq(ReferralTable.inviteeAccountID, invitee.accountID), isNull(ReferralTable.timeDeleted)))
|
||||||
.then((rows) => rows[0])
|
.then((rows) => rows[0])
|
||||||
if (!referral) throw new Error("Referral not found")
|
if (!referral) return
|
||||||
|
|
||||||
const result = await tx
|
const result = await tx
|
||||||
.insert(ReferralRewardTable)
|
.insert(ReferralRewardTable)
|
||||||
@@ -373,7 +393,7 @@ export namespace Referral {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
if (result.rowsAffected === 0) throw new Error("Referral already completed")
|
if (result.rowsAffected === 0) return
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
allowedHosts: true,
|
allowedHosts: true,
|
||||||
|
port: 3002,
|
||||||
},
|
},
|
||||||
worker: {
|
worker: {
|
||||||
format: "es",
|
format: "es",
|
||||||
|
|||||||
@@ -17,5 +17,6 @@
|
|||||||
"paths": {
|
"paths": {
|
||||||
"~/*": ["./src/*"]
|
"~/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"include": ["*.ts", "src", "../core/src/resource.d.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+8
-48
@@ -26,6 +26,14 @@ declare module "sst" {
|
|||||||
"AuthApi": import("@cloudflare/workers-types").Service
|
"AuthApi": import("@cloudflare/workers-types").Service
|
||||||
"AuthStorage": import("@cloudflare/workers-types").KVNamespace
|
"AuthStorage": import("@cloudflare/workers-types").KVNamespace
|
||||||
"Bucket": import("@cloudflare/workers-types").R2Bucket
|
"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": {
|
"Console": {
|
||||||
"type": "sst.cloudflare.SolidStart"
|
"type": "sst.cloudflare.SolidStart"
|
||||||
"url": string
|
"url": string
|
||||||
@@ -91,37 +99,6 @@ declare module "sst" {
|
|||||||
"type": "random.index/randomPassword.RandomPassword"
|
"type": "random.index/randomPassword.RandomPassword"
|
||||||
"value": string
|
"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
|
"LogProcessor": import("@cloudflare/workers-types").Service
|
||||||
"R2AccessKey": {
|
"R2AccessKey": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
@@ -156,23 +133,6 @@ declare module "sst" {
|
|||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
"Stat": import("@cloudflare/workers-types").Service
|
"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": {
|
"Teams": {
|
||||||
"type": "sst.cloudflare.SolidStart"
|
"type": "sst.cloudflare.SolidStart"
|
||||||
"url": string
|
"url": string
|
||||||
|
|||||||
Reference in New Issue
Block a user