Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 0646b69894 ExportMultipleRecordsAction crashes when rendered outside ViewComponentInstanceContext
https://sonarly.com/issue/6956?type=bug

ExportMultipleRecordsAction's hook chain requires ViewComponentInstanceContext (only provided on index pages), but the component can be rendered in the command menu which lacks this context, causing a crash when the action is registered for index-page view types.

Fix: The crash occurs because `useRecordGroupFilter` calls `useAtomComponentStateValue(recordIndexGroupFieldMetadataItemComponentState)` without an explicit instance ID, relying on `ViewComponentInstanceContext` being present in the component tree. When `ExportMultipleRecordsAction` renders inside the command menu (outside `ViewComponentInstanceContext.Provider`), `useAvailableComponentInstanceIdOrThrow` finds no instance ID and throws.

The fix threads the `recordIndexId` — already available in `useRecordIndexLazyFetchRecords` — down through the hook chain as an explicit instance ID, so `useAtomComponentStateValue` can resolve the atom without needing the context:

```typescript file=packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupFilter.ts lines=8-16
export const useRecordGroupFilter = (
  fields: FieldMetadataItem[],
  instanceId?: string,
) => {
  const currentRecordGroupDefinition = useCurrentRecordGroupDefinition();
  const recordIndexGroupFieldMetadataItem = useAtomComponentStateValue(
    recordIndexGroupFieldMetadataItemComponentState,
    instanceId,
  );
```

```typescript file=packages/twenty-front/src/modules/object-record/record-index/hooks/useFindManyRecordIndexTableParams.ts lines=18-30
export const useFindManyRecordIndexTableParams = (
  objectNameSingular: string,
  instanceId?: string,
) => {
  // ...
  const { recordGroupFilter } = useRecordGroupFilter(
    objectMetadataItem?.fields,
    instanceId,
  );
```

```typescript file=packages/twenty-front/src/modules/object-record/record-index/export/hooks/useRecordIndexLazyFetchRecords.ts lines=90-93
  const findManyRecordsParams = useFindManyRecordIndexTableParams(
    objectMetadataItem.nameSingular,
    recordIndexId,
  );
```

All other callers of `useFindManyRecordIndexTableParams` (record table, virtualization hooks) run inside `RecordIndexContainerGater` which mounts `ViewComponentInstanceContext.Provider`, so they continue working correctly without passing an `instanceId`.
2026-03-03 01:20:45 +00:00
Sonarly Claude Code e8dabf7755 Direct subscription creation fails for users who already have an active subscription
https://sonarly.com/issue/4334?type=bug

The `createDirectSubscription` method throws a `BillingException` when a customer already has a non-canceled subscription, but nothing upstream prevents the user from reaching this code path when that condition is true.

Fix: ## Root Cause

A TOCTOU (Time-Of-Check-Time-Of-Use) race condition in the `createDirectSubscription` flow: the frontend checks `onboardingStatus === PLAN_REQUIRED` (no subscriptions exist), but by the time the user clicks "Continue" a subscription may already have been created (e.g. a prior request succeeded but the response was lost, or a double-click). The guard added in commit `65f0a5bb18` prevented a database unique-constraint crash but replaced it with a user-facing `BillingException` instead of handling the duplicate-request case idempotently.

## Fix

In `createDirectSubscription`, change the existing-subscription guard from **throwing an exception** to **returning `successUrl` idempotently**. If the customer already has a non-canceled subscription, the desired end-state (an active subscription + redirect to success) is already satisfied — we just return the success URL, exactly as the method would have done after creating it fresh.

```typescript file=packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts lines=106-115
    if (
      isNonEmptyArray(customer?.billingSubscriptions) &&
      customer.billingSubscriptions.some(
        (subscription) => subscription.status !== SubscriptionStatus.Canceled,
      )
    ) {
      // Subscription already exists (e.g. race condition / duplicate request).
      // Return successUrl idempotently instead of throwing a user-facing error.
      return successUrl;
    }
```

This makes `createDirectSubscription` consistent with the `computeCheckoutSessionURL` path (which uses Stripe checkout sessions and is inherently idempotent). The change is a 1-line replacement in a single file; no other layers need to be touched because the caller already handles the returned URL correctly.
2026-03-03 01:12:49 +00:00
3 changed files with 8 additions and 1 deletions
@@ -5,10 +5,14 @@ import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/use
import { useMemo } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const useRecordGroupFilter = (fields: FieldMetadataItem[]) => {
export const useRecordGroupFilter = (
fields: FieldMetadataItem[],
instanceId?: string,
) => {
const currentRecordGroupDefinition = useCurrentRecordGroupDefinition();
const recordIndexGroupFieldMetadataItem = useAtomComponentStateValue(
recordIndexGroupFieldMetadataItemComponentState,
instanceId,
);
const recordGroupFilter = useMemo(() => {
@@ -89,6 +89,7 @@ export const useRecordIndexLazyFetchRecords = ({
const findManyRecordsParams = useFindManyRecordIndexTableParams(
objectMetadataItem.nameSingular,
recordIndexId,
);
const queryFilter = computeContextStoreFilters({
@@ -17,6 +17,7 @@ import {
export const useFindManyRecordIndexTableParams = (
objectNameSingular: string,
instanceId?: string,
) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
@@ -25,6 +26,7 @@ export const useFindManyRecordIndexTableParams = (
const { recordGroupFilter } = useRecordGroupFilter(
objectMetadataItem?.fields,
instanceId,
);
const currentRecordGroupDefinition = useCurrentRecordGroupDefinition();