@buildbase/sdk 0.0.18 β†’ 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.
Files changed (30) hide show
  1. package/README.md +537 -25
  2. package/dist/index.d.ts +784 -62
  3. package/dist/index.esm.js +5 -5
  4. package/dist/index.esm.js.map +1 -1
  5. package/dist/index.js +5 -5
  6. package/dist/index.js.map +1 -1
  7. package/dist/saas-os.css +1 -1
  8. package/dist/types/api/currency-utils.d.ts +44 -0
  9. package/dist/types/api/index.d.ts +5 -0
  10. package/dist/types/api/pricing-variant-utils.d.ts +53 -0
  11. package/dist/types/api/quota-utils.d.ts +7 -7
  12. package/dist/types/api/types.d.ts +156 -41
  13. package/dist/types/components/features/index.d.ts +16 -4
  14. package/dist/types/components/pricing/PricingPage.d.ts +2 -0
  15. package/dist/types/components/quota/index.d.ts +121 -0
  16. package/dist/types/components/subscription/index.d.ts +12 -3
  17. package/dist/types/components/user/auth.d.ts +8 -2
  18. package/dist/types/components/user/role.d.ts +8 -2
  19. package/dist/types/contexts/QuotaUsageContext/QuotaUsageContext.d.ts +22 -0
  20. package/dist/types/contexts/QuotaUsageContext/index.d.ts +2 -0
  21. package/dist/types/contexts/QuotaUsageContext/quotaUsageInvalidation.d.ts +19 -0
  22. package/dist/types/contexts/QuotaUsageContext/types.d.ts +14 -0
  23. package/dist/types/contexts/SubscriptionContext/types.d.ts +2 -0
  24. package/dist/types/index.d.ts +11 -3
  25. package/dist/types/providers/workspace/api.d.ts +28 -1
  26. package/dist/types/providers/workspace/hooks.d.ts +1 -1
  27. package/dist/types/providers/workspace/subscription-hooks.d.ts +180 -1
  28. package/dist/types/providers/workspace/types.d.ts +18 -1
  29. package/dist/types/providers/workspace/ui/SubscriptionDialog.d.ts +7 -1
  30. package/package.json +1 -1
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
@@ -318,6 +322,18 @@ function FeatureExample() {
318
322
 
319
323
  Use the `useUserFeatures` hook to check feature flags programmatically:
320
324
 
325
+ ```tsx
326
+ import { useUserFeatures } from '@buildbase/sdk';
327
+
328
+ function FeatureCheck() {
329
+ const { features, isFeatureEnabled, refreshFeatures } = useUserFeatures();
330
+
331
+ return (
332
+ <div>{isFeatureEnabled('premium-features') ? <PremiumContent /> : <StandardContent />}</div>
333
+ );
334
+ }
335
+ ```
336
+
321
337
  ## πŸ“‹ Subscription Gates
322
338
 
323
339
  Control UI visibility based on the current workspace’s subscription. Subscription data is loaded once per workspace and refetched when the workspace changes or when the subscription is updated (e.g. upgrade, cancel, resume).
@@ -411,22 +427,6 @@ function SubscriptionStatus() {
411
427
  - When subscription is updated via SDK (e.g. `useUpdateSubscription`, cancel, resume) β€” refetch is triggered automatically.
412
428
  - When you call `refetch()` (e.g. after redirect from checkout).
413
429
 
414
- ### Feature Flags Hook
415
-
416
- Use the `useUserFeatures` hook to check feature flags programmatically:
417
-
418
- ```tsx
419
- import { useUserFeatures } from '@buildbase/sdk';
420
-
421
- function FeatureCheck() {
422
- const { features, isFeatureEnabled, refreshFeatures } = useUserFeatures();
423
-
424
- return (
425
- <div>{isFeatureEnabled('premium-features') ? <PremiumContent /> : <StandardContent />}</div>
426
- );
427
- }
428
- ```
429
-
430
430
  ## πŸ‘€ User Management
431
431
 
432
432
  ### User Attributes
@@ -573,6 +573,485 @@ function PublicPricingPage() {
573
573
 
574
574
  **Backend requirement**: `GET /api/v1/public/{orgId}/plans/{groupSlug}` must be implemented and allow unauthenticated access.
575
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
+
576
1055
  ## πŸ“ Beta Form Component
577
1056
 
578
1057
  Use the pre-built `BetaForm` component for signup/waitlist forms:
@@ -640,7 +1119,7 @@ import { SaaSOSProvider, eventEmitter } from '@buildbase/sdk';
640
1119
 
641
1120
  ## πŸ›‘οΈ Error Handling
642
1121
 
643
- The SDK handles errors internally: API failures, auth errors, and component errors are logged and surfaced through hook states (e.g. `error` from `useSaaSWorkspaces`) and callbacks. Wrap your app in an error boundary of your choice to catch React errors. For failed operations, check the `error` property on hooks and handle it in your UI (e.g. toast or inline message).
1122
+ The SDK handles errors internally: API failures, auth errors, and component errors are logged and surfaced through hook states (e.g. `error` from `useSaaSWorkspaces`) and callbacks. **SaaSOSProvider** wraps its children in an internal **SDKErrorBoundary** to catch React render errors inside the SDK tree. For app-level errors, wrap your app (or routes) in your own error boundary (e.g. React’s `ErrorBoundary` or your framework’s error UI). For failed async operations, check the `error` property on hooks and show user feedback (e.g. toast or inline message). See [Error codes](docs/ERROR_CODES.md) for SDK error codes and HTTP mappings.
644
1123
 
645
1124
  ## βš™οΈ Settings
646
1125
 
@@ -671,24 +1150,29 @@ All SDK API clients extend a shared base class and are exported from the package
671
1150
  | `BaseApi` | Abstract base (URL, auth, `fetchJson`/`fetchResponse`) – extend for custom APIs |
672
1151
  | `IBaseApiConfig` | Config type: `serverUrl`, `version`, optional `orgId` |
673
1152
  | `UserApi` | User attributes and features |
674
- | `WorkspaceApi` | Workspaces, subscription, invoices, users |
1153
+ | `WorkspaceApi` | Workspaces, subscription, invoices, quota usage, users |
675
1154
  | `SettingsApi` | Organization settings |
676
1155
 
677
- Use the hooks (`useUserApi`, `useWorkspaceApi`, etc.) for a ready-made instance with OS config, or instantiate with your own config:
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
+
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.):
678
1165
 
679
1166
  ```tsx
680
1167
  import { UserApi, WorkspaceApi, SettingsApi, useSaaSOs } from '@buildbase/sdk';
681
1168
 
682
- // Via hook (uses OS config from context)
683
- const api = useWorkspaceApi(); // or useUserApi(), etc.
684
-
685
- // Or instantiate with config
686
1169
  const os = useSaaSOs();
687
1170
  const workspaceApi = new WorkspaceApi({
688
1171
  serverUrl: os.serverUrl,
689
1172
  version: os.version,
690
1173
  orgId: os.orgId,
691
1174
  });
1175
+ // Similarly: new UserApi({ ... }), new SettingsApi({ ... })
692
1176
  ```
693
1177
 
694
1178
  ### Hooks
@@ -704,7 +1188,9 @@ Prefer these SDK hooks for state and operations instead of `useAppSelector`:
704
1188
  | `useUserAttributes()` | User attributes and update/refresh |
705
1189
  | `useUserFeatures()` | User feature flags |
706
1190
  | `useSubscriptionContext()` | Subscription for current workspace (response, loading, refetch); use inside SubscriptionContextProvider |
707
- | 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` |
708
1194
 
709
1195
  Using hooks keeps your code stable if internal state shape changes and avoids direct Redux/context coupling.
710
1196
 
@@ -719,7 +1205,7 @@ All TypeScript types are exported for type safety. See the [TypeScript definitio
719
1205
 
720
1206
  ### Further documentation
721
1207
 
722
- - [Architecture](docs/ARCHITECTURE.md) – Layers, APIs (BaseApi, UserApi, WorkspaceApi, SettingsApi), state, auth flow
1208
+ - [Architecture](docs/ARCHITECTURE.md) – Layers, providers, APIs (BaseApi, UserApi, WorkspaceApi, SettingsApi), state, auth flow
723
1209
  - [Error codes](docs/ERROR_CODES.md) – SDK error codes and HTTP status mappings
724
1210
 
725
1211
  ## βš™οΈ Configuration Reference
@@ -896,6 +1382,32 @@ function BillingPage() {
896
1382
 
897
1383
  SubscriptionContextProvider is included in SaaSOSProvider by default, so no extra wrapper is needed.
898
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
+
899
1411
  ### Pattern 5: Handling Workspace Changes
900
1412
 
901
1413
  ```tsx