Skip to content

Commit 312f66d

Browse files
authored
Paddle payment 4 (#435)
* Refactor PricingPage component with new layout and plan selection functionality * Add create subscription functionality and update PricingPage integration * Implement Paddle pricing retrieval and integrate with PricingPage * feat-frontend: integrate Paddle Billing checkout with prefilled Clerk email * update:ebs-common-stage-prod.yml to load vite_paddle envs from secrets * update package-lock.json * chore: configure Paddle env injection during Docker build and CI workflow * chore:add temporary go to flows button in pricing UI * update auth gaurd * update auth gaurd * update auth gaurd * update pricing page * fix: persist isOrgSelected flag across routes * fix:persist isOrgselected flag across routes * fix: update token fetching logic to skip cache and include organization ID * fix: sync org-scoped token on user sign-in and organization selection * fix: resolve Ruff lint issues and optimize API calls with caching * fix: update plan configuration loading and add new pricing plans
1 parent 779867e commit 312f66d

18 files changed

Lines changed: 737 additions & 163 deletions

File tree

.github/workflows/ebs-common-stage-prod.yml

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,10 @@ jobs:
122122
PADDLE_ENVIRONMENT="prod"
123123
fi
124124
125-
if [ -z "$PADDLE_API_KEY" ] || [ -z "$PADDLE_CLIENT_KEY" ]; then
125+
VITE_PADDLE_CLIENT_KEY="$PADDLE_CLIENT_KEY"
126+
VITE_PADDLE_ENVIRONMENT="$PADDLE_ENVIRONMENT"
127+
128+
if [ -z "$PADDLE_API_KEY" ] || [ -z "$PADDLE_CLIENT_KEY" ] || [ -z "$PADDLE_ENVIRONMENT" ]; then
126129
echo "❌ Missing Paddle secrets for ${ENVIRONMENT} environment"
127130
exit 1
128131
fi
@@ -147,6 +150,18 @@ jobs:
147150
echo "PADDLE_ENVIRONMENT=${PADDLE_ENVIRONMENT}" >> "$ENV_FILE"
148151
fi
149152
153+
if grep -q '^VITE_PADDLE_CLIENT_KEY=' "$ENV_FILE"; then
154+
sed -i "s|^VITE_PADDLE_CLIENT_KEY=.*|VITE_PADDLE_CLIENT_KEY=${VITE_PADDLE_CLIENT_KEY}|" "$ENV_FILE"
155+
else
156+
echo "VITE_PADDLE_CLIENT_KEY=${VITE_PADDLE_CLIENT_KEY}" >> "$ENV_FILE"
157+
fi
158+
159+
if grep -q '^VITE_PADDLE_ENVIRONMENT=' "$ENV_FILE"; then
160+
sed -i "s|^VITE_PADDLE_ENVIRONMENT=.*|VITE_PADDLE_ENVIRONMENT=${PADDLE_ENVIRONMENT}|" "$ENV_FILE"
161+
else
162+
echo "VITE_PADDLE_ENVIRONMENT=${PADDLE_ENVIRONMENT}" >> "$ENV_FILE"
163+
fi
164+
150165
- name: Prepare Clerk API key
151166
env:
152167
ENVIRONMENT: ${{ github.event.inputs.environment }}
@@ -332,6 +347,8 @@ jobs:
332347
VITE_CLERK_AUTH_ENABLED=true \
333348
VITE_CLERK_PUBLISHABLE_KEY=${{ secrets.CLERK_PUBLISHABLE_KEY }} \
334349
VITE_CLERK_FRONTEND_API=${{ vars.STAGING_CLERK_FRONTEND_API }} \
350+
VITE_PADDLE_CLIENT_KEY=${{ secrets.STAGING_PADDLE_CLIENT_KEY }} \
351+
VITE_PADDLE_ENVIRONMENT=${{ github.event.inputs.environment }} \
335352
make docker_build TAG=langflow:staging-$BRANCH_NAME-$COMMIT_ID DOCKER=docker
336353
337354
docker tag langflow:staging-$BRANCH_NAME-$COMMIT_ID forwardemailforaifirstdesk/langflow:staging-$BRANCH_NAME-$COMMIT_ID
@@ -350,6 +367,8 @@ jobs:
350367
VITE_CLERK_AUTH_ENABLED=true \
351368
VITE_CLERK_PUBLISHABLE_KEY=${{ secrets.PROD_CLERK_PUBLISHABLE_KEY }} \
352369
VITE_CLERK_FRONTEND_API=${{ vars.PROD_CLERK_FRONTEND_API }} \
370+
VITE_PADDLE_CLIENT_KEY=${{ secrets.PROD_PADDLE_CLIENT_KEY }} \
371+
VITE_PADDLE_ENVIRONMENT=${{ github.event.inputs.environment }} \
353372
make docker_build TAG=langflow:prod-$BRANCH_NAME-$COMMIT_ID DOCKER=docker
354373
355374
docker tag langflow:prod-$BRANCH_NAME-$COMMIT_ID forwardemailforaifirstdesk/langflow:prod-$BRANCH_NAME-$COMMIT_ID

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ DOCKER_COMPOSE=docker_example/docker-compose.yml
1010

1111
VITE_CLERK_AUTH_ENABLED ?= false
1212
VITE_CLERK_PUBLISHABLE_KEY ?=
13+
VITE_PADDLE_CLIENT_KEY ?=
14+
VITE_PADDLE_ENVIRONMENT ?=
1315
PYTHON_REQUIRED=$(shell grep '^requires-python[[:space:]]*=' pyproject.toml | sed -n 's/.*"\([^"]*\)".*/\1/p')
1416
RED=\033[0;31m
1517
NC=\033[0m # No Color
@@ -342,6 +344,8 @@ dockerfile_build:
342344
--build-arg VITE_CLERK_AUTH_ENABLED=${VITE_CLERK_AUTH_ENABLED} \
343345
--build-arg VITE_CLERK_PUBLISHABLE_KEY=${VITE_CLERK_PUBLISHABLE_KEY} \
344346
--build-arg VITE_AUTO_LOGIN=$(VITE_AUTO_LOGIN) \
347+
--build-arg VITE_PADDLE_CLIENT_KEY=${VITE_PADDLE_CLIENT_KEY} \
348+
--build-arg VITE_PADDLE_ENVIRONMENT=${VITE_PADDLE_ENVIRONMENT} \
345349
-t $(TAG) .
346350

347351
dockerfile_build_be: dockerfile_build

docker/build_and_push.Dockerfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,12 @@ WORKDIR /tmp/src/frontend
6060

6161
ARG VITE_CLERK_AUTH_ENABLED=false
6262
ARG VITE_CLERK_PUBLISHABLE_KEY=""
63+
ARG VITE_PADDLE_CLIENT_KEY=""
64+
ARG VITE_PADDLE_ENVIRONMENT=""
6365
ENV VITE_CLERK_AUTH_ENABLED=$VITE_CLERK_AUTH_ENABLED
6466
ENV VITE_CLERK_PUBLISHABLE_KEY=$VITE_CLERK_PUBLISHABLE_KEY
67+
ENV VITE_PADDLE_CLIENT_KEY=$VITE_PADDLE_CLIENT_KEY
68+
ENV VITE_PADDLE_ENVIRONMENT=$VITE_PADDLE_ENVIRONMENT
6569

6670
RUN --mount=type=cache,target=/root/.npm \
6771
npm ci \

scripts/localsetup/local-docker-test.sh

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,17 +98,26 @@ show_verbose_help() {
9898
⚙️ Prerequisites:
9999
1. Docker must be installed and running
100100
2. .env-file must exist in the project root
101-
3. VITE_CLERK_PUBLISHABLE_KEY environment variable must be set
101+
3. Clerk and Paddle frontend environment variables must be set
102102
103103
🔑 Required Environment Variable:
104104
VITE_CLERK_PUBLISHABLE_KEY - Clerk publishable key (required for build)
105+
VITE_CLERK_FRONTEND_API - Clerk frontend API host (required for build)
106+
VITE_PADDLE_CLIENT_KEY - Paddle client-side token (required for build)
107+
VITE_PADDLE_ENVIRONMENT - Paddle environment: staging or production (required for build)
105108
106109
To set it, run:
107110
export VITE_CLERK_PUBLISHABLE_KEY="pk_test_your_key_here"
111+
export VITE_CLERK_FRONTEND_API="clerk.yourapp.dev"
112+
export VITE_PADDLE_CLIENT_KEY="test_XXXXXXXXXXXXXXXX"
113+
export VITE_PADDLE_ENVIRONMENT="staging"
108114
109115
💡 Examples:
110116
# Set the required environment variable
111117
export VITE_CLERK_PUBLISHABLE_KEY="pk_test_xxxxxxxxxxxx"
118+
export VITE_CLERK_FRONTEND_API="clerk.localhost.dev"
119+
export VITE_PADDLE_CLIENT_KEY="test_XXXXXXXXXXXXXXXX"
120+
export VITE_PADDLE_ENVIRONMENT="staging"
112121
113122
# Build the Docker image with default timestamp tag
114123
./local-docker-test.sh build
@@ -199,8 +208,8 @@ display_multiline_command() {
199208
echo ""
200209
}
201210

202-
# Check if VITE_CLERK_PUBLISHABLE_KEY is set
203-
check_clerk_envs() {
211+
# Check required frontend environment variables for Docker build
212+
check_required_frontend_envs() {
204213
local missing=false
205214

206215
echo ""
@@ -254,11 +263,67 @@ check_clerk_envs() {
254263
fi
255264
}
256265

266+
# Check Paddle variables together
267+
check_paddle_envs() {
268+
local missing=false
269+
270+
echo ""
271+
echo "🔍 Checking Paddle environment variables..."
272+
273+
if [ -z "${VITE_PADDLE_CLIENT_KEY:-}" ] && [ -z "${VITE_PADDLE_ENVIRONMENT:-}" ]; then
274+
echo "❌ Both Paddle environment variables are missing!"
275+
echo ""
276+
echo "You must set both before continuing:"
277+
echo " export VITE_PADDLE_CLIENT_KEY=\"test_your_client_key_here\""
278+
echo " export VITE_PADDLE_ENVIRONMENT=\"staging\""
279+
echo ""
280+
echo "💡 Example:"
281+
echo " export VITE_PADDLE_CLIENT_KEY=\"test_12345ABCDE\""
282+
echo " export VITE_PADDLE_ENVIRONMENT=\"production\""
283+
echo ""
284+
missing=true
285+
286+
elif [ -z "${VITE_PADDLE_CLIENT_KEY:-}" ]; then
287+
echo "❌ VITE_PADDLE_CLIENT_KEY is missing!"
288+
echo "✅ VITE_PADDLE_ENVIRONMENT is set to: ${VITE_PADDLE_ENVIRONMENT}"
289+
echo ""
290+
echo "Set it using:"
291+
echo " export VITE_PADDLE_CLIENT_KEY=\"test_your_client_key_here\""
292+
echo ""
293+
missing=true
294+
295+
elif [ -z "${VITE_PADDLE_ENVIRONMENT:-}" ]; then
296+
echo "❌ VITE_PADDLE_ENVIRONMENT is missing!"
297+
echo "✅ VITE_PADDLE_CLIENT_KEY is set"
298+
echo ""
299+
echo "Set it using:"
300+
echo " export VITE_PADDLE_ENVIRONMENT=\"staging\""
301+
echo ""
302+
echo "💡 Valid values: staging, production"
303+
echo ""
304+
missing=true
305+
306+
else
307+
echo "✅ Both Paddle environment variables are set"
308+
echo " - VITE_PADDLE_CLIENT_KEY: ${VITE_PADDLE_CLIENT_KEY}"
309+
echo " - VITE_PADDLE_ENVIRONMENT: ${VITE_PADDLE_ENVIRONMENT}"
310+
fi
311+
312+
if [ "$missing" = true ]; then
313+
echo ""
314+
echo "❌ Please fix the missing Paddle variables above before continuing."
315+
echo ""
316+
exit 1
317+
fi
318+
}
319+
257320
# Configuration from GitHub workflow (staging environment)
258321
VITE_AUTO_LOGIN=false
259322
VITE_CLERK_AUTH_ENABLED=true
260323
VITE_CLERK_PUBLISHABLE_KEY="${VITE_CLERK_PUBLISHABLE_KEY:-}"
261324
VITE_CLERK_FRONTEND_API="${VITE_CLERK_FRONTEND_API:-}"
325+
VITE_PADDLE_CLIENT_KEY="${VITE_PADDLE_CLIENT_KEY:-}"
326+
VITE_PADDLE_ENVIRONMENT="${VITE_PADDLE_ENVIRONMENT:-}"
262327

263328
# Docker image details
264329
IMAGE_NAME="langflow"
@@ -275,8 +340,9 @@ POSTGRES_PORT="5432"
275340

276341
# Function to build Docker image
277342
build_docker() {
278-
# Check Clerk environment variables together
279-
check_clerk_envs
343+
# Check required frontend environment variables
344+
check_required_frontend_envs
345+
check_paddle_envs
280346

281347
# Ask for custom tag
282348
echo ""
@@ -314,6 +380,8 @@ build_docker() {
314380
VITE_CLERK_AUTH_ENABLED=${VITE_CLERK_AUTH_ENABLED} \
315381
VITE_CLERK_PUBLISHABLE_KEY=${VITE_CLERK_PUBLISHABLE_KEY} \
316382
VITE_CLERK_FRONTEND_API=${VITE_CLERK_FRONTEND_API} \
383+
VITE_PADDLE_CLIENT_KEY=${VITE_PADDLE_CLIENT_KEY} \
384+
VITE_PADDLE_ENVIRONMENT=${VITE_PADDLE_ENVIRONMENT} \
317385
make docker_build"
318386
display_command "$BUILD_CMD" "Building Docker image"
319387

@@ -323,6 +391,8 @@ build_docker() {
323391
VITE_CLERK_AUTH_ENABLED=${VITE_CLERK_AUTH_ENABLED} \
324392
VITE_CLERK_PUBLISHABLE_KEY=${VITE_CLERK_PUBLISHABLE_KEY} \
325393
VITE_CLERK_FRONTEND_API=${VITE_CLERK_FRONTEND_API} \
394+
VITE_PADDLE_CLIENT_KEY=${VITE_PADDLE_CLIENT_KEY} \
395+
VITE_PADDLE_ENVIRONMENT=${VITE_PADDLE_ENVIRONMENT} \
326396
make docker_build
327397

328398
echo ""

src/backend/base/langflow/api/v1/billing.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from fastapi import APIRouter, HTTPException
2+
from lfx.log.logger import logger
23
from pydantic import BaseModel
34

4-
from lfx.log.logger import logger
55
from langflow.api.utils import CurrentActiveUser
66
from langflow.services.auth.clerk_utils import (
77
get_clerk_user_id_from_payload,
@@ -10,6 +10,7 @@
1010
get_paddle_subscription_id_from_clerk_payload,
1111
)
1212
from langflow.services.deps import get_settings_service
13+
from langflow.services.paddle.provisioning import get_paddle_prices
1314
from langflow.services.paddle.subscriptions import (
1415
ensure_paddle_customer_for_user,
1516
has_active_subscription,
@@ -21,7 +22,6 @@
2122
class EnsurePaddleCustomerRequest(BaseModel):
2223
email: str | None = None
2324

24-
2525
@router.post("/ensure-paddle-customer")
2626
async def ensure_paddle_customer(
2727
current_user: CurrentActiveUser,
@@ -102,4 +102,15 @@ async def get_org_access(
102102
**subscription_access,
103103
"is_admin": is_admin,
104104
"reason": "subscription_inactive",
105-
}
105+
}
106+
107+
@router.get("/paddle-prices")
108+
async def list_paddle_prices():
109+
"""Return a mapping of plan_key -> Paddle price_id."""
110+
try:
111+
return await get_paddle_prices()
112+
except ValueError as exc:
113+
raise HTTPException(status_code=404, detail=str(exc)) # noqa: B904
114+
except Exception as exc: #noqa: BLE001
115+
logger.exception(f"Error fetching Paddle prices {exc}")
116+
raise HTTPException(status_code=500, detail="Internal server error") #noqa: B904

src/backend/base/langflow/services/auth/clerk_utils.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import uuid
2-
from datetime import datetime, timedelta, timezone
32
from contextvars import ContextVar, Token
3+
from datetime import datetime, timedelta, timezone
44
from typing import Any, Literal
5-
from uuid import UUID, NAMESPACE_URL, uuid5
5+
from uuid import NAMESPACE_URL, UUID, uuid5
66

77
import httpx
88
from fastapi import HTTPException, Request, status
@@ -12,20 +12,20 @@
1212
from starlette.status import HTTP_401_UNAUTHORIZED
1313

1414
from langflow.logging.logger import logger
15-
from langflow.services.database.constants import DUMMY_USER_NAMESPACE_TEMPLATE
16-
from langflow.services.database.models.user import User
17-
from langflow.services.database.models.user.crud import get_user_by_id
18-
from langflow.services.deps import get_settings_service
1915
from langflow.services.auth.clerk_metadata_constants import (
20-
CLERK_JWT_SUB_KEY,
2116
CLERK_JWT_EMAIL_KEY,
2217
CLERK_JWT_ORG_KEY,
18+
CLERK_JWT_SUB_KEY,
2319
CLERK_JWT_UUID_KEY,
20+
ORG_ID_KEY,
21+
ORGANISATION_CREATED_BY_KEY,
2422
PADDLE_CUSTOMER_ID_KEY,
2523
PADDLE_SUBSCRIPTION_ID_KEY,
26-
ORGANISATION_CREATED_BY_KEY,
27-
ORG_ID_KEY,
2824
)
25+
from langflow.services.database.constants import DUMMY_USER_NAMESPACE_TEMPLATE
26+
from langflow.services.database.models.user import User
27+
from langflow.services.database.models.user.crud import get_user_by_id
28+
from langflow.services.deps import get_settings_service
2929

3030
# Context variable to store decoded clerk claims per request
3131
auth_header_ctx: ContextVar[dict | None] = ContextVar("auth_header_ctx", default=None)
@@ -63,7 +63,8 @@ def _get_headers(self) -> dict:
6363
secret_key = settings_service.settings.clerk_api_key
6464
secret_key = secret_key.strip()
6565
if not secret_key:
66-
raise RuntimeError("CLERK_API_KEY not configured")
66+
msg = "CLERK_API_KEY not configured"
67+
raise RuntimeError(msg)
6768
return {
6869
"Authorization": f"Bearer {secret_key}",
6970
"Content-Type": "application/json",
@@ -103,7 +104,7 @@ async def update_public_metadata(self, updates: dict) -> dict:
103104
merged = self.merge_metadata(existing, updates)
104105
await self.set_public_metadata(merged)
105106
return merged
106-
107+
107108

108109
async def update_clerk_organization(
109110
*,
@@ -112,7 +113,7 @@ async def update_clerk_organization(
112113
max_allowed_members: int | None = None,
113114
) -> None:
114115
"""Update a Clerk organization's public metadata and/or seat limit."""
115-
logger.info(f"Updating Clerk organization {org_id}: public_metadata={public_metadata}, max_allowed_members={max_allowed_members}")
116+
logger.info(f"Updating Clerk organization {org_id}: public_metadata={public_metadata}, max_allowed_members={max_allowed_members}") #noqa: E501
116117
manager = ClerkPublicMetadataManager.for_organization(org_id)
117118

118119
if public_metadata is not None:
@@ -169,8 +170,7 @@ def get_email_from_clerk_payload() -> str:
169170

170171

171172
async def get_paddle_customer_id_from_clerk_payload() -> str | None:
172-
"""
173-
Retrieve paddle_customer_id directly from Clerk JWT public metadata.
173+
"""Retrieve paddle_customer_id directly from Clerk JWT public metadata.
174174
175175
Assumes the JWT template includes:
176176
{

0 commit comments

Comments
 (0)