diff --git a/.github/workflows/build-host.yml b/.github/workflows/build-host.yml index 14298af4a3..69b8fc89fb 100644 --- a/.github/workflows/build-host.yml +++ b/.github/workflows/build-host.yml @@ -30,12 +30,14 @@ jobs: echo "MATRIX_URL=https://matrix.boxel.ai" >> $GITHUB_ENV echo "MATRIX_SERVER_NAME=boxel.ai" >> $GITHUB_ENV echo "EXPERIMENTAL_AI_ENABLED=true" >> $GITHUB_ENV + echo "STRIPE_PAYMENT_LINK=https://buy.stripe.com/00g29k8zLgI35xK000" >> $GITHUB_ENV elif [ "$INPUT_ENVIRONMENT" = "staging" ]; then echo "OWN_REALM_URL=https://realms-staging.stack.cards/experiments/" >> $GITHUB_ENV echo "RESOLVED_BASE_REALM_URL=https://realms-staging.stack.cards/base/" >> $GITHUB_ENV echo "MATRIX_URL=https://matrix-staging.stack.cards" >> $GITHUB_ENV echo "MATRIX_SERVER_NAME=stack.cards" >> $GITHUB_ENV echo "EXPERIMENTAL_AI_ENABLED=true" >> $GITHUB_ENV + echo "STRIPE_PAYMENT_LINK=https://buy.stripe.com/test_9AQdUjgaDePb8lWcMN" >> $GITHUB_ENV else echo "unrecognized environment" exit 1; diff --git a/.github/workflows/pr-boxel-host.yml b/.github/workflows/pr-boxel-host.yml index 6929080e8b..4857786c58 100644 --- a/.github/workflows/pr-boxel-host.yml +++ b/.github/workflows/pr-boxel-host.yml @@ -61,6 +61,7 @@ jobs: MATRIX_URL: https://matrix-staging.stack.cards MATRIX_SERVER_NAME: stack.cards EXPERIMENTAL_AI_ENABLED: true + STRIPE_PAYMENT_LINK: https://buy.stripe.com/test_9AQdUjgaDePb8lWcMN S3_PREVIEW_BUCKET_NAME: boxel-host-preview.stack.cards AWS_S3_BUCKET: boxel-host-preview.stack.cards AWS_REGION: us-east-1 @@ -92,6 +93,7 @@ jobs: MATRIX_URL: https://matrix.boxel.ai MATRIX_SERVER_NAME: boxel.ai EXPERIMENTAL_AI_ENABLED: true + STRIPE_PAYMENT_LINK: https://buy.stripe.com/00g29k8zLgI35xK000 S3_PREVIEW_BUCKET_NAME: boxel-host-preview.boxel.ai AWS_S3_BUCKET: boxel-host-preview.boxel.ai AWS_REGION: us-east-1 diff --git a/QUICKSTART.md b/QUICKSTART.md index af2a53fd70..faf6f794ca 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -88,13 +88,11 @@ To build the entire repository and run the application, follow these steps: - Visit SMTP UI at http://localhost:5001/ - Validate email - - Go back to Host and login + - Go back to Host http://localhost:4201/ and login -12. Validate email for login +12. Perform "Setup up Secure Payment Method" flow - - Visit SMTP UI at http://localhost:5001/ - - Validate email - - Go back to Host http://localhost:4201/ and login + - More detailed steps can be found in our [README](README.md) Payment Setup section 13. Run ai bot (Optional): diff --git a/README.md b/README.md index b033c939bc..7c9b4aeb90 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ For a quickstart, see [here](./QUICKSTART.md) - this project uses [volta](https://volta.sh/) for Javascript toolchain version management. Make sure you have the latest verison of volta on your system and have define [the ENV var described here](https://docs.volta.sh/advanced/pnpm). - this project uses [pnpm](https://pnpm.io/) for package management. run `pnpm install` to install the project dependencies first. - this project uses [docker](https://docker.com). Make sure to install docker on your system. +- this project uses [stripe-cli](https://stripe.com/docs/stripe-cli) to test payment flows. Make sure to install stripe-cli on your system. - Ensure that node_modules/.bin is in your path. e.g. include `export PATH="./node_modules/.bin:$PATH"` in your .zshrc ## Orientation @@ -229,6 +230,38 @@ In order to run the boxel-motion demo app: 2. `pnpm start` 3. Visit http://localhost:4200 in your browser +## Payment Setup + +There is some pre-setup needed to enable free plan on development account: + +1. Use stripe cli to listen for the webhooks that Stripe sends to the realm server + +``` +stripe listen --forward-to localhost:4201/_stripe-webhook --api-key sk_test_api_key_from_the_sandbox_account +``` + +2. You will get webhook signing secret from stripe cli after Step 1 is done + +``` +> Ready! You are using Stripe API Version [x]. Your webhook signing secret is whsec_xxxxx +``` + +3. Go to `packages/realm-server` and run the following command to sync the stripe products to the database, make sure you have the stripe api key set. You only need to run this once OR if you want to sync the products again. + +``` +STRIPE_API_KEY=... pnpm sync-stripe-products +``` + +4. Go to `packages/realm-server`, pass STRIPE_WEBHOOK_SECRET & STRIPE_API_KEY environment value and start the realm server. STRIPE_WEBHOOK_SECRET is the value you got from stripe cli in Step 2. + +``` +STRIPE_WEBHOOK_SECRET=... STRIPE_API_KEY=... pnpm start:all +``` + +5. Perform "Setup up Secure Payment Method" flow. Subscribe with valid test card [here](https://docs.stripe.com/testing#cards) + +You should be able to subscribe successfully after you perform the steps above. + ## Running the Tests There are currently 5 test suites: diff --git a/packages/ai-bot/lib/ai-cost.ts b/packages/ai-bot/lib/ai-billing.ts similarity index 84% rename from packages/ai-bot/lib/ai-cost.ts rename to packages/ai-bot/lib/ai-billing.ts index e6386fd2c7..d8cdc9bbb0 100644 --- a/packages/ai-bot/lib/ai-cost.ts +++ b/packages/ai-bot/lib/ai-billing.ts @@ -2,6 +2,7 @@ import { getCurrentActiveSubscription, getUserByMatrixUserId, spendCredits, + sumUpCreditsLedger, } from '@cardstack/billing/billing-queries'; import { PgAdapter, TransactionManager } from '@cardstack/postgres'; import { logger, retry } from '@cardstack/runtime-common'; @@ -60,6 +61,25 @@ export async function saveUsageCost( } } +export async function getAvailableCredits( + pgAdapter: PgAdapter, + matrixUserId: string, +) { + let user = await getUserByMatrixUserId(pgAdapter, matrixUserId); + + if (!user) { + throw new Error( + `should not happen: user with matrix id ${matrixUserId} not found in the users table`, + ); + } + + let availableCredits = await sumUpCreditsLedger(pgAdapter, { + userId: user.id, + }); + + return availableCredits; +} + async function fetchGenerationCost(generationId: string) { let response = await ( await fetch(`https://openrouter.ai/api/v1/generation?id=${generationId}`, { diff --git a/packages/ai-bot/lib/matrix.ts b/packages/ai-bot/lib/matrix.ts index 5d129994e1..dc8399d696 100644 --- a/packages/ai-bot/lib/matrix.ts +++ b/packages/ai-bot/lib/matrix.ts @@ -145,7 +145,7 @@ function getErrorMessage(error: any): string { return `OpenAI error: ${error.name} - ${error.message}`; } if (typeof error === 'string') { - return `Unknown error: ${error}`; + return error; } - return `Unknown error`; + return 'Unknown error'; } diff --git a/packages/ai-bot/main.ts b/packages/ai-bot/main.ts index fd8b82ab29..724e9cc760 100644 --- a/packages/ai-bot/main.ts +++ b/packages/ai-bot/main.ts @@ -25,17 +25,19 @@ import { MatrixClient } from './lib/matrix'; import type { MatrixEvent as DiscreteMatrixEvent } from 'https://cardstack.com/base/matrix-event'; import * as Sentry from '@sentry/node'; -import { saveUsageCost } from './lib/ai-cost'; +import { getAvailableCredits, saveUsageCost } from './lib/ai-billing'; import { PgAdapter } from '@cardstack/postgres'; let log = logger('ai-bot'); let trackAiUsageCostPromises = new Map>(); +const MINIMUM_CREDITS = 10; + class Assistant { private openai: OpenAI; private client: MatrixClient; - private pgAdapter: PgAdapter; + pgAdapter: PgAdapter; id: string; constructor(client: MatrixClient, id: string) { @@ -198,6 +200,12 @@ Common issues are: } const responder = new Responder(client, room.roomId); + if (historyError) { + return responder.finalize( + 'There was an error processing chat history. Please open another session.', + ); + } + await responder.initialize(); // Do not generate new responses if previous ones' cost is still being reported @@ -215,11 +223,15 @@ Common issues are: } } - if (historyError) { - responder.finalize( - 'There was an error processing chat history. Please open another session.', + let availableCredits = await getAvailableCredits( + assistant.pgAdapter, + senderMatrixUserId, + ); + + if (availableCredits < MINIMUM_CREDITS) { + return responder.onError( + `You need a minimum of ${MINIMUM_CREDITS} credits to continue using the AI bot. Please upgrade to a larger plan, or top up your account.`, ); - return; } let generationId: string | undefined; @@ -239,7 +251,6 @@ Common issues are: await responder.onError(error); }); - // We also need to catch the error when getting the final content let finalContent; try { finalContent = await runner.finalContent(); diff --git a/packages/base/card-api.gts b/packages/base/card-api.gts index 362cdb5832..2dc35abf04 100644 --- a/packages/base/card-api.gts +++ b/packages/base/card-api.gts @@ -268,6 +268,7 @@ export async function flushLogs() { export class IdentityContext { readonly identities = new Map(); + readonly errors = new Set(); } type JSONAPIResource = diff --git a/packages/billing/stripe-webhook-handlers/checkout-session-completed.ts b/packages/billing/stripe-webhook-handlers/checkout-session-completed.ts index d176fa5057..6eaee7b948 100644 --- a/packages/billing/stripe-webhook-handlers/checkout-session-completed.ts +++ b/packages/billing/stripe-webhook-handlers/checkout-session-completed.ts @@ -32,9 +32,16 @@ export async function handleCheckoutSessionCompleted( const matrixUserName = event.data.object.client_reference_id; if (matrixUserName) { + // The matrix user id was encoded to be alphanumeric by replacing + with - and / with _ + // Now we need to reverse that encoding to get back the original base64 string + const base64UserId = matrixUserName.replace(/-/g, '+').replace(/_/g, '/'); + const decodedMatrixUserName = Buffer.from( + base64UserId, + 'base64', + ).toString('utf8'); await updateUserStripeCustomerId( dbAdapter, - matrixUserName, + decodedMatrixUserName, stripeCustomerId, ); } diff --git a/packages/boxel-ui/addon/raw-icons/success-bordered.svg b/packages/boxel-ui/addon/raw-icons/success-bordered.svg index 96b76277d6..7bc413ace1 100644 --- a/packages/boxel-ui/addon/raw-icons/success-bordered.svg +++ b/packages/boxel-ui/addon/raw-icons/success-bordered.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/boxel-ui/addon/src/components/tabbed-header/index.gts b/packages/boxel-ui/addon/src/components/tabbed-header/index.gts index f6f30a5386..8011e4a8ab 100644 --- a/packages/boxel-ui/addon/src/components/tabbed-header/index.gts +++ b/packages/boxel-ui/addon/src/components/tabbed-header/index.gts @@ -21,7 +21,7 @@ interface Signature { default: []; headerIcon: []; }; - Element: HTMLDivElement; + Element: HTMLElement; } export default class TabbedHeader extends Component { @@ -32,6 +32,7 @@ export default class TabbedHeader extends Component { header-background-color=@headerBackgroundColor header-text-color=(getContrastColor @headerBackgroundColor) }} + ...attributes >
{{#if (has-block 'headerIcon')}} diff --git a/packages/boxel-ui/addon/src/icons/success-bordered.gts b/packages/boxel-ui/addon/src/icons/success-bordered.gts index e9378bc2c0..98dee8f9d5 100644 --- a/packages/boxel-ui/addon/src/icons/success-bordered.gts +++ b/packages/boxel-ui/addon/src/icons/success-bordered.gts @@ -10,14 +10,14 @@ const IconComponent: TemplateOnlyComponent =