Skip to content

Commit

Permalink
Merge pull request #4 from harishdeivanayagam/feature/cloud-host-license
Browse files Browse the repository at this point in the history
feat: cloud host license
  • Loading branch information
harishdeivanayagam authored Jan 17, 2025
2 parents ef6091a + 1fa5678 commit a0fa16b
Show file tree
Hide file tree
Showing 12 changed files with 387 additions and 66 deletions.
8 changes: 8 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
Copyright (c) 2024-present Harish Deivanayagam.

Portions of this software are licensed as follows:

* All content that resides under the "ee/" directory of this repository, if that directory exists, is licensed under the license defined in "ee/LICENSE".
* All third party components incorporated into the Rowfill Software are licensed under the original license provided by the owner of the applicable component.
* Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below.

GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007

Expand Down
36 changes: 36 additions & 0 deletions ee/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
The Rowfill Enterprise license (the “Enterprise License”)
Copyright (c) 2024-2025 Harish Deivanayagam.

With regard to the Rowfill Software:

This software and associated documentation files (the "Software") may only be
used in production, if you (and any entity that you represent) have agreed to,
and are in compliance with, the Rowfill Subscription Terms of Service, available
at https://cloud.rowfill.com/terms (the “Enterprise Terms”), or other
agreement governing the use of the Software, as agreed by you and Rowfill,
and otherwise have a valid Rowfill Enterprise license for the
correct number of user seats. Subject to the foregoing sentence, you are free to
modify this Software and publish patches to the Software. You agree that Rowfill
and/or its licensors (as applicable) retain all right, title and interest in and
to all such modifications and/or patches, and all such modifications and/or
patches may only be used, copied, modified, displayed, distributed, or otherwise
exploited with a valid Rowfill Enterprise license for the correct
number of user seats. Notwithstanding the foregoing, you may copy and modify
the Software for development and testing purposes, without requiring a
subscription. You agree that Rowfill and/or its licensors (as applicable) retain
all right, title and interest in and to all such modifications. You are not
granted any other rights beyond what is expressly stated herein. Subject to the
foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
and/or sell the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

For all third party components incorporated into the Rowfill Software, those
components are licensed under the original license provided by the owner of the
applicable component.
15 changes: 15 additions & 0 deletions prisma/migrations/20250117105538_billing_credits/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-- CreateTable
CREATE TABLE "Billing" (
"organizationId" TEXT NOT NULL,
"credits" INTEGER NOT NULL DEFAULT 1000,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "Billing_pkey" PRIMARY KEY ("organizationId")
);

-- CreateIndex
CREATE UNIQUE INDEX "Billing_organizationId_key" ON "Billing"("organizationId");

-- AddForeignKey
ALTER TABLE "Billing" ADD CONSTRAINT "Billing_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- CreateEnum
CREATE TYPE "Plan" AS ENUM ('FREE', 'PRO', 'ENTERPRISE');

-- AlterTable
ALTER TABLE "Billing" ADD COLUMN "expiresAt" TIMESTAMP(3),
ADD COLUMN "plan" "Plan" NOT NULL DEFAULT 'FREE';
17 changes: 17 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ enum ColumnDataType {
OBJECT
}

enum Plan {
FREE
PRO
ENTERPRISE
}

model User {
id String @id @default(cuid())
name String?
Expand Down Expand Up @@ -56,6 +62,7 @@ model Organization {
sheetColumnValues SheetColumnValue[]
indexedSources IndexedSource[]
extractedSheetRows ExtractedSheetRow[]
billing Billing?
}

model Member {
Expand All @@ -68,6 +75,16 @@ model Member {
@@id([userId, organizationId])
}

model Billing {
organizationId String @id @unique
organization Organization @relation(fields: [organizationId], references: [id])
credits Int @default(1000) // 1 credit = 1 Column Task or 10 Credits for 1 row extraction
plan Plan @default(FREE)
expiresAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

model Sheet {
id String @id @default(uuid())
name String
Expand Down
2 changes: 2 additions & 0 deletions src/app/auth/signup/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export async function signupUser(data: { name: string, email: string, organizati
},
})
})


return { success: true }
} catch (error) {
logger.error('Signup error:', error)
Expand Down
92 changes: 92 additions & 0 deletions src/app/console/ee/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"use server"

import { getAuthToken } from "@/lib/auth"
import { prisma } from "@/lib/prisma"

export async function getBillingAndCreateIfNotExists() {
const { organizationId } = await getAuthToken()

const cloudHosted = process.env.EE_ENABLED && process.env.EE_ENABLED === "true"

if (!cloudHosted) {
return null
}

let billing = await prisma.billing.findFirst({
where: {
organizationId: organizationId
}
})

if (!billing && process.env.EE_ENABLED && process.env.EE_ENABLED === "true") {
await prisma.billing.create({
data: {
organizationId: organizationId
}
})

billing = await prisma.billing.findFirst({
where: {
organizationId: organizationId
}
})
}

return billing
}

export async function getPlans() {

const cloudHosted = process.env.EE_ENABLED && process.env.EE_ENABLED === "true"

if (!cloudHosted) {
return []
}

// TODO: Implement plans
return [
{
name: "FREE",
price: "0",
credits: "1000",
for: "lifetime",
purchaseUrl: "",
},
{
name: "PRO_MONTHLY",
price: "35",
credits: "15000",
for: "monthly",
purchaseUrl: "", // TODO: Stripe Link
},
{
name: "PRO_YEARLY",
price: "350",
credits: "15000",
for: "yearly",
purchaseUrl: "", // TODO: Stripe Link
},
{
name: "ENTERPRISE",
price: "Custom",
credits: "100000+",
for: "Contract",
purchaseUrl: "", // TODO: Calendar Link
}
]
}

export async function handleDowngradeToFree() {
const { organizationId } = await getAuthToken()

await prisma.billing.update({
where: {
organizationId: organizationId
},
data: {
plan: "FREE"
}
})

return true
}
101 changes: 101 additions & 0 deletions src/app/console/ee/billing.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"use client"

import { Billing } from "@prisma/client"
import { useEffect, useState } from "react"
import { getBillingAndCreateIfNotExists, getPlans, handleDowngradeToFree } from "./actions"
import { Button } from "@/components/ui/button"
import { useToast } from "@/hooks/use-toast"
import { useRouter } from "next/navigation"

export default function BillingComponent() {

type Plan = {
name: string
price: string
credits: string
for: string
purchaseUrl: string
}

const [billing, setBilling] = useState<Billing | null>(null)
const [plans, setPlans] = useState<Plan[]>([])
const { toast } = useToast()
const router = useRouter()

useEffect(() => {
fetchData()
}, [])

const fetchData = async () => {
const billing = await getBillingAndCreateIfNotExists()
const plans = await getPlans()
setBilling(billing)
setPlans(plans)
}

const planButtonText = (plan: Plan) => {
if (billing && billing.plan === plan.name) {
return "Current Plan"
}

if (billing && billing.plan === "PRO" && plan.name === "FREE") {
return "Downgrade"
}

if (billing && billing.plan === "FREE" && plan.name.includes("PRO_")) {
return "Upgrade"
}

if (billing && billing.plan === "ENTERPRISE") {
return "Contact Support"
}

return "Contact Support"
}

const handlePlanButton = async (plan: Plan) => {
if (billing && billing.plan === "FREE" && plan.name !== "FREE") {
router.push(plan.purchaseUrl)
return
}

if (billing && (billing.plan === "FREE" || billing.plan === "PRO") && plan.name === "ENTERPRISE") {
router.push(plan.purchaseUrl)
return
}

if (billing && (billing.plan.includes("PRO_")) && plan.name === "FREE") {
// Handle Downgrade
await handleDowngradeToFree()
toast({
title: "Downgraded to Free",
description: "You have been downgraded to the Free plan",
})
return
}

toast({
title: "Failed to update plan",
description: "Please contact support",
variant: "destructive",
})
}

return (
<div>
{/* TODO: Billing goes here */}
{plans.map((plan, index) => (
<div key={`plan-${index}`} className="flex justify-between items-center p-2 rounded-md border-b-[1px] border-gray-200">
<div className="flex items-center gap-3">
<p className="text-xl flex items-center gap-2 font-bold">{plan.price} / {plan.for}</p>
<div className="flex flex-col gap-1">
<p className="text-lg">{plan.name}</p>
<p className="text-sm">{plan.credits}/month</p>
</div>
</div>
<Button disabled={planButtonText(plan) === "Current Plan" || planButtonText(plan) === "Contact Support"} onClick={() => handlePlanButton(plan)}>{planButtonText(plan)}</Button>
</div>
))}
</div>
)
}
6 changes: 6 additions & 0 deletions src/app/console/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import SourcesDialog from './sourcesDialog'
import { useSheetStore } from './shared'
import SearchDialog from './searchDialog'
import Image from 'next/image'
import { getBillingAndCreateIfNotExists } from './ee/actions'

export default function ConsoleLayoutContent({ children }: { children: React.ReactNode }) {
const [isAddProjectOpen, setIsAddProjectOpen] = useState(false)
Expand All @@ -27,6 +28,8 @@ export default function ConsoleLayoutContent({ children }: { children: React.Rea
init()
}, [])



useEffect(() => {
// Check if /sheets/slug pattern is in the pathname
if (sheets.length > 0 && !pathname.includes('/sheets/')) {
Expand All @@ -47,6 +50,9 @@ export default function ConsoleLayoutContent({ children }: { children: React.Rea
if (!auth) {
redirect('/auth/login')
}
if (process.env.NEXT_PUBLIC_EE_ENABLED && process.env.NEXT_PUBLIC_EE_ENABLED === "true") {
await getBillingAndCreateIfNotExists()
}
await handleFetchSheets()
}

Expand Down
2 changes: 1 addition & 1 deletion src/app/console/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export default function ConsoleDashboard() {
return <div className="flex items-center justify-center h-screen text-2xl font-bold">Console</div>
return <div className="flex items-center justify-center h-screen text-sm text-muted-foreground">No sheets found</div>
}
Loading

0 comments on commit a0fa16b

Please sign in to comment.