@buildbase/sdk 0.0.19 → 0.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,6 +14,9 @@ A React SDK for [BuildBase](https://www.buildbase.app/) that provides essential
14
14
  - [User Management](#-user-management)
15
15
  - [Workspace Management](#-complete-workspace-management)
16
16
  - [Public Pricing (No Login)](#-public-pricing-no-login)
17
+ - [Multi-Currency & Pricing Utilities](#-multi-currency--pricing-utilities)
18
+ - [Quota Usage Tracking](#-quota-usage-tracking)
19
+ - [Quota Gates](#-quota-gates)
17
20
  - [Beta Form Component](#-beta-form-component)
18
21
  - [Event System](#-event-system)
19
22
  - [Error Handling](#️-error-handling)
@@ -32,6 +35,7 @@ A React SDK for [BuildBase](https://www.buildbase.app/) that provides essential
32
35
  - **👥 Role-Based Access Control** - User roles and workspace-specific permissions
33
36
  - **🎯 Feature Flags** - Workspace-level and user-level feature toggles
34
37
  - **📋 Subscription Gates** - Show or hide UI based on current workspace subscription (plan)
38
+ - **📊 Quota Usage Tracking** - Record and monitor metered usage (API calls, storage, etc.) with real-time status
35
39
  - **👤 User Management** - User attributes and feature flags management
36
40
  - **📝 Beta Form** - Pre-built signup/waitlist form component
37
41
  - **📡 Event System** - Subscribe to user and workspace events
@@ -569,6 +573,485 @@ function PublicPricingPage() {
569
573
 
570
574
  **Backend requirement**: `GET /api/v1/public/{orgId}/plans/{groupSlug}` must be implemented and allow unauthenticated access.
571
575
 
576
+ ## 💱 Multi-Currency & Pricing Utilities
577
+
578
+ Plans support **pricing variants** (multi-currency). Use these utilities for display and lookup.
579
+
580
+ ### Currency utilities
581
+
582
+ | Export | Purpose |
583
+ |--------|--------|
584
+ | `CURRENCY_DISPLAY` | Map of currency code → symbol (e.g. `usd` → `$`) |
585
+ | `CURRENCY_FLAG` | Map of currency code → flag emoji |
586
+ | `PLAN_CURRENCY_CODES` | Allowed billing currency codes (for dropdowns/validation) |
587
+ | `PLAN_CURRENCY_OPTIONS` | Options array for plan currency selects |
588
+ | `getCurrencySymbol(currency)` | Symbol for a Stripe currency code |
589
+ | `getCurrencyFlag(currency)` | Flag emoji for a currency code |
590
+ | `formatCents(cents, currency)` | Format cents as localized price string |
591
+ | `formatOverageRate(cents, currency)` | Format overage rate for display |
592
+ | `formatOverageRateWithLabel(...)` | Overage rate with optional unit label |
593
+ | `formatQuotaIncludedOverage(...)` | "X included, then $Y / unit" style text |
594
+ | `getQuotaUnitLabelFromName(name)` | Human-readable unit label from quota name |
595
+
596
+ ### Pricing variant utilities
597
+
598
+ | Export | Purpose |
599
+ |--------|--------|
600
+ | `getPricingVariant(planVersion, currency)` | Get variant for a currency, or `null` |
601
+ | `getBasePriceCents(planVersion, currency, interval)` | Base price in cents for currency/interval |
602
+ | `getStripePriceIdForInterval(planVersion, currency, interval)` | Stripe price ID for checkout |
603
+ | `getQuotaOverageCents(planVersion, currency, quotaSlug, interval)` | Overage cents for a quota |
604
+ | `getQuotaDisplayWithVariant(planVersion, currency, quotaSlug, interval)` | Display value with overage for a variant |
605
+ | `getAvailableCurrenciesFromPlans(plans)` | Unique currency codes across plan versions |
606
+ | `getDisplayCurrency(planVersion, currency)` | Display currency (variant exists ? currency : plan.currency) |
607
+ | `getBillingIntervalAndCurrencyFromPriceId(planVersions, priceId)` | Resolve price ID to interval + currency |
608
+
609
+ Types: `IPricingVariant`, `PlanVersionWithPricingVariants`, `QuotaDisplayWithOverage`.
610
+
611
+ ### Quota utilities
612
+
613
+ | Export | Purpose |
614
+ |--------|--------|
615
+ | `getQuotaDisplayValue(quotaByInterval, interval?)` | Normalize `IQuotaByInterval` to `{ included, overage?, unitSize? }` |
616
+ | `formatQuotaWithPrice(value, unitName, options?)` | Format as "X included, then $Y.YY / unit" |
617
+
618
+ Types: `QuotaDisplayValue`, `FormatQuotaWithPriceOptions`. Plan/subscription types use `IQuotaByInterval` and `IQuotaIntervalValue` for per-interval quotas and overages.
619
+
620
+ ```tsx
621
+ import {
622
+ getCurrencySymbol,
623
+ formatCents,
624
+ getPricingVariant,
625
+ getBasePriceCents,
626
+ getQuotaDisplayValue,
627
+ formatQuotaWithPrice,
628
+ } from '@buildbase/sdk';
629
+
630
+ // Display price for a plan version in a currency
631
+ const variant = getPricingVariant(planVersion, 'usd');
632
+ const cents = getBasePriceCents(planVersion, 'usd', 'monthly');
633
+ if (cents != null) {
634
+ console.log(getCurrencySymbol('usd') + (cents / 100).toFixed(2));
635
+ }
636
+
637
+ // Quota display with overage
638
+ const display = getQuotaDisplayValue(planVersion.quotas?.videos, 'monthly');
639
+ const text = formatQuotaWithPrice(display, 'video', { currency: 'usd' });
640
+ ```
641
+
642
+ ## 📊 Quota Usage Tracking
643
+
644
+ Track and monitor metered usage for subscription quotas (e.g., API calls, emails, storage). Usage can be recorded from both the **client-side** (React app) and **server-side** (your backend).
645
+
646
+ ### When to use which?
647
+
648
+ | Scenario | Where to record | Why |
649
+ |----------|----------------|-----|
650
+ | User clicks "Send Email" button | Client-side (SDK hook) | User-initiated, immediate UI feedback needed |
651
+ | API request hits your backend | Server-side (REST API) | Backend controls the resource, more secure |
652
+ | Background job processes data | Server-side (REST API) | No browser context available |
653
+ | File upload completes | Either | Depends on where validation happens |
654
+
655
+ As a general rule: **record usage where the resource is consumed**. If your backend processes the work, record from the backend. If it's a client-side action, record from the client.
656
+
657
+ ---
658
+
659
+ ### Client-Side (React SDK)
660
+
661
+ Use the SDK hooks inside your React app. Quota gate components (see [Quota Gates](#-quota-gates)) automatically refresh after recording.
662
+
663
+ #### Record Usage
664
+
665
+ ```tsx
666
+ import { useRecordUsage, useSaaSWorkspaces } from '@buildbase/sdk';
667
+
668
+ function SendEmailButton() {
669
+ const { currentWorkspace } = useSaaSWorkspaces();
670
+ const { recordUsage, loading, error } = useRecordUsage(currentWorkspace?._id);
671
+
672
+ const handleSend = async () => {
673
+ try {
674
+ const result = await recordUsage({
675
+ quotaSlug: 'emails',
676
+ quantity: 1,
677
+ source: 'web-app', // optional: track where usage came from
678
+ idempotencyKey: 'email-abc', // optional: prevent duplicate recordings
679
+ });
680
+ console.log(`Used: ${result.consumed}/${result.included}, Available: ${result.available}`);
681
+ if (result.overage > 0) {
682
+ console.warn(`Overage: ${result.overage} units`);
683
+ }
684
+ } catch (err) {
685
+ console.error('Failed to record usage:', err);
686
+ }
687
+ };
688
+
689
+ return <button onClick={handleSend} disabled={loading}>Send Email</button>;
690
+ }
691
+ ```
692
+
693
+ #### Check Single Quota Status
694
+
695
+ ```tsx
696
+ import { useQuotaUsageStatus, useSaaSWorkspaces } from '@buildbase/sdk';
697
+
698
+ function QuotaStatusBar({ quotaSlug }: { quotaSlug: string }) {
699
+ const { currentWorkspace } = useSaaSWorkspaces();
700
+ const { status, loading, refetch } = useQuotaUsageStatus(currentWorkspace?._id, quotaSlug);
701
+
702
+ if (loading) return <Spinner />;
703
+ if (!status) return null;
704
+
705
+ const usagePercent = Math.round((status.consumed / status.included) * 100);
706
+
707
+ return (
708
+ <div>
709
+ <p>{quotaSlug}: {status.consumed} / {status.included} ({usagePercent}%)</p>
710
+ <p>Available: {status.available}</p>
711
+ {status.hasOverage && <p>Overage: {status.overage} units</p>}
712
+ <button onClick={refetch}>Refresh</button>
713
+ </div>
714
+ );
715
+ }
716
+ ```
717
+
718
+ #### Check All Quotas
719
+
720
+ ```tsx
721
+ import { useAllQuotaUsage, useSaaSWorkspaces } from '@buildbase/sdk';
722
+
723
+ function QuotaDashboard() {
724
+ const { currentWorkspace } = useSaaSWorkspaces();
725
+ const { quotas, loading, refetch } = useAllQuotaUsage(currentWorkspace?._id);
726
+
727
+ if (loading) return <Spinner />;
728
+ if (!quotas) return <p>No quota data available</p>;
729
+
730
+ return (
731
+ <div>
732
+ {Object.entries(quotas).map(([slug, usage]) => (
733
+ <div key={slug}>
734
+ <strong>{slug}</strong>: {usage.consumed} / {usage.included}
735
+ {usage.hasOverage && <span> (overage: {usage.overage})</span>}
736
+ </div>
737
+ ))}
738
+ </div>
739
+ );
740
+ }
741
+ ```
742
+
743
+ #### Usage Logs
744
+
745
+ ```tsx
746
+ import { useUsageLogs, useSaaSWorkspaces } from '@buildbase/sdk';
747
+
748
+ function UsageLogsTable() {
749
+ const { currentWorkspace } = useSaaSWorkspaces();
750
+ const {
751
+ logs, totalDocs, totalPages, page, hasNextPage, loading, refetch,
752
+ } = useUsageLogs(
753
+ currentWorkspace?._id,
754
+ 'api_calls', // optional: filter by quota slug
755
+ { limit: 20, page: 1 } // optional: pagination and filters
756
+ );
757
+
758
+ if (loading) return <Spinner />;
759
+
760
+ return (
761
+ <div>
762
+ <table>
763
+ <thead>
764
+ <tr><th>Quota</th><th>Quantity</th><th>Source</th><th>Date</th></tr>
765
+ </thead>
766
+ <tbody>
767
+ {logs.map(log => (
768
+ <tr key={log._id}>
769
+ <td>{log.quotaSlug}</td>
770
+ <td>{log.quantity}</td>
771
+ <td>{log.source ?? '-'}</td>
772
+ <td>{new Date(log.createdAt).toLocaleString()}</td>
773
+ </tr>
774
+ ))}
775
+ </tbody>
776
+ </table>
777
+ <p>Page {page} of {totalPages} ({totalDocs} total)</p>
778
+ </div>
779
+ );
780
+ }
781
+ ```
782
+
783
+ **`useUsageLogs` parameters:**
784
+
785
+ | Parameter | Type | Required | Description |
786
+ |-----------|------|----------|-------------|
787
+ | `workspaceId` | `string \| null \| undefined` | Yes | Workspace ID (null/undefined disables fetching) |
788
+ | `quotaSlug` | `string` | No | Filter logs by quota slug |
789
+ | `options.from` | `string` | No | ISO date string — filter logs from this date |
790
+ | `options.to` | `string` | No | ISO date string — filter logs until this date |
791
+ | `options.source` | `string` | No | Filter logs by source |
792
+ | `options.page` | `number` | No | Page number (default: 1) |
793
+ | `options.limit` | `number` | No | Results per page (default: 20) |
794
+
795
+ #### Client-Side Hooks Summary
796
+
797
+ | Hook | Purpose |
798
+ |------|---------|
799
+ | `useRecordUsage(workspaceId)` | Record quota consumption (mutation) |
800
+ | `useQuotaUsageStatus(workspaceId, quotaSlug)` | Get single quota status (auto-fetches) |
801
+ | `useAllQuotaUsage(workspaceId)` | Get all quotas status (auto-fetches) |
802
+ | `useUsageLogs(workspaceId, quotaSlug?, options?)` | Get paginated usage history (auto-fetches) |
803
+
804
+ ---
805
+
806
+ ### Server-Side (REST API)
807
+
808
+ For backend services, background jobs, or API routes — call the BuildBase API directly. This is the recommended approach when usage happens on your server (e.g., processing an API request, running a cron job, handling webhooks).
809
+
810
+ #### Step 1: Get a Session ID
811
+
812
+ Exchange your org API token for a session ID. You can do this once and reuse the session for multiple requests (default expiry: 30 days).
813
+
814
+ ```ts
815
+ // Do this once at startup or cache the result
816
+ const TOKEN = 'your-org-id:your-api-secret'; // from BuildBase dashboard
817
+
818
+ async function getSessionId(): Promise<string> {
819
+ const response = await fetch('https://your-server.buildbase.app/api/v1/public/token/exchange', {
820
+ method: 'POST',
821
+ headers: { 'Content-Type': 'application/json' },
822
+ body: JSON.stringify({
823
+ token: TOKEN,
824
+ expiresIn: 2592000, // 30 days (optional, default is 30 days)
825
+ }),
826
+ });
827
+ const data = await response.json();
828
+ return data.sessionId;
829
+ }
830
+ ```
831
+
832
+ #### Step 2: Record Usage
833
+
834
+ ```ts
835
+ const SESSION_ID = await getSessionId();
836
+ const BASE_URL = 'https://your-server.buildbase.app/api/v1/public';
837
+
838
+ async function recordUsage(workspaceId: string, quotaSlug: string, quantity: number) {
839
+ const response = await fetch(`${BASE_URL}/workspaces/${workspaceId}/subscription/usage`, {
840
+ method: 'POST',
841
+ headers: {
842
+ 'Content-Type': 'application/json',
843
+ 'x-session-id': SESSION_ID,
844
+ },
845
+ body: JSON.stringify({
846
+ quotaSlug,
847
+ quantity,
848
+ source: 'backend', // optional: helps distinguish from client-side usage
849
+ metadata: {}, // optional: attach custom data
850
+ idempotencyKey: undefined, // optional: prevent duplicate recordings
851
+ }),
852
+ });
853
+ return response.json();
854
+ }
855
+
856
+ // Example: Record usage in an Express route handler
857
+ app.post('/api/generate-report', async (req, res) => {
858
+ const { workspaceId } = req.user; // your auth
859
+
860
+ // Record quota usage BEFORE or AFTER doing the work
861
+ const usage = await recordUsage(workspaceId, 'reports', 1);
862
+
863
+ if (usage.available <= 0 && !usage.hasOverage) {
864
+ return res.status(429).json({ error: 'Report quota exceeded' });
865
+ }
866
+
867
+ // ... generate the report ...
868
+ res.json({ success: true, quotaRemaining: usage.available });
869
+ });
870
+ ```
871
+
872
+ #### Step 3: Check Usage Status (Optional)
873
+
874
+ ```ts
875
+ // Get status for a single quota
876
+ async function getQuotaStatus(workspaceId: string, quotaSlug: string) {
877
+ const response = await fetch(
878
+ `${BASE_URL}/workspaces/${workspaceId}/subscription/usage/status?quotaSlug=${quotaSlug}`,
879
+ { headers: { 'x-session-id': SESSION_ID } }
880
+ );
881
+ return response.json();
882
+ // Returns: { quotaSlug, consumed, included, available, overage, hasOverage }
883
+ }
884
+
885
+ // Get status for ALL quotas
886
+ async function getAllQuotaStatus(workspaceId: string) {
887
+ const response = await fetch(
888
+ `${BASE_URL}/workspaces/${workspaceId}/subscription/usage/all`,
889
+ { headers: { 'x-session-id': SESSION_ID } }
890
+ );
891
+ return response.json();
892
+ // Returns: { quotas: { [slug]: { consumed, included, available, overage, hasOverage } } }
893
+ }
894
+
895
+ // Example: Check before allowing an action
896
+ app.post('/api/send-email', async (req, res) => {
897
+ const status = await getQuotaStatus(req.user.workspaceId, 'emails');
898
+
899
+ if (status.available <= 0) {
900
+ return res.status(429).json({
901
+ error: 'Email quota exceeded',
902
+ consumed: status.consumed,
903
+ included: status.included,
904
+ });
905
+ }
906
+
907
+ await recordUsage(req.user.workspaceId, 'emails', 1);
908
+ // ... send the email ...
909
+ });
910
+ ```
911
+
912
+ #### Server-Side API Reference
913
+
914
+ | Endpoint | Method | Description |
915
+ |----------|--------|-------------|
916
+ | `/api/v1/public/token/exchange` | POST | Exchange API token for session ID |
917
+ | `/api/v1/public/workspaces/:id/subscription/usage` | POST | Record quota usage |
918
+ | `/api/v1/public/workspaces/:id/subscription/usage/status?quotaSlug=X` | GET | Get single quota status |
919
+ | `/api/v1/public/workspaces/:id/subscription/usage/all` | GET | Get all quotas status |
920
+ | `/api/v1/public/workspaces/:id/subscription/usage/logs` | GET | Get paginated usage logs |
921
+
922
+ All endpoints (except `/token/exchange`) require the `x-session-id` header.
923
+
924
+ **Record usage request body:**
925
+
926
+ | Field | Type | Required | Description |
927
+ |-------|------|----------|-------------|
928
+ | `quotaSlug` | `string` | Yes | Quota identifier (e.g. `'api_calls'`, `'emails'`, `'storage'`) |
929
+ | `quantity` | `number` | Yes | Units to consume (minimum 1) |
930
+ | `metadata` | `object` | No | Custom metadata to attach to the usage record |
931
+ | `source` | `string` | No | Source identifier (e.g. `'backend'`, `'worker'`, `'cron'`) |
932
+ | `idempotencyKey` | `string` | No | Unique key for deduplication |
933
+
934
+ **Record usage response:**
935
+
936
+ | Field | Type | Description |
937
+ |-------|------|-------------|
938
+ | `used` | `number` | Quantity recorded in this request |
939
+ | `consumed` | `number` | Total usage in the current billing period |
940
+ | `included` | `number` | Total units included in the plan |
941
+ | `available` | `number` | Remaining units before overage |
942
+ | `overage` | `number` | Units used beyond the included amount |
943
+ | `billedAsync` | `boolean` | Whether overage billing was queued to Stripe |
944
+
945
+ ## 🚦 Quota Gates
946
+
947
+ Control UI visibility based on quota usage status. Quota data is loaded once per workspace via `QuotaUsageContextProvider` (included in `SaaSOSProvider` by default) and refetched automatically after recording usage.
948
+
949
+ ### Gate Components
950
+
951
+ ```tsx
952
+ import {
953
+ WhenQuotaAvailable,
954
+ WhenQuotaExhausted,
955
+ WhenQuotaOverage,
956
+ WhenQuotaThreshold,
957
+ } from '@buildbase/sdk';
958
+
959
+ function Dashboard() {
960
+ return (
961
+ <div>
962
+ {/* Show action button only when quota has remaining units */}
963
+ <WhenQuotaAvailable slug="api_calls">
964
+ <MakeApiCallButton />
965
+ </WhenQuotaAvailable>
966
+
967
+ {/* Show upgrade prompt when quota is fully consumed */}
968
+ <WhenQuotaExhausted slug="api_calls">
969
+ <UpgradePrompt message="You've used all your API calls this month." />
970
+ </WhenQuotaExhausted>
971
+
972
+ {/* Show warning when usage exceeds included amount (overage billing active) */}
973
+ <WhenQuotaOverage slug="api_calls">
974
+ <OverageBillingWarning />
975
+ </WhenQuotaOverage>
976
+
977
+ {/* Show warning when usage reaches 80% of included amount */}
978
+ <WhenQuotaThreshold slug="api_calls" threshold={80}>
979
+ <p>Warning: You've used over 80% of your API calls.</p>
980
+ </WhenQuotaThreshold>
981
+ </div>
982
+ );
983
+ }
984
+ ```
985
+
986
+ ### With Loading and Fallback
987
+
988
+ All quota gate components support optional `loadingComponent` and `fallbackComponent` props:
989
+
990
+ ```tsx
991
+ <WhenQuotaAvailable
992
+ slug="emails"
993
+ loadingComponent={<Skeleton className="h-10" />}
994
+ fallbackComponent={<p>Email quota exhausted. <a href="/upgrade">Upgrade now</a></p>}
995
+ >
996
+ <SendEmailButton />
997
+ </WhenQuotaAvailable>
998
+
999
+ <WhenQuotaThreshold
1000
+ slug="storage"
1001
+ threshold={90}
1002
+ loadingComponent={<Spinner />}
1003
+ fallbackComponent={null}
1004
+ >
1005
+ <StorageWarningBanner />
1006
+ </WhenQuotaThreshold>
1007
+ ```
1008
+
1009
+ ### Quota Gate Components Reference
1010
+
1011
+ | Component | Renders children when | Props |
1012
+ |-----------|----------------------|-------|
1013
+ | `WhenQuotaAvailable` | Quota has remaining units (`available > 0`) | `slug`, `children`, `loadingComponent?`, `fallbackComponent?` |
1014
+ | `WhenQuotaExhausted` | Quota is fully consumed (`available <= 0`) | `slug`, `children`, `loadingComponent?`, `fallbackComponent?` |
1015
+ | `WhenQuotaOverage` | Usage exceeds included amount (`hasOverage`) | `slug`, `children`, `loadingComponent?`, `fallbackComponent?` |
1016
+ | `WhenQuotaThreshold` | Usage percentage >= threshold | `slug`, `threshold` (0-100), `children`, `loadingComponent?`, `fallbackComponent?` |
1017
+
1018
+ All gates must be used inside `QuotaUsageContextProvider` (included in `SaaSOSProvider`). By default they return `null` while loading or when the condition is not met.
1019
+
1020
+ ### useQuotaUsageContext
1021
+
1022
+ Use the hook when you need raw quota data or a manual refetch (e.g. after a bulk operation):
1023
+
1024
+ ```tsx
1025
+ import { useQuotaUsageContext } from '@buildbase/sdk';
1026
+
1027
+ function QuotaDebug() {
1028
+ const { quotas, loading, refetch } = useQuotaUsageContext();
1029
+
1030
+ if (loading) return <Spinner />;
1031
+ if (!quotas) return <p>No quota data</p>;
1032
+
1033
+ return (
1034
+ <div>
1035
+ {Object.entries(quotas).map(([slug, usage]) => (
1036
+ <p key={slug}>{slug}: {usage.consumed}/{usage.included}</p>
1037
+ ))}
1038
+ <button onClick={() => refetch()}>Refresh</button>
1039
+ </div>
1040
+ );
1041
+ }
1042
+ ```
1043
+
1044
+ | Property | Type | Description |
1045
+ |----------|------|-------------|
1046
+ | `quotas` | `Record<string, IQuotaUsageStatus> \| null` | Current quota usage data keyed by slug |
1047
+ | `loading` | `boolean` | True while quota data is being fetched |
1048
+ | `refetch` | `() => Promise<void>` | Manually refetch all quota usage |
1049
+
1050
+ **When quota usage refetches:**
1051
+ - When the current workspace changes (automatic).
1052
+ - When usage is recorded via `useRecordUsage` — refetch is triggered automatically.
1053
+ - When you call `refetch()` manually.
1054
+
572
1055
  ## 📝 Beta Form Component
573
1056
 
574
1057
  Use the pre-built `BetaForm` component for signup/waitlist forms:
@@ -667,9 +1150,17 @@ All SDK API clients extend a shared base class and are exported from the package
667
1150
  | `BaseApi` | Abstract base (URL, auth, `fetchJson`/`fetchResponse`) – extend for custom APIs |
668
1151
  | `IBaseApiConfig` | Config type: `serverUrl`, `version`, optional `orgId` |
669
1152
  | `UserApi` | User attributes and features |
670
- | `WorkspaceApi` | Workspaces, subscription, invoices, users |
1153
+ | `WorkspaceApi` | Workspaces, subscription, invoices, quota usage, users |
671
1154
  | `SettingsApi` | Organization settings |
672
1155
 
1156
+ ### Currency, pricing variant & quota utilities
1157
+
1158
+ | Category | Exports |
1159
+ | -------- | ------- |
1160
+ | **Currency** | `CURRENCY_DISPLAY`, `CURRENCY_FLAG`, `PLAN_CURRENCY_CODES`, `PLAN_CURRENCY_OPTIONS`, `getCurrencySymbol`, `getCurrencyFlag`, `formatCents`, `formatOverageRate`, `formatOverageRateWithLabel`, `formatQuotaIncludedOverage`, `getQuotaUnitLabelFromName` |
1161
+ | **Pricing variants** | `getPricingVariant`, `getBasePriceCents`, `getStripePriceIdForInterval`, `getQuotaOverageCents`, `getQuotaDisplayWithVariant`, `getAvailableCurrenciesFromPlans`, `getDisplayCurrency`, `getBillingIntervalAndCurrencyFromPriceId`; types: `IPricingVariant`, `PlanVersionWithPricingVariants`, `QuotaDisplayWithOverage` |
1162
+ | **Quota** | `getQuotaDisplayValue`, `formatQuotaWithPrice`; types: `QuotaDisplayValue`, `FormatQuotaWithPriceOptions`. Plan types use `IQuotaByInterval`, `IQuotaIntervalValue` for per-interval quotas. |
1163
+
673
1164
  Get OS config from `useSaaSOs()` and instantiate API classes when you need low-level access; otherwise prefer the high-level hooks (`useSaaSWorkspaces`, `useUserAttributes`, `useSaaSSettings`, etc.):
674
1165
 
675
1166
  ```tsx
@@ -697,7 +1188,9 @@ Prefer these SDK hooks for state and operations instead of `useAppSelector`:
697
1188
  | `useUserAttributes()` | User attributes and update/refresh |
698
1189
  | `useUserFeatures()` | User feature flags |
699
1190
  | `useSubscriptionContext()` | Subscription for current workspace (response, loading, refetch); use inside SubscriptionContextProvider |
700
- | Subscription hooks | `usePublicPlans`, `useSubscription`, `usePlanGroup`, etc. |
1191
+ | Subscription hooks | `usePublicPlans`, `useSubscription`, `usePlanGroup`, `useCreateCheckoutSession`, `useUpdateSubscription`, `useCancelSubscription`, `useResumeSubscription`, `useInvoices`, `useInvoice` |
1192
+ | `useQuotaUsageContext()` | Quota usage for current workspace (quotas, loading, refetch); use inside QuotaUsageContextProvider |
1193
+ | Quota usage hooks | `useRecordUsage`, `useQuotaUsageStatus`, `useAllQuotaUsage`, `useUsageLogs` |
701
1194
 
702
1195
  Using hooks keeps your code stable if internal state shape changes and avoids direct Redux/context coupling.
703
1196
 
@@ -889,6 +1382,32 @@ function BillingPage() {
889
1382
 
890
1383
  SubscriptionContextProvider is included in SaaSOSProvider by default, so no extra wrapper is needed.
891
1384
 
1385
+ ### Pattern 4c: Quota-Gated UI
1386
+
1387
+ ```tsx
1388
+ import { WhenQuotaAvailable, WhenQuotaExhausted, WhenQuotaThreshold } from '@buildbase/sdk';
1389
+
1390
+ function FeatureWithQuota() {
1391
+ return (
1392
+ <div>
1393
+ <WhenQuotaThreshold slug="api_calls" threshold={80}>
1394
+ <WarningBanner message="You're running low on API calls" />
1395
+ </WhenQuotaThreshold>
1396
+
1397
+ <WhenQuotaAvailable slug="api_calls" fallbackComponent={<UpgradePrompt />}>
1398
+ <ApiCallButton />
1399
+ </WhenQuotaAvailable>
1400
+
1401
+ <WhenQuotaExhausted slug="api_calls">
1402
+ <p>No API calls remaining. <a href="/billing">Upgrade your plan</a></p>
1403
+ </WhenQuotaExhausted>
1404
+ </div>
1405
+ );
1406
+ }
1407
+ ```
1408
+
1409
+ QuotaUsageContextProvider is included in SaaSOSProvider by default, so no extra wrapper is needed. Quota data auto-refreshes after `useRecordUsage` calls.
1410
+
892
1411
  ### Pattern 5: Handling Workspace Changes
893
1412
 
894
1413
  ```tsx