Skip to content

234 payment edge cases#235

Merged
InfinityBowman merged 13 commits into
mainfrom
234-payment-edge-cases
Jan 6, 2026
Merged

234 payment edge cases#235
InfinityBowman merged 13 commits into
mainfrom
234-payment-edge-cases

Conversation

@InfinityBowman
Copy link
Copy Markdown
Owner

@InfinityBowman InfinityBowman commented Jan 6, 2026

Summary by CodeRabbit

  • New Features

    • Exported low-level primitives for all UI components, enabling advanced custom layouts
    • Added plan change validation with quota enforcement to prevent unsupported downgrades
    • Introduced collaborator quota management with UI feedback when limits are reached
    • Added subscription data caching for offline support and faster load times
    • New payment issue alerts for past-due and incomplete subscriptions
  • Improvements

    • Enhanced trial countdown with urgency indicators and upgrade prompts
    • Better checkout result handling with success/canceled feedback messages
    • Improved subscription status displays with visual badges and warnings

✏️ Tip: You can customize this high-level summary in your review settings.

@InfinityBowman InfinityBowman linked an issue Jan 6, 2026 that may be closed by this pull request
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Jan 6, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
corates a85110f Commit Preview URL Jan 06 2026, 06:29 PM

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 6, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

📝 Walkthrough

Walkthrough

This PR expands UI component export surfaces with primitive variants across all components, implements end-to-end billing plan change validation with quota checks, adds collaborator quota enforcement to member management, introduces a payment issue alert UI component, implements localStorage-based subscription caching, and wires validation and quota checks through backend routes and frontend flows.

Changes

Cohort / File(s) Summary
UI Component Primitive Exports
packages/ui/src/components/Accordion.tsx, Avatar.tsx, Checkbox.tsx, Clipboard.tsx, Collapsible.tsx, Combobox.tsx, Dialog.tsx, Drawer.tsx, Editable.tsx, FileUpload.tsx, FloatingPanel.tsx, Menu.tsx, NumberInput.tsx, PasswordInput.tsx, PinInput.tsx, Popover.tsx, Progress.tsx, QRCode.tsx, RadioGroup.tsx, Select.tsx, Splitter.tsx, Switch.tsx, Tabs.tsx, TagsInput.tsx, Toast.tsx, ToggleGroup.tsx, Tooltip.tsx
Added *Primitive export aliases and hook exports (e.g., useCheckbox, useCombobox) to expose raw Ark UI primitives for custom layouts. Additionally exports useCollapsible hook in Collapsible.tsx.
UI Component Barrel Export
packages/ui/src/components/index.ts
Updated barrel exports to include all new *Primitive variants and associated hooks for ~27 components, expanding public API surface.
Billing API Backend
packages/workers/src/lib/billingResolver.js
Added getOrgResourceUsage(db, orgId) to count projects and collaborators; added validatePlanChange(db, orgId, targetPlanId) to validate downgrades against quota limits and return violations.
Billing Routes
packages/workers/src/routes/billing/index.js
Added new GET /api/billing/validate-plan-change endpoint; enhanced /checkout with pre-flight plan validation; exposed projectCount in /subscription response.
Billing API Frontend
packages/web/src/api/billing.js
Added new validatePlanChange(targetPlan) function to call backend validation endpoint via GET request.
Billing UI Components
packages/web/src/components/billing/BillingPage.jsx
Added onMount handling for Stripe checkout results (success/canceled params); introduced checkoutOutcome state; added success and canceled alert dismissal; integrated PaymentIssueBanner component.
Payment Issue Alert Component
packages/web/src/components/billing/PaymentIssueBanner.jsx
New component rendering status-specific payment issue alerts (past_due, incomplete, unpaid) with dynamic messaging and update action button.
Pricing & Plan Change
packages/web/src/components/billing/PricingTable.jsx
Added downgrade detection with tier ordering; integrated plan validation flow via validatePlanChange; added modal dialogs for validation errors and downgrade confirmation; added trial upgrade banner; enhanced button labeling for trial scenarios.
Trial & Subscription Status UI
packages/web/src/components/billing/SubscriptionCard.jsx
Added unpaid status; enhanced trial countdown with dynamic styling and urgency indicators (amber styling ≤3 days); added status-specific alert blocks (incomplete, unpaid); added trial expiry warning.
Quota-Aware Member Management
packages/web/src/components/project/overview-tab/AddMemberModal.jsx
Added quotaInfo prop usage; displays quota warning banner when at limit; disables input/button at quota threshold; imports quota helper and icons.
Quota & Subscription Integration
packages/web/src/components/project/overview-tab/OverviewTab.jsx
Added useSubscription and useMembers hooks; introduced collaboratorQuotaInfo and canAddMember memos; conditionally renders Add Member button or disabled placeholder with quota awareness.
Entitlements Grant-Type Resolution
packages/web/src/lib/entitlements.js
Added grant-type handling with getGrantPlan import; introduced resolvePlan() resolver to route grant-type IDs to getGrantPlan vs getPlan.
Subscription Caching & Visibility Handling
packages/web/src/primitives/useSubscription.js
Implemented localStorage-based caching with 7-day TTL; added loadCachedSubscription, saveCachedSubscription, clearCachedSubscription helpers; integrated visibility change listener for refetch; exposed isUsingCachedData and lastSynced computed properties; reduced staleTime to 2 minutes.
Member Quota Enforcement
packages/workers/src/routes/orgs/members.js
Added checkCollaboratorQuota call before org/project member addition; returns 403 error if quota exceeded.
Billing Tests
packages/workers/src/routes/billing/__tests__/index.test.js
Added tests for GET /api/billing/validate-plan-change (validation results, violations, errors) and enhanced /checkout tests with downgrade validation scenarios.
BillingResolver Tests
packages/workers/src/lib/__tests__/billingResolver.test.js
Comprehensive test suite for isSubscriptionActive, resolveOrgAccess, getOrgResourceUsage, and validatePlanChange with edge cases for subscriptions, grants, and quota enforcement.
Member Routes Tests
packages/workers/src/routes/__tests__/members.test.js
Added mocking for billingResolver.resolveOrgAccess and quotaTransaction.checkCollaboratorQuota; new test suite for quota enforcement (403 on exceeded, 201 on existing member).
Minor Formatting
packages/workers/src/routes/admin/orgs.js
Consolidated multi-line import into single-line statement.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Web as Web UI
    participant API as Backend API
    participant DB as Database
    
    rect rgb(200, 220, 255)
    note over User,DB: Plan Change Validation Flow
    User->>Web: Select target plan
    Web->>API: GET /api/billing/validate-plan-change?targetPlan=X
    API->>DB: Query org resource usage (projects, collaborators)
    DB-->>API: Usage counts
    API->>DB: Query target plan quotas
    DB-->>API: Target plan metadata & limits
    alt Validation Passes
        API-->>Web: { valid: true }
        Web->>Web: Show plan confirmation dialog
    else Violations Exist
        API-->>Web: { violations: [...] }
        Web->>Web: Show validation error dialog with violations
    end
    end
    
    rect rgb(220, 255, 220)
    note over User,DB: Checkout with Downgrade Validation
    User->>Web: Confirm plan change
    Web->>API: POST /api/billing/checkout {tier: X}
    API->>DB: Call validatePlanChange(orgId, tier)
    DB-->>API: Validation result
    alt Downgrade with Violations
        API-->>Web: 400 error {violations: [...]}
        Web->>Web: Block checkout, show error
    else Valid (upgrade or within limits)
        API->>API: Proceed to Stripe checkout
        API-->>Web: Stripe session URL
        Web->>User: Redirect to Stripe
    end
    end
Loading
sequenceDiagram
    actor User
    participant Web as Web UI
    participant API as Backend API
    participant DB as Database
    
    rect rgb(255, 240, 200)
    note over User,DB: Member Addition with Quota Check
    User->>Web: Open Add Member modal
    Web->>Web: Compute collaboratorQuotaInfo from subscription
    Web->>Web: canAddMember = !isAtQuota && isOwner
    alt At Quota Limit
        Web->>User: Show quota warning banner, disable input
    else Below Limit
        User->>Web: Search & select user to add
        Web->>API: POST /api/orgs/:id/members {userId: X}
        API->>API: checkCollaboratorQuota(orgId)
        API->>DB: Count current collaborators
        DB-->>API: Current count vs quota limit
        alt Quota Exceeded
            API-->>Web: 403 AUTH_FORBIDDEN {quotaExceeded: true}
            Web->>User: Show quota error
        else Within Limit
            API->>DB: Create org/project membership
            DB-->>API: Success
            API-->>Web: 201 Created {member: {...}}
            Web->>Web: Refresh member list, close modal
        end
    end
    end
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly Related Issues

  • Payment edge cases #234 — This PR directly addresses payment and checkout edge cases with the new validatePlanChange validation, billing UI updates (PaymentIssueBanner, PricingTable downgrade flow, BillingPage checkout handling), and quota enforcement wiring.

Possibly Related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title '234 payment edge cases' is vague and does not clearly convey the primary changes in this comprehensive pull request that spans UI component exports, billing infrastructure, subscription handling, and quota enforcement. Use a more descriptive title that captures the main initiative, such as 'Expose component primitives and implement payment edge case handling with quota enforcement' or similar phrasing that reflects the scope of changes.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 95.65% which is sufficient. The required threshold is 80.00%.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/workers/src/routes/admin/orgs.js (1)

28-30: Use validateQueryParams middleware and define schema for query parameter validation.

Lines 28–30 manually parse query parameters without validation middleware. Per coding guidelines, all query parameters must be validated using validateQueryParams middleware with a defined Zod schema. This route lacks orgSchemas in the validation config.

Add to packages/workers/src/config/validation.js:

export const orgSchemas = {
  list: z.object({
    page: z
      .string()
      .optional()
      .default('1')
      .transform(val => parseInt(val, 10))
      .pipe(z.number().int('Page must be an integer').min(1, 'Page must be at least 1')),
    limit: z
      .string()
      .optional()
      .default('20')
      .transform(val => parseInt(val, 10))
      .pipe(
        z
          .number()
          .int('Limit must be an integer')
          .min(1, 'Limit must be at least 1')
          .max(100, 'Limit must be at most 100'),
      ),
    search: z
      .string()
      .optional()
      .transform(val => val?.trim()),
  }),
};

Then import and apply the middleware to the route, and refactor the handler to extract validated params from c.get('validatedQuery'). See database.js and storage.js for the implementation pattern.

🤖 Fix all issues with AI Agents
In @packages/ui/src/components/NumberInput.tsx:
- Around line 134-136: The module currently shadows the Ark UI primitive by
re-exporting NumberInputComponent as NumberInput, so the later export "export {
NumberInput as NumberInputPrimitive }" exports the component instead of the
original primitive; fix by importing the Ark primitive under an alias (e.g.,
import { NumberInput as NumberInputPrimitiveFromArk } from '...'), change the
raw export to "export { NumberInputPrimitiveFromArk as NumberInputPrimitive }",
and update any internal references that intended to use the original Ark
primitive to use the new alias (ensure NumberInputComponent remains the
component and NumberInputPrimitiveFromArk is the raw primitive).
- Line 119: The input element in NumberInput uses the incorrect Tailwind
selector `data-invalid:` which doesn't match Ark UI's attribute format; update
the class string in the NumberInput component (look for the element building the
class with sizes().input and showControls()) to replace
`data-invalid:border-red-500 data-invalid:ring-red-500` with
`data-[invalid]:border-red-500 data-[invalid]:ring-red-500` so the invalid state
styles are applied consistently like other Ark UI components.

In @packages/ui/src/components/TagsInput.tsx:
- Around line 124-126: The export is wrong because the local alias TagsInput now
points to TagsInputComponent, so export { TagsInput as TagsInputPrimitive }
re-exports the component instead of the Ark UI primitive; fix by importing the
Ark primitive under a distinct name (e.g., import { TagsInput as ArkTagsInput }
from '...') and then update the component to use ArkTagsInput where the
primitive was referenced, leaving the final export as export { ArkTagsInput as
TagsInputPrimitive } while keeping TagsInputComponent exported as TagsInput.
🧹 Nitpick comments (13)
packages/ui/src/components/PasswordInput.tsx (1)

80-82: Primitive export is correct but consider aliasing the import for clarity.

The export aligns with the PR objective to expose raw Ark UI primitives. However, having both the import and export share the PasswordInput name can confuse future maintainers about which binding is being referenced.

Consider the clearer aliasing pattern used in Drawer.tsx
-import { PasswordInput } from '@ark-ui/solid/password-input';
+import { PasswordInput as ArkPasswordInput } from '@ark-ui/solid/password-input';

Then update the component to use ArkPasswordInput.* and export:

-export { PasswordInput as PasswordInputPrimitive };
+export { ArkPasswordInput as PasswordInputPrimitive };

This makes it immediately clear that PasswordInputPrimitive refers to the Ark UI import rather than the wrapped component.

packages/ui/src/components/Clipboard.tsx (1)

234-236: Primitive export is correct but consider aliasing the import for clarity.

The export aligns with the PR objective to expose raw Ark UI primitives. Similar to PasswordInput.tsx, consider adopting the aliasing pattern demonstrated in Drawer.tsx (import { Clipboard as ArkClipboard }) to make it immediately clear which binding ClipboardPrimitive references.

packages/ui/src/components/PinInput.tsx (1)

95-97: Primitive export is correct but consider aliasing the import for clarity.

The export aligns with the PR objective to expose raw Ark UI primitives. Consider adopting the aliasing pattern from Drawer.tsx (import { PinInput as ArkPinInput }) to improve maintainability and make the export intent more explicit.

packages/ui/src/components/Menu.tsx (1)

166-168: Primitive export is correct but consider aliasing the import for clarity.

The export aligns with the PR objective to expose raw Ark UI primitives. Like the other components in this PR, consider adopting the aliasing pattern from Drawer.tsx (import { Menu as ArkMenu }) for improved code clarity.

packages/ui/src/components/index.ts (1)

5-38: All new Primitive exports and hooks exist in their source files.

Verification confirms that all exported identifiers in lines 5-38 are properly defined in their respective component files. No missing exports will cause build failures.

There is a minor inconsistency in export order: lines 9 and 24 place hooks before primitives (useCollapsible, CollapsiblePrimitive and useSelect, SelectPrimitive), while other lines consistently place primitives before hooks. Consider standardizing to place primitives before hooks for improved consistency:

-export { default as Collapsible, useCollapsible, CollapsiblePrimitive } from './Collapsible';
+export { default as Collapsible, CollapsiblePrimitive, useCollapsible } from './Collapsible';
-export { default as Select, useSelect, SelectPrimitive } from './Select';
+export { default as Select, SelectPrimitive, useSelect } from './Select';
packages/web/src/components/project/overview-tab/OverviewTab.jsx (1)

51-60: Consider handling loading state for quotas.

When quotas() is undefined (loading), max defaults to 0, which could incorrectly show "quota reached" before data loads. Consider either:

  1. Adding a loading check before evaluating quota
  2. Defaulting to a permissive value during loading
Possible approach to handle loading state
  const collaboratorQuotaInfo = createMemo(() => {
-   const max = quotas()?.['collaborators.org.max'] ?? 0;
+   const quotaMax = quotas()?.['collaborators.org.max'];
+   // Return null during loading to avoid false "quota reached" state
+   if (quotaMax === undefined) return null;
    const used = orgMemberCount();
-   return { used, max };
+   return { used, max: quotaMax };
  });

  const canAddMember = createMemo(() => {
    if (!isOwner()) return false;
+   // Allow during loading
+   if (!quotas()) return true;
    return hasQuota('collaborators.org.max', { used: orgMemberCount(), requested: 1 });
  });

Then update the modal and button to handle null quotaInfo gracefully.

packages/web/src/lib/entitlements.js (1)

11-11: Consider importing GRANT_TYPES from @corates/shared/plans to avoid duplication.

The GRANT_TYPES array duplicates knowledge that already exists in the shared plans module (where getGrantPlan handles 'trial' and 'single_project'). If a new grant type is added to the backend, this frontend constant could fall out of sync.

Suggested approach

Export GRANT_TYPES from @corates/shared/plans and import it here, or derive the check from the existence of a grant plan:

-const GRANT_TYPES = ['trial', 'single_project'];
+import { getPlan, DEFAULT_PLAN, isUnlimitedQuota, getGrantPlan, GRANT_TYPES } from '@corates/shared/plans';

Alternatively, you could make resolvePlan try getGrantPlan first and fall back to getPlan if the result equals the default plan, though that may have performance implications.

packages/workers/src/lib/billingResolver.js (1)

258-274: Consider wrapping DB operations in try-catch for resilience.

The getOrgResourceUsage function performs two database queries but lacks error handling. If either query fails, the destructuring assignment (const [projectResult] = ...) could throw an unexpected error.

Per coding guidelines, database operations should be wrapped in try-catch blocks with domain errors.

Suggested approach
 export async function getOrgResourceUsage(db, orgId) {
+  try {
     // Count projects in org
     const [projectResult] = await db
       .select({ count: count() })
       .from(projects)
       .where(eq(projects.orgId, orgId));
 
     // Count non-owner members (owner doesn't count toward collaborator limit)
     const [memberResult] = await db
       .select({ count: count() })
       .from(member)
       .where(and(eq(member.organizationId, orgId), ne(member.role, 'owner')));
 
     return {
       projects: projectResult?.count || 0,
       collaborators: memberResult?.count || 0,
     };
+  } catch (error) {
+    console.error('[BillingResolver] Error getting org resource usage:', error);
+    throw error;
+  }
 }
packages/workers/src/lib/__tests__/billingResolver.test.js (1)

511-566: Tests rely on hardcoded plan quotas - consider documenting this dependency.

These tests assume starter_team plan has projects.max: 3 and collaborators.org.max: 5. If the plan configuration changes, these tests will fail silently or produce misleading results.

Consider adding a brief comment or using constants from the plans module to make the dependency explicit.

packages/web/src/components/billing/SubscriptionCard.jsx (1)

138-160: Consider extracting alert blocks into a reusable component.

The incomplete, unpaid, and past_due alert blocks share similar structure (icon + title + description). A small AlertBlock component could reduce duplication.

Optional refactor example
function AlertBlock(props) {
  const colorStyles = {
    red: 'border-red-200 bg-red-50 text-red-800 text-red-600 text-red-500',
    yellow: 'border-yellow-200 bg-yellow-50 text-yellow-800 text-yellow-600 text-yellow-500',
    amber: 'border-amber-200 bg-amber-50 text-amber-800 text-amber-600 text-amber-500',
  };
  // ... implementation
}

This is optional since the current implementation is clear and readable.

packages/workers/src/routes/__tests__/members.test.js (1)

1526-1540: Add assertion to verify quota check was bypassed.

The comment states the quota check won't be called, but there's no assertion to verify this. Adding an explicit check would make the test more robust and document the expected behavior.

Proposed fix
     // Should succeed because user-2 is already an org member
     // checkCollaboratorQuota should NOT have been called (existing org member)
     expect(res.status).toBe(201);
     const body = await json(res);
     expect(body.userId).toBe('user-2');
+    expect(mockCheckCollaboratorQuota).not.toHaveBeenCalled();
packages/web/src/components/billing/PricingTable.jsx (1)

440-449: Display friendly plan name instead of raw tier ID.

The downgrade confirmation shows currentTier() which returns the raw tier ID (e.g., "starter_team") rather than a user-friendly name. Consider using the plan's display name for better UX.

Proposed fix
+              const getCurrentPlanName = () => {
+                const current = plans.find(p => p.tier === currentTier());
+                return current?.name || currentTier();
+              };
+
               <Show when={pendingDowngrade()}>
                 <div class='mb-6 rounded-lg border border-amber-200 bg-amber-50 p-4'>
                   <p class='text-sm text-amber-800'>
-                    You're switching from <span class='font-semibold'>{currentTier()}</span> to{' '}
+                    You're switching from <span class='font-semibold'>{getCurrentPlanName()}</span> to{' '}
                     <span class='font-semibold'>{pendingDowngrade()?.name}</span>.
                   </p>
packages/workers/src/routes/billing/index.js (1)

171-218: Consider using validateQueryParams middleware for query string validation.

Per coding guidelines, query string validation should use the validateQueryParams middleware pattern. To implement this, add a billingSchemas object to packages/workers/src/config/validation.js with a schema for targetPlan, then apply the middleware to the route. This would replace the manual validation at lines 176-184 with schema-based validation using c.get('validatedQuery').

The current manual validation is functionally correct and properly uses createDomainError, but middleware-based validation is the preferred pattern for consistency across the codebase.

<NumberInput.Input
placeholder={local.placeholder}
class={`${sizes().input} flex-1 border border-gray-300 text-center ${showControls() ? '' : 'rounded-lg'} read-only:bg-gray-50 focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none disabled:cursor-not-allowed disabled:bg-gray-100 data-[invalid]:border-red-500 data-[invalid]:ring-red-500 ${local.inputClass || ''}`}
class={`${sizes().input} flex-1 border border-gray-300 text-center ${showControls() ? '' : 'rounded-lg'} read-only:bg-gray-50 focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none disabled:cursor-not-allowed disabled:bg-gray-100 data-invalid:border-red-500 data-invalid:ring-red-500 ${local.inputClass || ''}`}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Search for data attribute usages in UI components
rg -n "data-\[invalid\]|data-invalid|data-\[" packages/ui/src/components/ -A 2 -B 2

Repository: InfinityBowman/corates

Length of output: 11827


🏁 Script executed:

# Also check the Ark UI dependency version in package.json
rg -n "@ark-ui|@headlessui" packages/ui/ | head -20

Repository: InfinityBowman/corates

Length of output: 2046


🏁 Script executed:

# Look for NumberInput component usage and related configs
find packages/ui -type f -name "*.tsx" -o -name "*.ts" | xargs grep -l "NumberInput" | head -10

Repository: InfinityBowman/corates

Length of output: 144


Fix invalid state selector in NumberInput input element.

The selector data-invalid: will not match Ark UI NumberInput's actual data attributes. All other Ark UI components in the codebase consistently use data-[invalid]: with square brackets. Change line 119 to use data-[invalid]:border-red-500 data-[invalid]:ring-red-500 instead of data-invalid:border-red-500 data-invalid:ring-red-500.

🤖 Prompt for AI Agents
In @packages/ui/src/components/NumberInput.tsx around line 119, The input
element in NumberInput uses the incorrect Tailwind selector `data-invalid:`
which doesn't match Ark UI's attribute format; update the class string in the
NumberInput component (look for the element building the class with
sizes().input and showControls()) to replace `data-invalid:border-red-500
data-invalid:ring-red-500` with `data-[invalid]:border-red-500
data-[invalid]:ring-red-500` so the invalid state styles are applied
consistently like other Ark UI components.

Comment on lines +134 to +136

// Export raw Ark UI primitive for custom layouts
export { NumberInput as NumberInputPrimitive };
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Wrong primitive exported due to name shadowing.

After line 133 exports NumberInputComponent as NumberInput, the identifier NumberInput in the module scope now refers to the component, not the Ark UI primitive imported on line 5. Therefore, line 136 exports the component again instead of the Ark UI primitive.

Proposed fix: Import Ark UI primitive with an alias
-import { NumberInput } from '@ark-ui/solid/number-input';
+import { NumberInput as ArkNumberInput } from '@ark-ui/solid/number-input';

Then update all references in the component:

 const NumberInputComponent: Component<NumberInputProps> = props => {
   // ... component code using ArkNumberInput.Root, ArkNumberInput.Input, etc.
   
   return (
-    <NumberInput.Root
+    <ArkNumberInput.Root
       // ... props
     >
       <Show when={local.label}>
-        <NumberInput.Label class='mb-1 block text-sm font-medium text-gray-700'>
+        <ArkNumberInput.Label class='mb-1 block text-sm font-medium text-gray-700'>
           {local.label}
-        </NumberInput.Label>
+        </ArkNumberInput.Label>
       </Show>
       <div class='flex'>
         <Show when={showControls()}>
-          <NumberInput.DecrementTrigger
+          <ArkNumberInput.DecrementTrigger
             // ... props
           >
             <FiMinus class='h-4 w-4' />
-          </NumberInput.DecrementTrigger>
+          </ArkNumberInput.DecrementTrigger>
         </Show>
-        <NumberInput.Input
+        <ArkNumberInput.Input
           // ... props
         />
         <Show when={showControls()}>
-          <NumberInput.IncrementTrigger
+          <ArkNumberInput.IncrementTrigger
             // ... props
           >
             <FiPlus class='h-4 w-4' />
-          </NumberInput.IncrementTrigger>
+          </ArkNumberInput.IncrementTrigger>
         </Show>
       </div>
-    </NumberInput.Root>
+    </ArkNumberInput.Root>
   );
 };
 
 export { NumberInputComponent as NumberInput };
 
-// Export raw Ark UI primitive for custom layouts
-export { NumberInput as NumberInputPrimitive };
+// Export raw Ark UI primitive for custom layouts  
+export { ArkNumberInput as NumberInputPrimitive };

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @packages/ui/src/components/NumberInput.tsx around lines 134-136, The module
currently shadows the Ark UI primitive by re-exporting NumberInputComponent as
NumberInput, so the later export "export { NumberInput as NumberInputPrimitive
}" exports the component instead of the original primitive; fix by importing the
Ark primitive under an alias (e.g., import { NumberInput as
NumberInputPrimitiveFromArk } from '...'), change the raw export to "export {
NumberInputPrimitiveFromArk as NumberInputPrimitive }", and update any internal
references that intended to use the original Ark primitive to use the new alias
(ensure NumberInputComponent remains the component and
NumberInputPrimitiveFromArk is the raw primitive).

Comment on lines +124 to +126

// Export raw Ark UI primitive for custom layouts
export { TagsInput as TagsInputPrimitive };
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Wrong primitive exported due to name shadowing.

After line 123 exports TagsInputComponent as TagsInput, the identifier TagsInput now refers to the component, not the Ark UI primitive imported on line 5. Line 126 therefore exports the component again instead of the Ark UI primitive.

Proposed fix: Import Ark UI primitive with an alias
-import { TagsInput } from '@ark-ui/solid/tags-input';
+import { TagsInput as ArkTagsInput } from '@ark-ui/solid/tags-input';

Then update all references in the component:

 const TagsInputComponent: Component<TagsInputProps> = props => {
   // ... component logic
   
   return (
-    <TagsInput.Root
+    <ArkTagsInput.Root
       // ... props
     >
-      <TagsInput.Context>
+      <ArkTagsInput.Context>
         {api => (
           <>
             {local.label && (
-              <TagsInput.Label class='mb-1 block text-sm font-medium text-gray-700'>
+              <ArkTagsInput.Label class='mb-1 block text-sm font-medium text-gray-700'>
                 {local.label}
-              </TagsInput.Label>
+              </ArkTagsInput.Label>
             )}
-            <TagsInput.Control class='...'>
+            <ArkTagsInput.Control class='...'>
               <Index each={api().value}>
                 {(value, index) => (
-                  <TagsInput.Item index={index} value={value()} class='...'>
-                    <TagsInput.ItemPreview class='...'>
-                      <TagsInput.ItemText>{value()}</TagsInput.ItemText>
-                      <TagsInput.ItemDeleteTrigger class='...'>
+                  <ArkTagsInput.Item index={index} value={value()} class='...'>
+                    <ArkTagsInput.ItemPreview class='...'>
+                      <ArkTagsInput.ItemText>{value()}</ArkTagsInput.ItemText>
+                      <ArkTagsInput.ItemDeleteTrigger class='...'>
                         <FiX class='h-3 w-3' />
-                      </TagsInput.ItemDeleteTrigger>
-                    </TagsInput.ItemPreview>
-                    <TagsInput.ItemInput class='sr-only' />
-                  </TagsInput.Item>
+                      </ArkTagsInput.ItemDeleteTrigger>
+                    </ArkTagsInput.ItemPreview>
+                    <ArkTagsInput.ItemInput class='sr-only' />
+                  </ArkTagsInput.Item>
                 )}
               </Index>
-              <TagsInput.Input
+              <ArkTagsInput.Input
                 placeholder={local.placeholder || 'Add tag...'}
                 class={`...`}
               />
-            </TagsInput.Control>
+            </ArkTagsInput.Control>
           </>
         )}
-      </TagsInput.Context>
-      <TagsInput.HiddenInput />
-    </TagsInput.Root>
+      </ArkTagsInput.Context>
+      <ArkTagsInput.HiddenInput />
+    </ArkTagsInput.Root>
   );
 };
 
 export { TagsInputComponent as TagsInput };
 
-// Export raw Ark UI primitive for custom layouts
-export { TagsInput as TagsInputPrimitive };
+// Export raw Ark UI primitive for custom layouts  
+export { ArkTagsInput as TagsInputPrimitive };

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @packages/ui/src/components/TagsInput.tsx around lines 124-126, The export is
wrong because the local alias TagsInput now points to TagsInputComponent, so
export { TagsInput as TagsInputPrimitive } re-exports the component instead of
the Ark UI primitive; fix by importing the Ark primitive under a distinct name
(e.g., import { TagsInput as ArkTagsInput } from '...') and then update the
component to use ArkTagsInput where the primitive was referenced, leaving the
final export as export { ArkTagsInput as TagsInputPrimitive } while keeping
TagsInputComponent exported as TagsInput.

@InfinityBowman InfinityBowman merged commit 7243f90 into main Jan 6, 2026
2 of 3 checks passed
@InfinityBowman InfinityBowman deleted the 234-payment-edge-cases branch January 6, 2026 18:28
@coderabbitai coderabbitai Bot mentioned this pull request Apr 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Payment edge cases

2 participants