Skip to content

Commit

Permalink
Merge branch 'main' into glimmer-scoped-css-0.7.0
Browse files Browse the repository at this point in the history
  • Loading branch information
backspace committed Dec 12, 2024
2 parents 18c214e + e1c4286 commit 9686d30
Show file tree
Hide file tree
Showing 163 changed files with 6,875 additions and 2,761 deletions.
12 changes: 11 additions & 1 deletion .github/workflows/manual-vscode-boxel-tools.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ jobs:
- name: Package
run: pnpm vscode:package
working-directory: packages/vscode-boxel-tools
- name: Publish
- name: Publish to Visual Studio Marketplace
run: |
if [ "${{ inputs.environment }}" = "production" ]; then
pnpm vscode:publish
Expand All @@ -104,3 +104,13 @@ jobs:
working-directory: packages/vscode-boxel-tools
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
- name: Publish to Open VSX
run: |
if [ "${{ inputs.environment }}" = "production" ]; then
npx ovsx publish --no-dependencies --pat $OVSX_TOKEN
else
npx ovsx publish --no-dependencies --pre-release --pat $OVSX_TOKEN
fi
working-directory: packages/vscode-boxel-tools
env:
OVSX_TOKEN: ${{ secrets.OVSX_TOKEN }}
9 changes: 8 additions & 1 deletion packages/ai-bot/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,14 @@ export function constructHistory(
try {
rawEvent.content.data = JSON.parse(rawEvent.content.data);
} catch (e) {
Sentry.captureException(e);
Sentry.captureException(e, {
attachments: [
{
data: rawEvent.content.data,
filename: 'rawEventContentData.txt',
},
],
});
log.error('Error parsing JSON', e);
throw new HistoryConstructionError((e as Error).message);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/base/base64-image.gts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ class Edit extends Component<typeof Base64ImageField> {
// this allows multiple radio groups rendered on the page
// to stay independent of one another.
let groupNumber = 0;
class ImageSizeField extends FieldDef {
export class ImageSizeField extends FieldDef {
static displayName = 'Image Size';
static [primitive]: 'actual' | 'contain' | 'cover';
static [useIndexBasedKey]: never;
Expand Down
18 changes: 16 additions & 2 deletions packages/base/cards-grid.gts
Original file line number Diff line number Diff line change
Expand Up @@ -286,8 +286,22 @@ class Isolated extends Component<typeof CardsGrid> {
display: none;
}
.instance-error {
opacity: 0.33;
color: var(--boxel-error-100);
position: relative;
}
.instance-error::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 0, 0, 0.1);
}
.instance-error .boundaries {
box-shadow: 0 0 0 1px var(--boxel-error-300);
}
.instance-error:hover .boundaries {
box-shadow: 0 0 0 1px var(--boxel-dark);
}
</style>
</template>
Expand Down
15 changes: 15 additions & 0 deletions packages/base/command.gts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import {
contains,
field,
linksTo,
linksToMany,
primitive,
queryableValue,
} from './card-api';
import CodeRefField from './code-ref';
import BooleanField from './boolean';
import { SkillCard } from './skill-card';

export type CommandStatus = 'applied' | 'ready' | 'applying';

Expand Down Expand Up @@ -75,3 +77,16 @@ export class CreateInstanceInput extends CardDef {
@field module = contains(CodeRefField);
@field realm = contains(StringField);
}

export class CreateAIAssistantRoomInput extends CardDef {
@field name = contains(StringField);
}

export class CreateAIAssistantRoomResult extends CardDef {
@field roomId = contains(StringField);
}

export class AddSkillsToRoomInput extends CardDef {
@field roomId = contains(StringField);
@field skills = linksToMany(SkillCard);
}
6 changes: 3 additions & 3 deletions packages/base/markdown.gts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class View extends Component<typeof MarkdownField> {
<style scoped>
.markdown-content {
max-width: 100%;
font-size: var(--markdown-font-size, 1rem);
font-size: var(--markdown-font-size, inherit);
font-family: var(--markdown-font-family, inherit);
overflow: hidden;
}
Expand Down Expand Up @@ -67,8 +67,8 @@ class View extends Component<typeof MarkdownField> {
font-family: inherit;
font-size: inherit;
font-weight: 400;
margin-top: var(--boxel-sp-sm);
margin-bottom: var(--boxel-sp-sm);
margin-top: var(--boxel-sp-lg);
margin-bottom: var(--boxel-sp);
}
/* Bold */
Expand Down
38 changes: 17 additions & 21 deletions packages/billing/billing-queries.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
DBAdapter,
Expression,
PgPrimitive,
addExplicitParens,
asExpressions,
every,
Expand All @@ -17,6 +18,7 @@ export interface User {
matrixUserId: string;
stripeCustomerId: string;
stripeCustomerEmail: string | null;
matrixRegistrationToken: string | null;
}

export interface Plan {
Expand Down Expand Up @@ -57,6 +59,16 @@ export interface LedgerEntry {
subscriptionCycleId: string | null;
}

function planRowToPlan(row: Record<string, PgPrimitive>): Plan {
return {
id: row.id,
name: row.name,
monthlyPrice: parseFloat(row.monthly_price as string),
creditsIncluded: row.credits_included,
stripePlanId: row.stripe_plan_id,
} as Plan;
}

export async function insertStripeEvent(
dbAdapter: DBAdapter,
event: StripeEvent,
Expand Down Expand Up @@ -98,13 +110,7 @@ export async function getPlanByStripeId(
return null;
}

return {
id: results[0].id,
name: results[0].name,
monthlyPrice: results[0].monthly_price,
creditsIncluded: results[0].credits_included,
stripePlanId: results[0].stripe_plan_id,
} as Plan;
return planRowToPlan(results[0]);
}

export async function updateUserStripeCustomerId(
Expand Down Expand Up @@ -167,6 +173,7 @@ export async function getUserByStripeId(
id: results[0].id,
matrixUserId: results[0].matrix_user_id,
stripeCustomerId: results[0].stripe_customer_id,
matrixRegistrationToken: results[0].matrix_registration_token,
} as User;
}

Expand All @@ -188,6 +195,7 @@ export async function getUserByMatrixUserId(
matrixUserId: results[0].matrix_user_id,
stripeCustomerId: results[0].stripe_customer_id,
stripeCustomerEmail: results[0].stripe_customer_email,
matrixRegistrationToken: results[0].matrix_registration_token,
} as User;
}

Expand Down Expand Up @@ -453,13 +461,7 @@ export async function getPlanById(
return null;
}

return {
id: results[0].id,
name: results[0].name,
monthlyPrice: results[0].monthly_price,
creditsIncluded: results[0].credits_included,
stripePlanId: results[0].stripe_plan_id,
} as Plan;
return planRowToPlan(results[0]);
}

export async function getPlanByMonthlyPrice(
Expand All @@ -475,13 +477,7 @@ export async function getPlanByMonthlyPrice(
return null;
}

return {
id: results[0].id,
name: results[0].name,
monthlyPrice: results[0].monthly_price,
creditsIncluded: results[0].credits_included,
stripePlanId: results[0].stripe_plan_id,
} as Plan;
return planRowToPlan(results[0]);
}

export async function expireRemainingPlanAllowanceInSubscriptionCycle(
Expand Down
26 changes: 16 additions & 10 deletions packages/billing/proration-calculator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,23 @@ export class ProrationCalculator {
}) {
let { currentPlan, newPlan, invoiceLines, currentAllowance } = params;

// Sum up monetary credit (refunds) given to the user by Stripe for unused time on previous plans
// (there can be multiple such lines if user switches to larger plans multiple times in the same billing period)
// and convert it to credits. In other words, take away the credits calculated from the money that Stripe
// returned to the user for unused time.
let creditsToExpireForUnusedTime = 0;
for (let line of invoiceLines) {
if (line.amount > 0) continue;
creditsToExpireForUnusedTime += this.centsToCredits(
-line.amount,
currentPlan,
);

if (currentPlan.monthlyPrice === 0) {
// If the user is upgrading from a free plan, we do not want to carry over any credits from the free plan into the paid plan
creditsToExpireForUnusedTime = currentAllowance;
} else {
// Sum up monetary credit (refunds) given to the user by Stripe for unused time on previous plans
// (there can be multiple such lines if user switches to larger plans multiple times in the same billing period)
// and convert it to credits. In other words, take away the credits calculated from the money that Stripe
// returned to the user for unused time.
for (let line of invoiceLines) {
if (line.amount > 0) continue;
creditsToExpireForUnusedTime += this.centsToCredits(
-line.amount,
currentPlan,
);
}
}

// Find invoice line for the new plan the user is subscribing to
Expand Down
80 changes: 68 additions & 12 deletions packages/billing/stripe-webhook-handlers/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { DBAdapter } from '@cardstack/runtime-common';
import { DBAdapter, decodeWebSafeBase64 } from '@cardstack/runtime-common';
import { handlePaymentSucceeded } from './payment-succeeded';
import { handleCheckoutSessionCompleted } from './checkout-session-completed';

import Stripe from 'stripe';
import { handleSubscriptionDeleted } from './subscription-deleted';
import { getUserByStripeId } from '../billing-queries';

export type StripeEvent = {
id: string;
Expand Down Expand Up @@ -99,11 +100,15 @@ export type StripeCheckoutSessionCompletedWebhookEvent = StripeEvent & {
// Invoice immediately (when prorating): CHECKED
// When switching to a cheaper subscription -> WAIT UNTIL END OF BILLING PERIOD TO UPDATE

export default async function stripeWebhookHandler(
dbAdapter: DBAdapter,
request: Request,
sendBillingNotification: (stripeUserId: string) => Promise<void>,
): Promise<Response> {
export default async function stripeWebhookHandler({
dbAdapter,
request,
sendMatrixEvent,
}: {
dbAdapter: DBAdapter;
request: Request;
sendMatrixEvent: (matrixUserId: string, eventType: string) => Promise<void>;
}): Promise<Response> {
let signature = request.headers.get('stripe-signature');

if (!signature) {
Expand All @@ -130,27 +135,78 @@ export default async function stripeWebhookHandler(

switch (type) {
// These handlers should eventually become jobs which workers will process asynchronously
case 'invoice.payment_succeeded':
case 'invoice.payment_succeeded': {
await handlePaymentSucceeded(
dbAdapter,
event as StripeInvoicePaymentSucceededWebhookEvent,
);
sendBillingNotification(event.data.object.customer);
sendBillingNotification({
dbAdapter,
sendMatrixEvent,
stripeEvent: event,
});
break;
case 'customer.subscription.deleted': // canceled by the user, or expired due to payment failure, or payment dispute
}
case 'customer.subscription.deleted': {
// canceled by the user, or expired due to payment failure, or payment dispute
await handleSubscriptionDeleted(
dbAdapter,
event as StripeSubscriptionDeletedWebhookEvent,
);
sendBillingNotification(event.data.object.customer);
sendBillingNotification({
dbAdapter,
sendMatrixEvent,
stripeEvent: event,
});
break;
case 'checkout.session.completed':
}
case 'checkout.session.completed': {
await handleCheckoutSessionCompleted(
dbAdapter,
event as StripeCheckoutSessionCompletedWebhookEvent,
);
sendBillingNotification(event.data.object.customer);
sendBillingNotification({
dbAdapter,
sendMatrixEvent,
stripeEvent: event,
});
break;
}
}
return new Response('ok');
}

async function sendBillingNotification({
dbAdapter,
sendMatrixEvent,
stripeEvent,
}: {
dbAdapter: DBAdapter;
sendMatrixEvent: (matrixUserId: string, eventType: string) => Promise<void>;
stripeEvent: StripeEvent;
}) {
let matrixUserId = await extractMatrixUserId(dbAdapter, stripeEvent);
await sendMatrixEvent(matrixUserId, 'billing-notification');
}

// Stripe events will have a `customer` (stripe customer id) field in the "invoice.payment_succeeded" event
// but not in the "checkout.session.completed" event. In the latter case, we need to look up the user by
// the `client_reference_id` field, which is a url parameter with the value of an encoded matrix user id
// (these are the payment links for subscribing to the free plan, and buying extra credits)
async function extractMatrixUserId(dbAdapter: DBAdapter, event: StripeEvent) {
let encodedMatrixUserId = event.data.object.client_reference_id;
let matrixUserId = encodedMatrixUserId
? decodeWebSafeBase64(encodedMatrixUserId)
: undefined;

if (!matrixUserId && event.data.object.customer) {
let user = await getUserByStripeId(dbAdapter, event.data.object.customer);
matrixUserId = user?.matrixUserId;
}

if (!matrixUserId) {
throw new Error('Failed to extract matrix user id from stripe event');
}

return matrixUserId;
}
4 changes: 0 additions & 4 deletions packages/billing/stripe-webhook-handlers/payment-succeeded.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ import { StripeInvoicePaymentSucceededWebhookEvent } from '.';
import { PgAdapter, TransactionManager } from '@cardstack/postgres';
import { ProrationCalculator } from '../proration-calculator';

// TODOs that will be handled in a separated PRs:
// - signal to frontend that subscription has been created and credits have been added
// - put this in a background job

export async function handlePaymentSucceeded(
dbAdapter: DBAdapter,
event: StripeInvoicePaymentSucceededWebhookEvent,
Expand Down
4 changes: 2 additions & 2 deletions packages/boxel-ui/addon/src/components/avatar/index.gts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface Signature {
displayName?: string;
isReady: boolean;
thumbnailURL?: string;
userId: string;
userId?: string | null;
};
Element: HTMLDivElement;
}
Expand Down Expand Up @@ -73,7 +73,7 @@ export default class Avatar extends Component<Signature> {
}
let name = this.args.displayName?.length
? this.args.displayName
: this.args.userId.replace(/^@/, '');
: this.args.userId?.replace(/^@/, '') ?? '';
return name.slice(0, 1).toUpperCase();
}
}
Loading

0 comments on commit 9686d30

Please sign in to comment.