@buildbase/sdk 0.0.40 → 0.0.42
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 +440 -335
- package/dist/css/styles.css +1 -1
- package/dist/index.d.ts +26 -7
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/react/index.d.ts +81 -39
- package/dist/react/index.js +6 -6
- package/dist/react/index.mjs +6 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,8 @@ Also works server-side (Next.js API routes, Express, Hono) — see [Server-Side
|
|
|
10
10
|
- [Installation](#installation)
|
|
11
11
|
- [Quick Start](#quick-start)
|
|
12
12
|
- [Authentication](#-authentication)
|
|
13
|
+
- [Redirect Preservation](#redirect-preservation)
|
|
14
|
+
- [Affiliate / Referral Tracking](#affiliate--referral-tracking)
|
|
13
15
|
- [Role-Based Access Control](#-role-based-access-control)
|
|
14
16
|
- [Feature Flags](#️-feature-flags)
|
|
15
17
|
- [Subscription Gates](#-subscription-gates)
|
|
@@ -35,7 +37,7 @@ Also works server-side (Next.js API routes, Express, Hono) — see [Server-Side
|
|
|
35
37
|
|
|
36
38
|
## 🚀 Features
|
|
37
39
|
|
|
38
|
-
- **🔐 Authentication System** - Complete auth flow with sign-in/sign-out
|
|
40
|
+
- **🔐 Authentication System** - Complete auth flow with sign-in/sign-out and redirect preservation
|
|
39
41
|
- **🏢 Workspace Management** - Multi-workspace support with switching capabilities
|
|
40
42
|
- **👥 Role-Based Access Control** - User roles and workspace-specific permissions
|
|
41
43
|
- **🎯 Feature Flags** - Workspace-level and user-level feature toggles
|
|
@@ -45,6 +47,7 @@ Also works server-side (Next.js API routes, Express, Hono) — see [Server-Side
|
|
|
45
47
|
- **📬 Notifications** - Email + push notification system with per-event channel control, workspace preferences, and server-side `notification.send()` API
|
|
46
48
|
- **💺 Seat-Based Pricing** - Per-seat billing with included seats, billable seat tracking, and seat limit enforcement
|
|
47
49
|
- **💱 Multi-Currency** - Per-currency pricing variants with workspace billing currency lock
|
|
50
|
+
- **🤝 Affiliate Tracking** - Pass referral data to Stripe checkout via `stripeOptions` (Rewardful, Endorsely, FirstPromoter, etc.)
|
|
48
51
|
- **📊 Quota Usage Tracking** - Record and monitor metered usage (API calls, storage, etc.) with real-time status
|
|
49
52
|
- **📈 Usage Dashboard** - Built-in workspace settings page showing quota consumption, overage billing breakdowns, and billing period info
|
|
50
53
|
- **👤 User Management** - User attributes and feature flags management
|
|
@@ -67,17 +70,17 @@ npm install @buildbase/sdk react@^19.0.0 react-dom@^19.0.0
|
|
|
67
70
|
|
|
68
71
|
```tsx
|
|
69
72
|
// app/layout.tsx (or your root layout)
|
|
70
|
-
import '@buildbase/sdk/css'
|
|
73
|
+
import '@buildbase/sdk/css';
|
|
71
74
|
```
|
|
72
75
|
|
|
73
76
|
### 2. Create Provider
|
|
74
77
|
|
|
75
78
|
```tsx
|
|
76
79
|
// components/provider.tsx
|
|
77
|
-
'use client'
|
|
80
|
+
'use client';
|
|
78
81
|
|
|
79
|
-
import { SaaSOSProvider } from '@buildbase/sdk/react'
|
|
80
|
-
import { ApiVersion } from '@buildbase/sdk'
|
|
82
|
+
import { SaaSOSProvider } from '@buildbase/sdk/react';
|
|
83
|
+
import { ApiVersion } from '@buildbase/sdk';
|
|
81
84
|
|
|
82
85
|
export default function SaaSProvider({ children }: { children: React.ReactNode }) {
|
|
83
86
|
return (
|
|
@@ -91,40 +94,40 @@ export default function SaaSProvider({ children }: { children: React.ReactNode }
|
|
|
91
94
|
callbacks: {
|
|
92
95
|
// Called on page refresh to restore session from httpOnly cookie
|
|
93
96
|
getSession: async () => {
|
|
94
|
-
const res = await fetch('/api/auth/session')
|
|
95
|
-
const data = await res.json()
|
|
96
|
-
return data.sessionId ?? null
|
|
97
|
+
const res = await fetch('/api/auth/session');
|
|
98
|
+
const data = await res.json();
|
|
99
|
+
return data.sessionId ?? null;
|
|
97
100
|
},
|
|
98
101
|
|
|
99
102
|
// Called after OAuth redirect to exchange code for sessionId
|
|
100
|
-
handleAuthentication: async
|
|
103
|
+
handleAuthentication: async code => {
|
|
101
104
|
const res = await fetch('/api/auth/verify', {
|
|
102
105
|
method: 'POST',
|
|
103
106
|
headers: { 'Content-Type': 'application/json' },
|
|
104
107
|
body: JSON.stringify({ code }),
|
|
105
|
-
})
|
|
106
|
-
const data = await res.json()
|
|
107
|
-
return { sessionId: data.sessionId }
|
|
108
|
+
});
|
|
109
|
+
const data = await res.json();
|
|
110
|
+
return { sessionId: data.sessionId };
|
|
108
111
|
},
|
|
109
112
|
|
|
110
113
|
// Called on sign out to clear the httpOnly cookie
|
|
111
114
|
onSignOut: async () => {
|
|
112
|
-
await fetch('/api/auth/signout', { method: 'POST' })
|
|
113
|
-
window.location.reload()
|
|
115
|
+
await fetch('/api/auth/signout', { method: 'POST' });
|
|
116
|
+
window.location.reload();
|
|
114
117
|
},
|
|
115
118
|
|
|
116
119
|
handleEvent: (eventType, data) => {
|
|
117
|
-
console.log('SDK Event:', eventType, data)
|
|
120
|
+
console.log('SDK Event:', eventType, data);
|
|
118
121
|
},
|
|
119
122
|
onWorkspaceChange: async ({ workspace, user, role }) => {
|
|
120
|
-
console.log('Switching to:', workspace.name, 'as', role)
|
|
123
|
+
console.log('Switching to:', workspace.name, 'as', role);
|
|
121
124
|
},
|
|
122
125
|
},
|
|
123
126
|
}}
|
|
124
127
|
>
|
|
125
128
|
{children}
|
|
126
129
|
</SaaSOSProvider>
|
|
127
|
-
)
|
|
130
|
+
);
|
|
128
131
|
}
|
|
129
132
|
```
|
|
130
133
|
|
|
@@ -138,8 +141,8 @@ The SDK uses the same session pattern as next-auth: the session token lives in a
|
|
|
138
141
|
|
|
139
142
|
```tsx
|
|
140
143
|
// app/layout.tsx
|
|
141
|
-
import SaaSProvider from '@/components/provider'
|
|
142
|
-
import '@buildbase/sdk/css'
|
|
144
|
+
import SaaSProvider from '@/components/provider';
|
|
145
|
+
import '@buildbase/sdk/css';
|
|
143
146
|
|
|
144
147
|
export default function RootLayout({ children }) {
|
|
145
148
|
return (
|
|
@@ -148,7 +151,7 @@ export default function RootLayout({ children }) {
|
|
|
148
151
|
<SaaSProvider>{children}</SaaSProvider>
|
|
149
152
|
</body>
|
|
150
153
|
</html>
|
|
151
|
-
)
|
|
154
|
+
);
|
|
152
155
|
}
|
|
153
156
|
```
|
|
154
157
|
|
|
@@ -212,7 +215,7 @@ function AuthExample() {
|
|
|
212
215
|
{!isAuthenticated ? (
|
|
213
216
|
<div>
|
|
214
217
|
<h1>Welcome! Please sign in</h1>
|
|
215
|
-
<button onClick={signIn} disabled={status === 'loading'}>
|
|
218
|
+
<button onClick={() => signIn()} disabled={status === 'loading'}>
|
|
216
219
|
{status === 'loading' ? 'Signing in...' : 'Sign In'}
|
|
217
220
|
</button>
|
|
218
221
|
</div>
|
|
@@ -239,7 +242,7 @@ const {
|
|
|
239
242
|
isLoading, // Boolean: true when checking authentication status
|
|
240
243
|
isRedirecting, // Boolean: true when redirecting for OAuth
|
|
241
244
|
status, // AuthStatus: 'loading' | 'redirecting' | 'authenticating' | 'authenticated' | 'unauthenticated' (use AuthStatus enum for type-safe checks)
|
|
242
|
-
signIn, // Function: initiates sign-in flow
|
|
245
|
+
signIn, // Function: initiates sign-in flow. Accepts optional returnUrl to redirect back after login.
|
|
243
246
|
signOut, // Function: signs out the user
|
|
244
247
|
openWorkspaceSettings, // Function: opens workspace settings dialog to a specific section
|
|
245
248
|
} = useSaaSAuth();
|
|
@@ -250,13 +253,13 @@ const {
|
|
|
250
253
|
Open the workspace settings dialog to a specific section:
|
|
251
254
|
|
|
252
255
|
```tsx
|
|
253
|
-
openWorkspaceSettings('profile');
|
|
254
|
-
openWorkspaceSettings('general');
|
|
255
|
-
openWorkspaceSettings('users');
|
|
256
|
+
openWorkspaceSettings('profile'); // Account profile
|
|
257
|
+
openWorkspaceSettings('general'); // Workspace name, icon
|
|
258
|
+
openWorkspaceSettings('users'); // Workspace members
|
|
256
259
|
openWorkspaceSettings('subscription'); // Plan & Billing
|
|
257
|
-
openWorkspaceSettings('usage');
|
|
258
|
-
openWorkspaceSettings('features');
|
|
259
|
-
openWorkspaceSettings('danger');
|
|
260
|
+
openWorkspaceSettings('usage'); // Quota usage dashboard
|
|
261
|
+
openWorkspaceSettings('features'); // Feature toggles
|
|
262
|
+
openWorkspaceSettings('danger'); // Delete workspace (owner only)
|
|
260
263
|
```
|
|
261
264
|
|
|
262
265
|
### Authentication Components
|
|
@@ -281,6 +284,52 @@ function App() {
|
|
|
281
284
|
}
|
|
282
285
|
```
|
|
283
286
|
|
|
287
|
+
### Redirect Preservation
|
|
288
|
+
|
|
289
|
+
The SDK automatically preserves the URL when `signIn()` is called. After login, the user is redirected back to the page they were on.
|
|
290
|
+
|
|
291
|
+
```tsx
|
|
292
|
+
// Automatic — just call signIn(), the current URL is saved
|
|
293
|
+
signIn();
|
|
294
|
+
|
|
295
|
+
// Custom — pass a specific URL to redirect to after login
|
|
296
|
+
signIn('https://app.com/dashboard?bb=action:selectPlan,plan:abc');
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
This works across the full OAuth round-trip via localStorage (10-minute TTL, validated with `validateRedirectUrl()`).
|
|
300
|
+
|
|
301
|
+
For advanced use cases, the low-level helpers are also exported:
|
|
302
|
+
|
|
303
|
+
```tsx
|
|
304
|
+
import { saveAuthIntent, consumeAuthIntent, clearAuthIntent } from '@buildbase/sdk';
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Affiliate / Referral Tracking
|
|
308
|
+
|
|
309
|
+
Pass affiliate data to Stripe checkout sessions via `stripeOptions` on the checkout request:
|
|
310
|
+
|
|
311
|
+
```tsx
|
|
312
|
+
createCheckoutSession({
|
|
313
|
+
planVersionId: '...',
|
|
314
|
+
stripeOptions: {
|
|
315
|
+
// Rewardful, FirstPromoter, PartnerStack — read client_reference_id
|
|
316
|
+
clientReferenceId: Rewardful.referral,
|
|
317
|
+
|
|
318
|
+
// Endorsely — reads subscription metadata
|
|
319
|
+
subscriptionMetadata: { endorsely_referral: window.endorsely_referral },
|
|
320
|
+
|
|
321
|
+
// Custom tracking on the checkout session
|
|
322
|
+
metadata: { campaign: 'summer-sale' },
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
| Field | Stripe mapping | Use case |
|
|
328
|
+
| ---------------------- | ----------------------------- | -------------------------------------- |
|
|
329
|
+
| `clientReferenceId` | `client_reference_id` | Rewardful, FirstPromoter, etc. |
|
|
330
|
+
| `metadata` | `metadata` (checkout session) | Custom tracking, Endorsely |
|
|
331
|
+
| `subscriptionMetadata` | `subscription_data.metadata` | Data that persists on the subscription |
|
|
332
|
+
|
|
284
333
|
## 👥 Role-Based Access Control
|
|
285
334
|
|
|
286
335
|
### Role Components
|
|
@@ -374,7 +423,11 @@ Control UI visibility based on the current workspace’s subscription. Subscript
|
|
|
374
423
|
### Subscription Gate Components
|
|
375
424
|
|
|
376
425
|
```tsx
|
|
377
|
-
import {
|
|
426
|
+
import {
|
|
427
|
+
WhenSubscription,
|
|
428
|
+
WhenNoSubscription,
|
|
429
|
+
WhenSubscriptionToPlans,
|
|
430
|
+
} from '@buildbase/sdk/react';
|
|
378
431
|
|
|
379
432
|
function BillingExample() {
|
|
380
433
|
return (
|
|
@@ -489,10 +542,10 @@ function TrialExample() {
|
|
|
489
542
|
}
|
|
490
543
|
```
|
|
491
544
|
|
|
492
|
-
| Component | Renders when
|
|
493
|
-
| ----------------- |
|
|
494
|
-
| `WhenTrialing` | Subscription status is `trialing`
|
|
495
|
-
| `WhenNotTrialing` | Subscription status is NOT `trialing`
|
|
545
|
+
| Component | Renders when |
|
|
546
|
+
| ----------------- | --------------------------------------------------------------- |
|
|
547
|
+
| `WhenTrialing` | Subscription status is `trialing` |
|
|
548
|
+
| `WhenNotTrialing` | Subscription status is NOT `trialing` |
|
|
496
549
|
| `WhenTrialEnding` | Trialing AND trial ends within `daysThreshold` days (default 3) |
|
|
497
550
|
|
|
498
551
|
All trial gates support `loadingComponent` and `fallbackComponent` props.
|
|
@@ -518,13 +571,13 @@ function TrialInfo() {
|
|
|
518
571
|
}
|
|
519
572
|
```
|
|
520
573
|
|
|
521
|
-
| Property
|
|
522
|
-
|
|
|
523
|
-
| `isTrialing`
|
|
524
|
-
| `daysRemaining`
|
|
525
|
-
| `trialEndsAt`
|
|
526
|
-
| `trialStartedAt
|
|
527
|
-
| `isTrialEnding`
|
|
574
|
+
| Property | Type | Description |
|
|
575
|
+
| ---------------- | -------------- | ------------------------------------------------- |
|
|
576
|
+
| `isTrialing` | `boolean` | Whether subscription is in trial |
|
|
577
|
+
| `daysRemaining` | `number` | Days left in trial (0 if not trialing or expired) |
|
|
578
|
+
| `trialEndsAt` | `Date \| null` | Trial end date |
|
|
579
|
+
| `trialStartedAt` | `Date \| null` | Trial start date |
|
|
580
|
+
| `isTrialEnding` | `boolean` | True when 3 or fewer days remaining |
|
|
528
581
|
|
|
529
582
|
## 🔔 Push Notifications
|
|
530
583
|
|
|
@@ -533,7 +586,7 @@ Browser push notifications — built into the SDK. Users can enable/disable from
|
|
|
533
586
|
**Only setup required:** Create `public/push-sw.js` in your app:
|
|
534
587
|
|
|
535
588
|
```js
|
|
536
|
-
self.addEventListener('push', function(event) {
|
|
589
|
+
self.addEventListener('push', function (event) {
|
|
537
590
|
if (!event.data) return;
|
|
538
591
|
try {
|
|
539
592
|
var payload = event.data.json();
|
|
@@ -552,17 +605,23 @@ self.addEventListener('push', function(event) {
|
|
|
552
605
|
data: { url: payload.url, ...(payload.data || {}) },
|
|
553
606
|
};
|
|
554
607
|
event.waitUntil(self.registration.showNotification(payload.title || 'Notification', options));
|
|
555
|
-
} catch (e) {
|
|
608
|
+
} catch (e) {
|
|
609
|
+
console.error('[PushSW]', e);
|
|
610
|
+
}
|
|
556
611
|
});
|
|
557
612
|
|
|
558
|
-
self.addEventListener('notificationclick', function(event) {
|
|
613
|
+
self.addEventListener('notificationclick', function (event) {
|
|
559
614
|
event.notification.close();
|
|
560
615
|
var url = event.notification.data && event.notification.data.url;
|
|
561
616
|
if (url) {
|
|
562
|
-
event.waitUntil(
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
617
|
+
event.waitUntil(
|
|
618
|
+
clients.matchAll({ type: 'window', includeUncontrolled: true }).then(function (list) {
|
|
619
|
+
for (var i = 0; i < list.length; i++) {
|
|
620
|
+
if (list[i].url === url && 'focus' in list[i]) return list[i].focus();
|
|
621
|
+
}
|
|
622
|
+
if (clients.openWindow) return clients.openWindow(url);
|
|
623
|
+
})
|
|
624
|
+
);
|
|
566
625
|
}
|
|
567
626
|
});
|
|
568
627
|
```
|
|
@@ -580,16 +639,16 @@ Send email and push notifications to workspace members. The system has three lay
|
|
|
580
639
|
### Sending Notifications (Server-Side)
|
|
581
640
|
|
|
582
641
|
```ts
|
|
583
|
-
import { notification } from '@/lib/buildbase'
|
|
642
|
+
import { notification } from '@/lib/buildbase';
|
|
584
643
|
|
|
585
644
|
// Notify a specific user
|
|
586
645
|
await notification.send(workspaceId, 'comment_added', userId, {
|
|
587
|
-
title: 'New Comment',
|
|
646
|
+
title: 'New Comment', // Push title (falls back to event name)
|
|
588
647
|
message: 'Alice commented on your project', // Push body + email {{message}}
|
|
589
648
|
icon: 'https://example.com/comment-icon.png', // Custom push icon (falls back to org icon)
|
|
590
|
-
image: 'https://example.com/screenshot.jpg',
|
|
649
|
+
image: 'https://example.com/screenshot.jpg', // Large image in push notification body
|
|
591
650
|
url: 'https://app.example.com/projects/123#comments', // Opens on push click + {{url}} in email
|
|
592
|
-
})
|
|
651
|
+
});
|
|
593
652
|
|
|
594
653
|
// Notify all workspace members (omit userId)
|
|
595
654
|
await notification.send(workspaceId, 'new_release', undefined, {
|
|
@@ -597,7 +656,7 @@ await notification.send(workspaceId, 'new_release', undefined, {
|
|
|
597
656
|
message: 'Version 2.0 is now available with dark mode and API v2!',
|
|
598
657
|
image: 'https://example.com/release-banner.jpg',
|
|
599
658
|
url: 'https://app.example.com/changelog',
|
|
600
|
-
})
|
|
659
|
+
});
|
|
601
660
|
```
|
|
602
661
|
|
|
603
662
|
### Ad-hoc Notifications
|
|
@@ -612,7 +671,7 @@ await notification.send(workspaceId, 'deployment_success', userId, {
|
|
|
612
671
|
icon: 'https://example.com/deploy-icon.png',
|
|
613
672
|
url: '/deployments/latest',
|
|
614
673
|
channels: { push: true },
|
|
615
|
-
})
|
|
674
|
+
});
|
|
616
675
|
```
|
|
617
676
|
|
|
618
677
|
> **Note:** Ad-hoc events support push only. Email requires a registered event with a linked email template.
|
|
@@ -630,20 +689,20 @@ await notification.send(workspaceId, 'new_message', userId, {
|
|
|
630
689
|
{ action: 'reply', title: 'Reply', icon: 'https://example.com/reply.png' },
|
|
631
690
|
{ action: 'dismiss', title: 'Dismiss' },
|
|
632
691
|
],
|
|
633
|
-
tag: 'chat-alice',
|
|
634
|
-
renotify: true,
|
|
692
|
+
tag: 'chat-alice', // Replaces previous "chat-alice" notification instead of stacking
|
|
693
|
+
renotify: true, // Still vibrate/sound when replacing
|
|
635
694
|
channels: { push: true },
|
|
636
|
-
})
|
|
695
|
+
});
|
|
637
696
|
|
|
638
697
|
// Critical alert — stays visible until user interacts
|
|
639
698
|
await notification.send(workspaceId, 'payment_failed', userId, {
|
|
640
699
|
title: 'Payment Failed',
|
|
641
700
|
message: 'Your subscription will be suspended in 3 days',
|
|
642
701
|
badge: 'https://example.com/alert-badge.png',
|
|
643
|
-
requireInteraction: true,
|
|
644
|
-
urgency: 'high',
|
|
702
|
+
requireInteraction: true, // No auto-dismiss
|
|
703
|
+
urgency: 'high', // Prioritized delivery on mobile
|
|
645
704
|
channels: { push: true },
|
|
646
|
-
})
|
|
705
|
+
});
|
|
647
706
|
|
|
648
707
|
// Silent notification — no sound or vibration
|
|
649
708
|
await notification.send(workspaceId, 'sync_complete', userId, {
|
|
@@ -652,19 +711,19 @@ await notification.send(workspaceId, 'sync_complete', userId, {
|
|
|
652
711
|
silent: true,
|
|
653
712
|
urgency: 'low',
|
|
654
713
|
channels: { push: true },
|
|
655
|
-
})
|
|
714
|
+
});
|
|
656
715
|
```
|
|
657
716
|
|
|
658
|
-
| Option
|
|
659
|
-
|
|
660
|
-
| `badge`
|
|
661
|
-
| `tag`
|
|
662
|
-
| `actions`
|
|
663
|
-
| `silent`
|
|
664
|
-
| `requireInteraction` | `boolean`
|
|
665
|
-
| `renotify`
|
|
666
|
-
| `timestamp`
|
|
667
|
-
| `dir`
|
|
717
|
+
| Option | Type | Description |
|
|
718
|
+
| -------------------- | ------------------------------- | ---------------------------------------------------------------- |
|
|
719
|
+
| `badge` | `string` | Small monochrome icon for status bar (Android/ChromeOS) |
|
|
720
|
+
| `tag` | `string` | Replaces existing notification with same tag instead of stacking |
|
|
721
|
+
| `actions` | `Array<{action, title, icon?}>` | Up to 2 action buttons on the notification |
|
|
722
|
+
| `silent` | `boolean` | No sound or vibration |
|
|
723
|
+
| `requireInteraction` | `boolean` | Stays visible until user interacts |
|
|
724
|
+
| `renotify` | `boolean` | Sound/vibrate again when replacing via `tag` |
|
|
725
|
+
| `timestamp` | `number` | Custom timestamp (ms since epoch) shown on notification |
|
|
726
|
+
| `dir` | `'ltr' \| 'rtl' \| 'auto'` | Text direction for title/body |
|
|
668
727
|
|
|
669
728
|
### Delivery Options
|
|
670
729
|
|
|
@@ -675,26 +734,26 @@ Control how and when the push service delivers the notification:
|
|
|
675
734
|
await notification.send(workspaceId, 'daily_digest', undefined, {
|
|
676
735
|
title: 'Your Daily Digest',
|
|
677
736
|
message: '12 new updates in your workspace',
|
|
678
|
-
scheduledAt: '2026-04-16T09:00:00Z',
|
|
737
|
+
scheduledAt: '2026-04-16T09:00:00Z', // Deliver at 9am UTC tomorrow
|
|
679
738
|
channels: { push: true },
|
|
680
|
-
})
|
|
739
|
+
});
|
|
681
740
|
|
|
682
741
|
// Short-lived notification — discard if not delivered in 1 hour
|
|
683
742
|
await notification.send(workspaceId, 'flash_sale', undefined, {
|
|
684
743
|
title: 'Flash Sale — 50% off!',
|
|
685
744
|
message: 'Ends in 1 hour',
|
|
686
745
|
image: 'https://example.com/sale-banner.jpg',
|
|
687
|
-
ttl: 3600,
|
|
688
|
-
urgency: 'high',
|
|
746
|
+
ttl: 3600, // Expires after 1 hour (seconds)
|
|
747
|
+
urgency: 'high', // Deliver ASAP
|
|
689
748
|
channels: { push: true },
|
|
690
|
-
})
|
|
749
|
+
});
|
|
691
750
|
```
|
|
692
751
|
|
|
693
|
-
| Option
|
|
694
|
-
|
|
695
|
-
| `scheduledAt` | `string`
|
|
696
|
-
| `ttl`
|
|
697
|
-
| `urgency`
|
|
752
|
+
| Option | Type | Description |
|
|
753
|
+
| ------------- | ------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
754
|
+
| `scheduledAt` | `string` | ISO 8601 date. Delays delivery until the specified time |
|
|
755
|
+
| `ttl` | `number` | Time-to-live in seconds. Push service discards if not delivered in time. Default: 86400 (24h) |
|
|
756
|
+
| `urgency` | `'very-low' \| 'low' \| 'normal' \| 'high'` | Delivery priority hint. Affects battery usage on mobile |
|
|
698
757
|
|
|
699
758
|
### Channel Control
|
|
700
759
|
|
|
@@ -705,19 +764,19 @@ By default, both email and push are sent (based on event config). Override per-s
|
|
|
705
764
|
await notification.send(workspaceId, 'typing_indicator', userId, {
|
|
706
765
|
message: 'Alice is typing...',
|
|
707
766
|
channels: { push: true },
|
|
708
|
-
})
|
|
767
|
+
});
|
|
709
768
|
|
|
710
769
|
// Only email — digest or report, no push
|
|
711
770
|
await notification.send(workspaceId, 'weekly_report', undefined, {
|
|
712
771
|
message: 'Your weekly activity report is ready',
|
|
713
772
|
channels: { email: true },
|
|
714
|
-
})
|
|
773
|
+
});
|
|
715
774
|
|
|
716
775
|
// Both channels explicitly
|
|
717
776
|
await notification.send(workspaceId, 'comment_added', userId, {
|
|
718
777
|
message: 'New comment on your project',
|
|
719
778
|
channels: { email: true, push: true },
|
|
720
|
-
})
|
|
779
|
+
});
|
|
721
780
|
```
|
|
722
781
|
|
|
723
782
|
> **Note:** Even with `channels` override, the 4-layer gate still applies. If the admin disabled push globally, `channels: { push: true }` won't send push.
|
|
@@ -728,21 +787,21 @@ Both email and push support merge tags with `{{tag}}` syntax:
|
|
|
728
787
|
|
|
729
788
|
```ts
|
|
730
789
|
await notification.send(workspaceId, 'export_ready', userId, {
|
|
731
|
-
title: '{{workspaceName}} — Export Ready',
|
|
790
|
+
title: '{{workspaceName}} — Export Ready', // → "Acme Corp — Export Ready"
|
|
732
791
|
message: 'Hi {{name}}, your export is ready', // → "Hi Alice, your export is ready"
|
|
733
792
|
downloadUrl: 'https://example.com/exports/123',
|
|
734
793
|
fileName: 'report.csv',
|
|
735
|
-
})
|
|
794
|
+
});
|
|
736
795
|
```
|
|
737
796
|
|
|
738
|
-
| Tag
|
|
739
|
-
|
|
740
|
-
| `{{name}}`
|
|
741
|
-
| `{{email}}`
|
|
742
|
-
| `{{workspaceName}}` | Workspace name
|
|
743
|
-
| `{{message}}`
|
|
744
|
-
| `{{url}}`
|
|
745
|
-
| `{{anyKey}}`
|
|
797
|
+
| Tag | Resolves to | Available in |
|
|
798
|
+
| ------------------- | ------------------------ | ---------------------------------- |
|
|
799
|
+
| `{{name}}` | Recipient's name | Email + Push |
|
|
800
|
+
| `{{email}}` | Recipient's email | Email + Push |
|
|
801
|
+
| `{{workspaceName}}` | Workspace name | Email + Push |
|
|
802
|
+
| `{{message}}` | The `message` field | Email template |
|
|
803
|
+
| `{{url}}` | The `url` field | Email template + Push click target |
|
|
804
|
+
| `{{anyKey}}` | Value from `data` object | Email + Push |
|
|
746
805
|
|
|
747
806
|
### Response
|
|
748
807
|
|
|
@@ -780,7 +839,7 @@ An email template is auto-created for each custom event. Edit it in **Email Temp
|
|
|
780
839
|
The workspace settings panel shows notification preferences automatically — **only for events where the developer enabled "User Control"**. System notifications are never shown to end users.
|
|
781
840
|
|
|
782
841
|
```tsx
|
|
783
|
-
import { SaaSOSProvider } from '@buildbase/sdk/react'
|
|
842
|
+
import { SaaSOSProvider } from '@buildbase/sdk/react';
|
|
784
843
|
|
|
785
844
|
// The Notifications tab in workspace settings shows:
|
|
786
845
|
// - Browser push toggle (subscribe/unsubscribe)
|
|
@@ -790,34 +849,34 @@ import { SaaSOSProvider } from '@buildbase/sdk/react'
|
|
|
790
849
|
### Types
|
|
791
850
|
|
|
792
851
|
```ts
|
|
793
|
-
import type { NotificationData, NotificationResult, NotificationEvent } from '@buildbase/sdk'
|
|
852
|
+
import type { NotificationData, NotificationResult, NotificationEvent } from '@buildbase/sdk';
|
|
794
853
|
|
|
795
854
|
interface NotificationData {
|
|
796
|
-
title?: string;
|
|
797
|
-
message?: string;
|
|
798
|
-
icon?: string;
|
|
799
|
-
image?: string;
|
|
800
|
-
badge?: string;
|
|
801
|
-
url?: string;
|
|
802
|
-
tag?: string;
|
|
803
|
-
actions?: Array<{action
|
|
804
|
-
silent?: boolean;
|
|
805
|
-
requireInteraction?: boolean;
|
|
806
|
-
renotify?: boolean;
|
|
807
|
-
timestamp?: number;
|
|
808
|
-
dir?: 'ltr' | 'rtl' | 'auto';
|
|
809
|
-
ttl?: number;
|
|
810
|
-
urgency?: 'very-low'|'low'|'normal'|'high'; // Delivery priority
|
|
811
|
-
scheduledAt?: string;
|
|
855
|
+
title?: string; // Push title (falls back to event name)
|
|
856
|
+
message?: string; // Push body + email {{message}}
|
|
857
|
+
icon?: string; // Custom push icon URL (falls back to org icon)
|
|
858
|
+
image?: string; // Large image in push notification body
|
|
859
|
+
badge?: string; // Small monochrome status bar icon
|
|
860
|
+
url?: string; // Opens on push click + {{url}} in email
|
|
861
|
+
tag?: string; // Replace instead of stack notifications
|
|
862
|
+
actions?: Array<{ action; title; icon? }>; // Action buttons (max 2)
|
|
863
|
+
silent?: boolean; // No sound/vibration
|
|
864
|
+
requireInteraction?: boolean; // No auto-dismiss
|
|
865
|
+
renotify?: boolean; // Re-alert on tag replace
|
|
866
|
+
timestamp?: number; // Custom timestamp (ms)
|
|
867
|
+
dir?: 'ltr' | 'rtl' | 'auto'; // Text direction
|
|
868
|
+
ttl?: number; // Time-to-live (seconds)
|
|
869
|
+
urgency?: 'very-low' | 'low' | 'normal' | 'high'; // Delivery priority
|
|
870
|
+
scheduledAt?: string; // ISO 8601 delayed delivery
|
|
812
871
|
channels?: { email?: boolean; push?: boolean }; // Override which channels to use
|
|
813
|
-
[key: string]: any;
|
|
872
|
+
[key: string]: any; // Custom merge tags for email + push
|
|
814
873
|
}
|
|
815
874
|
|
|
816
875
|
interface NotificationResult {
|
|
817
876
|
sent: boolean;
|
|
818
877
|
channels: { email: boolean; push: boolean };
|
|
819
878
|
notifiedCount?: number;
|
|
820
|
-
reason?: string;
|
|
879
|
+
reason?: string; // Only when sent=false
|
|
821
880
|
}
|
|
822
881
|
|
|
823
882
|
interface NotificationEvent {
|
|
@@ -836,23 +895,21 @@ The SDK supports 8 locales with ICU MessageFormat for plurals, selects, and numb
|
|
|
836
895
|
### Setup
|
|
837
896
|
|
|
838
897
|
```tsx
|
|
839
|
-
<SaaSOSProvider locale="hi">
|
|
840
|
-
{/* All SDK UI renders in Hindi */}
|
|
841
|
-
</SaaSOSProvider>
|
|
898
|
+
<SaaSOSProvider locale="hi">{/* All SDK UI renders in Hindi */}</SaaSOSProvider>
|
|
842
899
|
```
|
|
843
900
|
|
|
844
901
|
### Supported Locales
|
|
845
902
|
|
|
846
|
-
| Code | Language | Numerals
|
|
847
|
-
|
|
848
|
-
| `en` | English
|
|
849
|
-
| `es` | Spanish
|
|
850
|
-
| `fr` | French
|
|
851
|
-
| `de` | German
|
|
852
|
-
| `ja` | Japanese | 1,234.56
|
|
853
|
-
| `zh` | Chinese
|
|
854
|
-
| `hi` | Hindi
|
|
855
|
-
| `ar` | Arabic
|
|
903
|
+
| Code | Language | Numerals | Direction |
|
|
904
|
+
| ---- | -------- | ------------------------- | --------- |
|
|
905
|
+
| `en` | English | 1,234.56 | LTR |
|
|
906
|
+
| `es` | Spanish | 1.234,56 | LTR |
|
|
907
|
+
| `fr` | French | 1 234,56 | LTR |
|
|
908
|
+
| `de` | German | 1.234,56 | LTR |
|
|
909
|
+
| `ja` | Japanese | 1,234.56 | LTR |
|
|
910
|
+
| `zh` | Chinese | 1,234.56 | LTR |
|
|
911
|
+
| `hi` | Hindi | Devanagari (e.g. 1,234) | LTR |
|
|
912
|
+
| `ar` | Arabic | Arabic-Indic (e.g. 1,234) | RTL |
|
|
856
913
|
|
|
857
914
|
### useTranslation Hook
|
|
858
915
|
|
|
@@ -864,10 +921,10 @@ function MyComponent() {
|
|
|
864
921
|
|
|
865
922
|
return (
|
|
866
923
|
<div dir={dir}>
|
|
867
|
-
<p>{t('subscription.currentPlan')}</p>
|
|
868
|
-
<p>{t('users.memberCount', { count: 5 })}</p>
|
|
869
|
-
<p>{fmtNum(1234)}</p>
|
|
870
|
-
<p>{fmtCents(1999, 'usd')}</p>
|
|
924
|
+
<p>{t('subscription.currentPlan')}</p> {/* Type-safe key lookup */}
|
|
925
|
+
<p>{t('users.memberCount', { count: 5 })}</p> {/* ICU plural: "5 members" */}
|
|
926
|
+
<p>{fmtNum(1234)}</p> {/* Locale-aware: "1,234" or "1,234" */}
|
|
927
|
+
<p>{fmtCents(1999, 'usd')}</p> {/* "$19.99" or "19.99 US$" */}
|
|
871
928
|
</div>
|
|
872
929
|
);
|
|
873
930
|
}
|
|
@@ -909,13 +966,13 @@ For full SaaS platforms (Slack, GitHub, Discord):
|
|
|
909
966
|
|
|
910
967
|
Platform mode supports granular overrides from the admin dashboard:
|
|
911
968
|
|
|
912
|
-
| Setting
|
|
913
|
-
|
|
914
|
-
| Can Create Workspace
|
|
915
|
-
| Can Invite Members
|
|
916
|
-
| Show Workspace Switcher
|
|
917
|
-
| Max Workspaces Per User
|
|
918
|
-
| Auto-Create First Workspace | On / Off
|
|
969
|
+
| Setting | Options | Default |
|
|
970
|
+
| --------------------------- | -------------------------------- | -------- |
|
|
971
|
+
| Can Create Workspace | Everyone / Owner Only / Disabled | Everyone |
|
|
972
|
+
| Can Invite Members | Everyone / Admin Only / Disabled | Everyone |
|
|
973
|
+
| Show Workspace Switcher | On / Off | On |
|
|
974
|
+
| Max Workspaces Per User | 0 (unlimited) or a number | 0 |
|
|
975
|
+
| Auto-Create First Workspace | On / Off | On |
|
|
919
976
|
|
|
920
977
|
These let you achieve team-like, managed, or enterprise-like behavior without a separate mode.
|
|
921
978
|
|
|
@@ -984,6 +1041,8 @@ function WorkspaceManager() {
|
|
|
984
1041
|
updateFeature, // Toggle workspace feature
|
|
985
1042
|
getProfile, // Get current user profile
|
|
986
1043
|
updateUserProfile, // Update user profile
|
|
1044
|
+
updateWorkspaceSettings, // Update workspace settings
|
|
1045
|
+
updateWorkspacePermissions, // Update workspace permissions
|
|
987
1046
|
} = useSaaSWorkspaces();
|
|
988
1047
|
|
|
989
1048
|
// Example: Create a workspace
|
|
@@ -1036,15 +1095,22 @@ import { PricingPage } from '@buildbase/sdk/react';
|
|
|
1036
1095
|
|
|
1037
1096
|
function PublicPricingPage() {
|
|
1038
1097
|
return (
|
|
1039
|
-
<PricingPage slug="main-pricing">
|
|
1040
|
-
{({ loading, error, items, plans, refetch }) => {
|
|
1098
|
+
<PricingPage slug="main-pricing" redirectBaseUrl="https://app.com/dashboard">
|
|
1099
|
+
{({ loading, error, items, plans, selectPlan, refetch }) => {
|
|
1041
1100
|
if (loading) return <Loading />;
|
|
1042
1101
|
if (error) return <Error message={error} />;
|
|
1043
1102
|
|
|
1044
1103
|
return (
|
|
1045
1104
|
<div>
|
|
1046
1105
|
{plans.map(plan => (
|
|
1047
|
-
<
|
|
1106
|
+
<div key={plan._id}>
|
|
1107
|
+
<PlanCard plan={plan} items={items} />
|
|
1108
|
+
<button onClick={() => selectPlan(plan._id, 'monthly', 'usd')}>
|
|
1109
|
+
{plan.trial?.enabled
|
|
1110
|
+
? `Start ${plan.trial.durationDays}-Day Trial`
|
|
1111
|
+
: 'Select Plan'}
|
|
1112
|
+
</button>
|
|
1113
|
+
</div>
|
|
1048
1114
|
))}
|
|
1049
1115
|
</div>
|
|
1050
1116
|
);
|
|
@@ -1054,12 +1120,22 @@ function PublicPricingPage() {
|
|
|
1054
1120
|
}
|
|
1055
1121
|
```
|
|
1056
1122
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
|
1123
|
+
`selectPlan()` handles everything automatically:
|
|
1124
|
+
|
|
1125
|
+
- **Authenticated** → opens the "Choose Your Plan" dialog
|
|
1126
|
+
- **Not authenticated** → saves a redirect URL, triggers sign-in, and after login the user lands on the dashboard with the plan picker dialog open
|
|
1127
|
+
|
|
1128
|
+
| Prop | Type | Description |
|
|
1129
|
+
| ----------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
|
|
1130
|
+
| `slug` | `string` | Plan group slug (e.g. 'main-pricing', 'enterprise') |
|
|
1131
|
+
| `children` | `(details) => ReactNode` | Render prop receiving plan details (see below) |
|
|
1132
|
+
| `redirectBaseUrl` | `string` | Base URL for post-login redirects (e.g. `"https://app.com/dashboard"`). Enables `selectPlan()` for unauthenticated users. |
|
|
1133
|
+
| `loadingFallback` | `ReactNode` | Custom loading UI (defaults to skeleton) |
|
|
1134
|
+
| `errorFallback` | `(error: string) => ReactNode` | Custom error UI |
|
|
1135
|
+
|
|
1136
|
+
**Render prop details**: `{ loading, error, items, plans, notes, refetch, selectPlan }`
|
|
1137
|
+
|
|
1138
|
+
- `selectPlan(planVersionId, interval, currency)` — One-call plan selection (handles auth + dialog automatically)
|
|
1063
1139
|
|
|
1064
1140
|
**Response shape**: `items` = subscription item definitions (features, limits, quotas with category); `plans` = plan versions with `pricing`, `quotas`, `features`, `limits`.
|
|
1065
1141
|
|
|
@@ -1071,41 +1147,41 @@ Plans support **pricing variants** (multi-currency). Use these utilities for dis
|
|
|
1071
1147
|
|
|
1072
1148
|
### Currency utilities
|
|
1073
1149
|
|
|
1074
|
-
| Export
|
|
1075
|
-
|
|
1076
|
-
| `CURRENCY_DISPLAY`
|
|
1077
|
-
| `CURRENCY_FLAG`
|
|
1078
|
-
| `PLAN_CURRENCY_CODES`
|
|
1079
|
-
| `PLAN_CURRENCY_OPTIONS`
|
|
1080
|
-
| `getCurrencySymbol(currency)`
|
|
1081
|
-
| `getCurrencyFlag(currency)`
|
|
1082
|
-
| `formatCents(cents, currency)`
|
|
1083
|
-
| `formatOverageRate(cents, currency)` | Format overage rate for display
|
|
1084
|
-
| `formatOverageRateWithLabel(...)`
|
|
1085
|
-
| `formatQuotaIncludedOverage(...)`
|
|
1086
|
-
| `getQuotaUnitLabelFromName(name)`
|
|
1150
|
+
| Export | Purpose |
|
|
1151
|
+
| ------------------------------------ | --------------------------------------------------------- |
|
|
1152
|
+
| `CURRENCY_DISPLAY` | Map of currency code → symbol (e.g. `usd` → `$`) |
|
|
1153
|
+
| `CURRENCY_FLAG` | Map of currency code → flag emoji |
|
|
1154
|
+
| `PLAN_CURRENCY_CODES` | Allowed billing currency codes (for dropdowns/validation) |
|
|
1155
|
+
| `PLAN_CURRENCY_OPTIONS` | Options array for plan currency selects |
|
|
1156
|
+
| `getCurrencySymbol(currency)` | Symbol for a Stripe currency code |
|
|
1157
|
+
| `getCurrencyFlag(currency)` | Flag emoji for a currency code |
|
|
1158
|
+
| `formatCents(cents, currency)` | Format cents as localized price string |
|
|
1159
|
+
| `formatOverageRate(cents, currency)` | Format overage rate for display |
|
|
1160
|
+
| `formatOverageRateWithLabel(...)` | Overage rate with optional unit label |
|
|
1161
|
+
| `formatQuotaIncludedOverage(...)` | "X included, then $Y / unit" style text |
|
|
1162
|
+
| `getQuotaUnitLabelFromName(name)` | Human-readable unit label from quota name |
|
|
1087
1163
|
|
|
1088
1164
|
### Pricing variant utilities
|
|
1089
1165
|
|
|
1090
|
-
| Export
|
|
1091
|
-
|
|
1092
|
-
| `getPricingVariant(planVersion, currency)`
|
|
1093
|
-
| `getBasePriceCents(planVersion, currency, interval)`
|
|
1094
|
-
| `getStripePriceIdForInterval(planVersion, currency, interval)`
|
|
1095
|
-
| `getQuotaOverageCents(planVersion, currency, quotaSlug, interval)`
|
|
1096
|
-
| `getQuotaDisplayWithVariant(planVersion, currency, quotaSlug, interval)` | Display value with overage for a variant
|
|
1097
|
-
| `getAvailableCurrenciesFromPlans(plans)`
|
|
1098
|
-
| `getDisplayCurrency(planVersion, currency)`
|
|
1099
|
-
| `getBillingIntervalAndCurrencyFromPriceId(planVersions, priceId)`
|
|
1166
|
+
| Export | Purpose |
|
|
1167
|
+
| ------------------------------------------------------------------------ | ------------------------------------------------------------ |
|
|
1168
|
+
| `getPricingVariant(planVersion, currency)` | Get variant for a currency, or `null` |
|
|
1169
|
+
| `getBasePriceCents(planVersion, currency, interval)` | Base price in cents for currency/interval |
|
|
1170
|
+
| `getStripePriceIdForInterval(planVersion, currency, interval)` | Stripe price ID for checkout |
|
|
1171
|
+
| `getQuotaOverageCents(planVersion, currency, quotaSlug, interval)` | Overage cents for a quota |
|
|
1172
|
+
| `getQuotaDisplayWithVariant(planVersion, currency, quotaSlug, interval)` | Display value with overage for a variant |
|
|
1173
|
+
| `getAvailableCurrenciesFromPlans(plans)` | Unique currency codes across plan versions |
|
|
1174
|
+
| `getDisplayCurrency(planVersion, currency)` | Display currency (variant exists ? currency : plan.currency) |
|
|
1175
|
+
| `getBillingIntervalAndCurrencyFromPriceId(planVersions, priceId)` | Resolve price ID to interval + currency |
|
|
1100
1176
|
|
|
1101
1177
|
Types: `IPricingVariant`, `PlanVersionWithPricingVariants`, `QuotaDisplayWithOverage`.
|
|
1102
1178
|
|
|
1103
1179
|
### Quota utilities
|
|
1104
1180
|
|
|
1105
|
-
| Export
|
|
1106
|
-
|
|
1181
|
+
| Export | Purpose |
|
|
1182
|
+
| -------------------------------------------------- | ------------------------------------------------------------------- |
|
|
1107
1183
|
| `getQuotaDisplayValue(quotaByInterval, interval?)` | Normalize `IQuotaByInterval` to `{ included, overage?, unitSize? }` |
|
|
1108
|
-
| `formatQuotaWithPrice(value, unitName, options?)`
|
|
1184
|
+
| `formatQuotaWithPrice(value, unitName, options?)` | Format as "X included, then $Y.YY / unit" |
|
|
1109
1185
|
|
|
1110
1186
|
Types: `QuotaDisplayValue`, `FormatQuotaWithPriceOptions`. Plan/subscription types use `IQuotaByInterval` and `IQuotaIntervalValue` for per-interval quotas and overages.
|
|
1111
1187
|
|
|
@@ -1137,12 +1213,12 @@ Track and monitor metered usage for subscription quotas (e.g., API calls, emails
|
|
|
1137
1213
|
|
|
1138
1214
|
### When to use which?
|
|
1139
1215
|
|
|
1140
|
-
| Scenario
|
|
1141
|
-
|
|
1216
|
+
| Scenario | Where to record | Why |
|
|
1217
|
+
| ------------------------------- | ---------------------- | -------------------------------------------- |
|
|
1142
1218
|
| User clicks "Send Email" button | Client-side (SDK hook) | User-initiated, immediate UI feedback needed |
|
|
1143
|
-
| API request hits your backend
|
|
1144
|
-
| Background job processes data
|
|
1145
|
-
| File upload completes
|
|
1219
|
+
| API request hits your backend | Server-side (REST API) | Backend controls the resource, more secure |
|
|
1220
|
+
| Background job processes data | Server-side (REST API) | No browser context available |
|
|
1221
|
+
| File upload completes | Either | Depends on where validation happens |
|
|
1146
1222
|
|
|
1147
1223
|
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.
|
|
1148
1224
|
|
|
@@ -1166,8 +1242,8 @@ function SendEmailButton() {
|
|
|
1166
1242
|
const result = await recordUsage({
|
|
1167
1243
|
quotaSlug: 'emails',
|
|
1168
1244
|
quantity: 1,
|
|
1169
|
-
source: 'web-app',
|
|
1170
|
-
idempotencyKey: 'email-abc',
|
|
1245
|
+
source: 'web-app', // optional: track where usage came from
|
|
1246
|
+
idempotencyKey: 'email-abc', // optional: prevent duplicate recordings
|
|
1171
1247
|
});
|
|
1172
1248
|
console.log(`Used: ${result.consumed}/${result.included}, Available: ${result.available}`);
|
|
1173
1249
|
if (result.overage > 0) {
|
|
@@ -1178,7 +1254,11 @@ function SendEmailButton() {
|
|
|
1178
1254
|
}
|
|
1179
1255
|
};
|
|
1180
1256
|
|
|
1181
|
-
return
|
|
1257
|
+
return (
|
|
1258
|
+
<button onClick={handleSend} disabled={loading}>
|
|
1259
|
+
Send Email
|
|
1260
|
+
</button>
|
|
1261
|
+
);
|
|
1182
1262
|
}
|
|
1183
1263
|
```
|
|
1184
1264
|
|
|
@@ -1198,7 +1278,9 @@ function QuotaStatusBar({ quotaSlug }: { quotaSlug: string }) {
|
|
|
1198
1278
|
|
|
1199
1279
|
return (
|
|
1200
1280
|
<div>
|
|
1201
|
-
<p>
|
|
1281
|
+
<p>
|
|
1282
|
+
{quotaSlug}: {status.consumed} / {status.included} ({usagePercent}%)
|
|
1283
|
+
</p>
|
|
1202
1284
|
<p>Available: {status.available}</p>
|
|
1203
1285
|
{status.hasOverage && <p>Overage: {status.overage} units</p>}
|
|
1204
1286
|
<button onClick={refetch}>Refresh</button>
|
|
@@ -1239,12 +1321,10 @@ import { useUsageLogs, useSaaSWorkspaces } from '@buildbase/sdk/react';
|
|
|
1239
1321
|
|
|
1240
1322
|
function UsageLogsTable() {
|
|
1241
1323
|
const { currentWorkspace } = useSaaSWorkspaces();
|
|
1242
|
-
const {
|
|
1243
|
-
logs, totalDocs, totalPages, page, hasNextPage, loading, refetch,
|
|
1244
|
-
} = useUsageLogs(
|
|
1324
|
+
const { logs, totalDocs, totalPages, page, hasNextPage, loading, refetch } = useUsageLogs(
|
|
1245
1325
|
currentWorkspace?._id,
|
|
1246
|
-
'api_calls',
|
|
1247
|
-
{ limit: 20, page: 1 }
|
|
1326
|
+
'api_calls', // optional: filter by quota slug
|
|
1327
|
+
{ limit: 20, page: 1 } // optional: pagination and filters
|
|
1248
1328
|
);
|
|
1249
1329
|
|
|
1250
1330
|
if (loading) return <Spinner />;
|
|
@@ -1253,7 +1333,12 @@ function UsageLogsTable() {
|
|
|
1253
1333
|
<div>
|
|
1254
1334
|
<table>
|
|
1255
1335
|
<thead>
|
|
1256
|
-
<tr
|
|
1336
|
+
<tr>
|
|
1337
|
+
<th>Quota</th>
|
|
1338
|
+
<th>Quantity</th>
|
|
1339
|
+
<th>Source</th>
|
|
1340
|
+
<th>Date</th>
|
|
1341
|
+
</tr>
|
|
1257
1342
|
</thead>
|
|
1258
1343
|
<tbody>
|
|
1259
1344
|
{logs.map(log => (
|
|
@@ -1266,7 +1351,9 @@ function UsageLogsTable() {
|
|
|
1266
1351
|
))}
|
|
1267
1352
|
</tbody>
|
|
1268
1353
|
</table>
|
|
1269
|
-
<p>
|
|
1354
|
+
<p>
|
|
1355
|
+
Page {page} of {totalPages} ({totalDocs} total)
|
|
1356
|
+
</p>
|
|
1270
1357
|
</div>
|
|
1271
1358
|
);
|
|
1272
1359
|
}
|
|
@@ -1274,23 +1361,23 @@ function UsageLogsTable() {
|
|
|
1274
1361
|
|
|
1275
1362
|
**`useUsageLogs` parameters:**
|
|
1276
1363
|
|
|
1277
|
-
| Parameter
|
|
1278
|
-
|
|
1279
|
-
| `workspaceId`
|
|
1280
|
-
| `quotaSlug`
|
|
1281
|
-
| `options.from`
|
|
1282
|
-
| `options.to`
|
|
1283
|
-
| `options.source` | `string`
|
|
1284
|
-
| `options.page`
|
|
1285
|
-
| `options.limit`
|
|
1364
|
+
| Parameter | Type | Required | Description |
|
|
1365
|
+
| ---------------- | ----------------------------- | -------- | ----------------------------------------------- |
|
|
1366
|
+
| `workspaceId` | `string \| null \| undefined` | Yes | Workspace ID (null/undefined disables fetching) |
|
|
1367
|
+
| `quotaSlug` | `string` | No | Filter logs by quota slug |
|
|
1368
|
+
| `options.from` | `string` | No | ISO date string — filter logs from this date |
|
|
1369
|
+
| `options.to` | `string` | No | ISO date string — filter logs until this date |
|
|
1370
|
+
| `options.source` | `string` | No | Filter logs by source |
|
|
1371
|
+
| `options.page` | `number` | No | Page number (default: 1) |
|
|
1372
|
+
| `options.limit` | `number` | No | Results per page (default: 20) |
|
|
1286
1373
|
|
|
1287
1374
|
#### Client-Side Hooks Summary
|
|
1288
1375
|
|
|
1289
|
-
| Hook
|
|
1290
|
-
|
|
1291
|
-
| `useRecordUsage(workspaceId)`
|
|
1292
|
-
| `useQuotaUsageStatus(workspaceId, quotaSlug)`
|
|
1293
|
-
| `useAllQuotaUsage(workspaceId)`
|
|
1376
|
+
| Hook | Purpose |
|
|
1377
|
+
| ------------------------------------------------- | ------------------------------------------ |
|
|
1378
|
+
| `useRecordUsage(workspaceId)` | Record quota consumption (mutation) |
|
|
1379
|
+
| `useQuotaUsageStatus(workspaceId, quotaSlug)` | Get single quota status (auto-fetches) |
|
|
1380
|
+
| `useAllQuotaUsage(workspaceId)` | Get all quotas status (auto-fetches) |
|
|
1294
1381
|
| `useUsageLogs(workspaceId, quotaSlug?, options?)` | Get paginated usage history (auto-fetches) |
|
|
1295
1382
|
|
|
1296
1383
|
---
|
|
@@ -1337,8 +1424,8 @@ async function recordUsage(workspaceId: string, quotaSlug: string, quantity: num
|
|
|
1337
1424
|
body: JSON.stringify({
|
|
1338
1425
|
quotaSlug,
|
|
1339
1426
|
quantity,
|
|
1340
|
-
source: 'backend',
|
|
1341
|
-
metadata: {},
|
|
1427
|
+
source: 'backend', // optional: helps distinguish from client-side usage
|
|
1428
|
+
metadata: {}, // optional: attach custom data
|
|
1342
1429
|
idempotencyKey: undefined, // optional: prevent duplicate recordings
|
|
1343
1430
|
}),
|
|
1344
1431
|
});
|
|
@@ -1376,10 +1463,9 @@ async function getQuotaStatus(workspaceId: string, quotaSlug: string) {
|
|
|
1376
1463
|
|
|
1377
1464
|
// Get status for ALL quotas
|
|
1378
1465
|
async function getAllQuotaStatus(workspaceId: string) {
|
|
1379
|
-
const response = await fetch(
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
);
|
|
1466
|
+
const response = await fetch(`${BASE_URL}/workspaces/${workspaceId}/subscription/usage/all`, {
|
|
1467
|
+
headers: { 'x-session-id': SESSION_ID },
|
|
1468
|
+
});
|
|
1383
1469
|
return response.json();
|
|
1384
1470
|
// Returns: { quotas: { [slug]: { consumed, included, available, overage, hasOverage } } }
|
|
1385
1471
|
}
|
|
@@ -1416,26 +1502,23 @@ await bb.usage.recordBatch(workspaceId, {
|
|
|
1416
1502
|
{ quotaSlug: 'images', quantity: 500, source: 'batch-export-job' },
|
|
1417
1503
|
{ quotaSlug: 'videos', quantity: 10, source: 'batch-export-job' },
|
|
1418
1504
|
{ quotaSlug: 'images', quantity: 200, metadata: { jobId: 'abc123' } },
|
|
1419
|
-
]
|
|
1505
|
+
],
|
|
1420
1506
|
});
|
|
1421
1507
|
// Returns: { success, total: 3, succeeded: 3, failed: 0, results: [...] }
|
|
1422
1508
|
```
|
|
1423
1509
|
|
|
1424
1510
|
```ts
|
|
1425
1511
|
// Using REST API directly
|
|
1426
|
-
const response = await fetch(
|
|
1427
|
-
|
|
1428
|
-
{
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
}),
|
|
1437
|
-
}
|
|
1438
|
-
);
|
|
1512
|
+
const response = await fetch(`${BASE_URL}/workspaces/${workspaceId}/subscription/usage/batch`, {
|
|
1513
|
+
method: 'POST',
|
|
1514
|
+
headers: { 'x-session-id': SESSION_ID, 'Content-Type': 'application/json' },
|
|
1515
|
+
body: JSON.stringify({
|
|
1516
|
+
items: [
|
|
1517
|
+
{ quotaSlug: 'images', quantity: 500, source: 'batch-export' },
|
|
1518
|
+
{ quotaSlug: 'videos', quantity: 10 },
|
|
1519
|
+
],
|
|
1520
|
+
}),
|
|
1521
|
+
});
|
|
1439
1522
|
```
|
|
1440
1523
|
|
|
1441
1524
|
- Max **100 items** per request
|
|
@@ -1445,35 +1528,35 @@ const response = await fetch(
|
|
|
1445
1528
|
|
|
1446
1529
|
#### Server-Side API Reference
|
|
1447
1530
|
|
|
1448
|
-
| Endpoint
|
|
1449
|
-
|
|
1450
|
-
| `/api/v1/public/token/exchange`
|
|
1451
|
-
| `/api/v1/public/workspaces/:id/subscription/usage`
|
|
1452
|
-
| `/api/v1/public/workspaces/:id/subscription/usage/status?quotaSlug=X` | GET
|
|
1453
|
-
| `/api/v1/public/workspaces/:id/subscription/usage/all`
|
|
1454
|
-
| `/api/v1/public/workspaces/:id/subscription/usage/logs`
|
|
1531
|
+
| Endpoint | Method | Description |
|
|
1532
|
+
| --------------------------------------------------------------------- | ------ | --------------------------------- |
|
|
1533
|
+
| `/api/v1/public/token/exchange` | POST | Exchange API token for session ID |
|
|
1534
|
+
| `/api/v1/public/workspaces/:id/subscription/usage` | POST | Record quota usage |
|
|
1535
|
+
| `/api/v1/public/workspaces/:id/subscription/usage/status?quotaSlug=X` | GET | Get single quota status |
|
|
1536
|
+
| `/api/v1/public/workspaces/:id/subscription/usage/all` | GET | Get all quotas status |
|
|
1537
|
+
| `/api/v1/public/workspaces/:id/subscription/usage/logs` | GET | Get paginated usage logs |
|
|
1455
1538
|
|
|
1456
1539
|
All endpoints (except `/token/exchange`) require the `x-session-id` header.
|
|
1457
1540
|
|
|
1458
1541
|
**Record usage request body:**
|
|
1459
1542
|
|
|
1460
|
-
| Field
|
|
1461
|
-
|
|
1462
|
-
| `quotaSlug`
|
|
1463
|
-
| `quantity`
|
|
1464
|
-
| `metadata`
|
|
1465
|
-
| `source`
|
|
1466
|
-
| `idempotencyKey` | `string` | No
|
|
1543
|
+
| Field | Type | Required | Description |
|
|
1544
|
+
| ---------------- | -------- | -------- | -------------------------------------------------------------- |
|
|
1545
|
+
| `quotaSlug` | `string` | Yes | Quota identifier (e.g. `'api_calls'`, `'emails'`, `'storage'`) |
|
|
1546
|
+
| `quantity` | `number` | Yes | Units to consume (minimum 1) |
|
|
1547
|
+
| `metadata` | `object` | No | Custom metadata to attach to the usage record |
|
|
1548
|
+
| `source` | `string` | No | Source identifier (e.g. `'backend'`, `'worker'`, `'cron'`) |
|
|
1549
|
+
| `idempotencyKey` | `string` | No | Unique key for deduplication |
|
|
1467
1550
|
|
|
1468
1551
|
**Record usage response:**
|
|
1469
1552
|
|
|
1470
|
-
| Field
|
|
1471
|
-
|
|
1472
|
-
| `used`
|
|
1473
|
-
| `consumed`
|
|
1474
|
-
| `included`
|
|
1475
|
-
| `available`
|
|
1476
|
-
| `overage`
|
|
1553
|
+
| Field | Type | Description |
|
|
1554
|
+
| ------------- | --------- | -------------------------------------------- |
|
|
1555
|
+
| `used` | `number` | Quantity recorded in this request |
|
|
1556
|
+
| `consumed` | `number` | Total usage in the current billing period |
|
|
1557
|
+
| `included` | `number` | Total units included in the plan |
|
|
1558
|
+
| `available` | `number` | Remaining units before overage |
|
|
1559
|
+
| `overage` | `number` | Units used beyond the included amount |
|
|
1477
1560
|
| `billedAsync` | `boolean` | Whether overage billing was queued to Stripe |
|
|
1478
1561
|
|
|
1479
1562
|
## 🚦 Quota Gates
|
|
@@ -1542,12 +1625,12 @@ All quota gate components support optional `loadingComponent` and `fallbackCompo
|
|
|
1542
1625
|
|
|
1543
1626
|
### Quota Gate Components Reference
|
|
1544
1627
|
|
|
1545
|
-
| Component
|
|
1546
|
-
|
|
1547
|
-
| `WhenQuotaAvailable` | Quota has remaining units (`available > 0`)
|
|
1548
|
-
| `WhenQuotaExhausted` | Quota is fully consumed (`available <= 0`)
|
|
1549
|
-
| `WhenQuotaOverage`
|
|
1550
|
-
| `WhenQuotaThreshold` | Usage percentage >= threshold
|
|
1628
|
+
| Component | Renders children when | Props |
|
|
1629
|
+
| -------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
1630
|
+
| `WhenQuotaAvailable` | Quota has remaining units (`available > 0`) | `slug`, `children`, `loadingComponent?`, `fallbackComponent?` |
|
|
1631
|
+
| `WhenQuotaExhausted` | Quota is fully consumed (`available <= 0`) | `slug`, `children`, `loadingComponent?`, `fallbackComponent?` |
|
|
1632
|
+
| `WhenQuotaOverage` | Usage exceeds included amount (`hasOverage`) | `slug`, `children`, `loadingComponent?`, `fallbackComponent?` |
|
|
1633
|
+
| `WhenQuotaThreshold` | Usage percentage >= threshold | `slug`, `threshold` (0-100), `children`, `loadingComponent?`, `fallbackComponent?` |
|
|
1551
1634
|
|
|
1552
1635
|
All gates must be used inside `QuotaUsageContextProvider` (included in `SaaSOSProvider`). By default they return `null` while loading or when the condition is not met.
|
|
1553
1636
|
|
|
@@ -1567,7 +1650,9 @@ function QuotaDebug() {
|
|
|
1567
1650
|
return (
|
|
1568
1651
|
<div>
|
|
1569
1652
|
{Object.entries(quotas).map(([slug, usage]) => (
|
|
1570
|
-
<p key={slug}>
|
|
1653
|
+
<p key={slug}>
|
|
1654
|
+
{slug}: {usage.consumed}/{usage.included}
|
|
1655
|
+
</p>
|
|
1571
1656
|
))}
|
|
1572
1657
|
<button onClick={() => refetch()}>Refresh</button>
|
|
1573
1658
|
</div>
|
|
@@ -1575,13 +1660,14 @@ function QuotaDebug() {
|
|
|
1575
1660
|
}
|
|
1576
1661
|
```
|
|
1577
1662
|
|
|
1578
|
-
| Property
|
|
1579
|
-
|
|
1580
|
-
| `quotas`
|
|
1581
|
-
| `loading` | `boolean`
|
|
1582
|
-
| `refetch` | `() => Promise<void>`
|
|
1663
|
+
| Property | Type | Description |
|
|
1664
|
+
| --------- | ------------------------------------------- | -------------------------------------- |
|
|
1665
|
+
| `quotas` | `Record<string, IQuotaUsageStatus> \| null` | Current quota usage data keyed by slug |
|
|
1666
|
+
| `loading` | `boolean` | True while quota data is being fetched |
|
|
1667
|
+
| `refetch` | `() => Promise<void>` | Manually refetch all quota usage |
|
|
1583
1668
|
|
|
1584
1669
|
**When quota usage refetches:**
|
|
1670
|
+
|
|
1585
1671
|
- When the current workspace changes (automatic).
|
|
1586
1672
|
- When usage is recorded via `useRecordUsage` — refetch is triggered automatically.
|
|
1587
1673
|
- When you call `refetch()` manually.
|
|
@@ -1598,7 +1684,6 @@ function SignupPage() {
|
|
|
1598
1684
|
<BetaForm
|
|
1599
1685
|
onSuccess={() => console.log('Form submitted!')}
|
|
1600
1686
|
onError={error => console.error(error)}
|
|
1601
|
-
language="en" // Optional: 'en' | 'es' | 'fr' | 'de' | 'zh' | 'ja' | 'ko'
|
|
1602
1687
|
showSuccessMessage={true}
|
|
1603
1688
|
hideLogo={false}
|
|
1604
1689
|
hideTitles={false}
|
|
@@ -1685,25 +1770,25 @@ All SDK API clients extend a shared base class and are exported from the package
|
|
|
1685
1770
|
| `BaseApi` | Abstract base (URL, auth, `fetchJson`/`fetchResponse`) – extend for custom APIs |
|
|
1686
1771
|
| `IBaseApiConfig` | Config type: `serverUrl`, `version`, optional `orgId` |
|
|
1687
1772
|
| `UserApi` | User attributes and features |
|
|
1688
|
-
| `WorkspaceApi` | Workspaces, subscription, invoices, quota usage, users
|
|
1773
|
+
| `WorkspaceApi` | Workspaces, subscription, invoices, quota usage, users |
|
|
1689
1774
|
| `SettingsApi` | Organization settings |
|
|
1690
1775
|
|
|
1691
1776
|
### Components
|
|
1692
1777
|
|
|
1693
|
-
| Component
|
|
1694
|
-
|
|
|
1695
|
-
| `SubscriptionContextProvider`
|
|
1696
|
-
| `WhenSubscription`, `WhenNoSubscription`, `WhenSubscriptionToPlans` | Subscription gate components
|
|
1697
|
-
| `WhenTrialing`, `WhenNotTrialing`, `WhenTrialEnding`
|
|
1698
|
-
| `WhenQuotaAvailable`, `WhenQuotaExhausted`, `WhenQuotaOverage`
|
|
1778
|
+
| Component | Purpose |
|
|
1779
|
+
| ------------------------------------------------------------------- | ------------------------------------------------------------------- |
|
|
1780
|
+
| `SubscriptionContextProvider` | Provides subscription data to children (included in SaaSOSProvider) |
|
|
1781
|
+
| `WhenSubscription`, `WhenNoSubscription`, `WhenSubscriptionToPlans` | Subscription gate components |
|
|
1782
|
+
| `WhenTrialing`, `WhenNotTrialing`, `WhenTrialEnding` | Trial gate components |
|
|
1783
|
+
| `WhenQuotaAvailable`, `WhenQuotaExhausted`, `WhenQuotaOverage` | Quota gate components |
|
|
1699
1784
|
|
|
1700
1785
|
### Currency, pricing variant & quota utilities
|
|
1701
1786
|
|
|
1702
|
-
| Category
|
|
1703
|
-
|
|
|
1704
|
-
| **Currency**
|
|
1787
|
+
| Category | Exports |
|
|
1788
|
+
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1789
|
+
| **Currency** | `CURRENCY_DISPLAY`, `CURRENCY_FLAG`, `PLAN_CURRENCY_CODES`, `PLAN_CURRENCY_OPTIONS`, `getCurrencySymbol`, `getCurrencyFlag`, `formatCents`, `formatOverageRate`, `formatOverageRateWithLabel`, `formatQuotaIncludedOverage`, `getQuotaUnitLabelFromName` |
|
|
1705
1790
|
| **Pricing variants** | `getPricingVariant`, `getBasePriceCents`, `getStripePriceIdForInterval`, `getQuotaOverageCents`, `getQuotaDisplayWithVariant`, `getAvailableCurrenciesFromPlans`, `getDisplayCurrency`, `getBillingIntervalAndCurrencyFromPriceId`; types: `IPricingVariant`, `PlanVersionWithPricingVariants`, `QuotaDisplayWithOverage` |
|
|
1706
|
-
| **Quota**
|
|
1791
|
+
| **Quota** | `getQuotaDisplayValue`, `formatQuotaWithPrice`; types: `QuotaDisplayValue`, `FormatQuotaWithPriceOptions`. Plan types use `IQuotaByInterval`, `IQuotaIntervalValue` for per-interval quotas. |
|
|
1707
1792
|
|
|
1708
1793
|
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.):
|
|
1709
1794
|
|
|
@@ -1724,21 +1809,21 @@ const workspaceApi = new WorkspaceApi({
|
|
|
1724
1809
|
|
|
1725
1810
|
Prefer these SDK hooks for state and operations instead of `useAppSelector`:
|
|
1726
1811
|
|
|
1727
|
-
| Hook | Purpose
|
|
1728
|
-
| -------------------------- |
|
|
1729
|
-
| `useSaaSAuth()` | Auth state (user, session, status), signIn, signOut, openWorkspaceSettings
|
|
1730
|
-
| `useSaaSWorkspaces()` | Workspaces, currentWorkspace, loading, switching/switchingToId, CRUD and switch actions
|
|
1731
|
-
| `useSaaSOs()` | OS config (serverUrl, version, orgId, auth, settings) when you need the full config object
|
|
1732
|
-
| `useSaaSSettings()` | Organization settings and getSettings (prefer this when you only need settings)
|
|
1733
|
-
| `useUserAttributes()` | User attributes and update/refresh
|
|
1734
|
-
| `useUserFeatures()` | User feature flags
|
|
1735
|
-
| `useSubscriptionContext()` | Subscription for current workspace (response, loading, refetch); use inside SubscriptionContextProvider
|
|
1736
|
-
| `useTrialStatus()` | Trial state: `isTrialing`, `daysRemaining`, `trialEndsAt`, `isTrialEnding`
|
|
1737
|
-
| `usePushNotifications()` | Push notification state and actions: `isSubscribed`, `subscribe()`, `unsubscribe()`
|
|
1812
|
+
| Hook | Purpose |
|
|
1813
|
+
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1814
|
+
| `useSaaSAuth()` | Auth state (user, session, status), signIn(returnUrl?), signOut, openWorkspaceSettings |
|
|
1815
|
+
| `useSaaSWorkspaces()` | Workspaces, currentWorkspace, loading, switching/switchingToId, CRUD and switch actions |
|
|
1816
|
+
| `useSaaSOs()` | OS config (serverUrl, version, orgId, auth, settings) when you need the full config object |
|
|
1817
|
+
| `useSaaSSettings()` | Organization settings and getSettings (prefer this when you only need settings) |
|
|
1818
|
+
| `useUserAttributes()` | User attributes and update/refresh |
|
|
1819
|
+
| `useUserFeatures()` | User feature flags |
|
|
1820
|
+
| `useSubscriptionContext()` | Subscription for current workspace (response, loading, refetch); use inside SubscriptionContextProvider |
|
|
1821
|
+
| `useTrialStatus()` | Trial state: `isTrialing`, `daysRemaining`, `trialEndsAt`, `isTrialEnding` |
|
|
1822
|
+
| `usePushNotifications()` | Push notification state and actions: `isSubscribed`, `subscribe()`, `unsubscribe()` |
|
|
1738
1823
|
| Subscription hooks | `usePublicPlans`, `useSubscription`, `useSubscriptionManagement`, `usePlanGroup`, `usePlanGroupVersions`, `usePublicPlanGroupVersion`, `useCreateCheckoutSession`, `useUpdateSubscription`, `useCancelSubscription`, `useResumeSubscription`, `useInvoices`, `useInvoice` |
|
|
1739
|
-
| `useQuotaUsageContext()` | Quota usage for current workspace (quotas, loading, refetch); use inside QuotaUsageContextProvider
|
|
1740
|
-
| Quota usage hooks | `useRecordUsage`, `useQuotaUsageStatus`, `useAllQuotaUsage`, `useUsageLogs`
|
|
1741
|
-
| Invalidation helpers | `invalidateSubscription()`, `invalidateQuotaUsage()` — trigger context refetch after server-side mutations
|
|
1824
|
+
| `useQuotaUsageContext()` | Quota usage for current workspace (quotas, loading, refetch); use inside QuotaUsageContextProvider |
|
|
1825
|
+
| Quota usage hooks | `useRecordUsage`, `useQuotaUsageStatus`, `useAllQuotaUsage`, `useUsageLogs` |
|
|
1826
|
+
| Invalidation helpers | `invalidateSubscription()`, `invalidateQuotaUsage()` — trigger context refetch after server-side mutations |
|
|
1742
1827
|
|
|
1743
1828
|
Using hooks keeps your code stable if internal state shape changes and avoids direct Redux/context coupling.
|
|
1744
1829
|
|
|
@@ -1796,6 +1881,7 @@ interface OnWorkspaceChangeParams {
|
|
|
1796
1881
|
```
|
|
1797
1882
|
|
|
1798
1883
|
**Session flow** (same pattern as next-auth):
|
|
1884
|
+
|
|
1799
1885
|
- Session token is stored in an **httpOnly cookie** (set by your server, not readable by JS)
|
|
1800
1886
|
- On page refresh, the SDK calls `getSession()` once to restore the session
|
|
1801
1887
|
- Session data (user info) lives **in-memory only** (React context) — no localStorage
|
|
@@ -1808,19 +1894,18 @@ interface OnWorkspaceChangeParams {
|
|
|
1808
1894
|
|
|
1809
1895
|
### BetaForm Props
|
|
1810
1896
|
|
|
1811
|
-
| Prop
|
|
1812
|
-
|
|
|
1813
|
-
| `onSuccess`
|
|
1814
|
-
| `onError`
|
|
1815
|
-
| `className`
|
|
1816
|
-
| `fieldClassName`
|
|
1817
|
-
| `
|
|
1818
|
-
| `
|
|
1819
|
-
| `
|
|
1820
|
-
| `
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
| `hideTitles` | `boolean` | `false` | Hide titles |
|
|
1897
|
+
| Prop | Type | Default | Description |
|
|
1898
|
+
| -------------------- | ------------------------- | -------------------------------- | --------------------------------------- |
|
|
1899
|
+
| `onSuccess` | `() => void` | - | Callback when form submits successfully |
|
|
1900
|
+
| `onError` | `(error: string) => void` | - | Callback when form submission fails |
|
|
1901
|
+
| `className` | `string` | `'w-full'` | CSS class for form container |
|
|
1902
|
+
| `fieldClassName` | `string` | `'flex flex-col gap-1.5 w-full'` | CSS class for form fields |
|
|
1903
|
+
| `autoFocus` | `boolean` | `true` | Auto-focus name field |
|
|
1904
|
+
| `showSuccessMessage` | `boolean` | `true` | Show success message after submit |
|
|
1905
|
+
| `hideLogo` | `boolean` | `false` | Hide logo |
|
|
1906
|
+
| `hideTitles` | `boolean` | `false` | Hide titles |
|
|
1907
|
+
|
|
1908
|
+
> **Note:** The form language is inherited from the `SaaSOSProvider` locale setting. Supported locales: en, es, fr, de, ja, zh, hi, ar.
|
|
1824
1909
|
|
|
1825
1910
|
## 🎯 Common Patterns
|
|
1826
1911
|
|
|
@@ -1918,7 +2003,11 @@ function Dashboard() {
|
|
|
1918
2003
|
### Pattern 4b: Subscription-Gated UI
|
|
1919
2004
|
|
|
1920
2005
|
```tsx
|
|
1921
|
-
import {
|
|
2006
|
+
import {
|
|
2007
|
+
WhenSubscription,
|
|
2008
|
+
WhenNoSubscription,
|
|
2009
|
+
WhenSubscriptionToPlans,
|
|
2010
|
+
} from '@buildbase/sdk/react';
|
|
1922
2011
|
|
|
1923
2012
|
function BillingPage() {
|
|
1924
2013
|
return (
|
|
@@ -1958,7 +2047,9 @@ function FeatureWithQuota() {
|
|
|
1958
2047
|
</WhenQuotaAvailable>
|
|
1959
2048
|
|
|
1960
2049
|
<WhenQuotaExhausted slug="api_calls">
|
|
1961
|
-
<p>
|
|
2050
|
+
<p>
|
|
2051
|
+
No API calls remaining. <a href="/billing">Upgrade your plan</a>
|
|
2052
|
+
</p>
|
|
1962
2053
|
</WhenQuotaExhausted>
|
|
1963
2054
|
</div>
|
|
1964
2055
|
);
|
|
@@ -2246,7 +2337,7 @@ if (isFeatureEnabled('premium')) {
|
|
|
2246
2337
|
```tsx
|
|
2247
2338
|
// ✅ Good
|
|
2248
2339
|
const { signIn, status } = useSaaSAuth();
|
|
2249
|
-
<button onClick={signIn} disabled={status === 'loading'}>
|
|
2340
|
+
<button onClick={() => signIn()} disabled={status === 'loading'}>
|
|
2250
2341
|
{status === 'loading' ? 'Signing in...' : 'Sign In'}
|
|
2251
2342
|
</button>;
|
|
2252
2343
|
```
|
|
@@ -2320,31 +2411,45 @@ Configure once, use everywhere. Same pattern as Auth.js.
|
|
|
2320
2411
|
|
|
2321
2412
|
```ts
|
|
2322
2413
|
// lib/buildbase.ts
|
|
2323
|
-
import BuildBase from '@buildbase/sdk'
|
|
2324
|
-
import { cookies } from 'next/headers'
|
|
2325
|
-
|
|
2326
|
-
export const {
|
|
2414
|
+
import BuildBase from '@buildbase/sdk';
|
|
2415
|
+
import { cookies } from 'next/headers';
|
|
2416
|
+
|
|
2417
|
+
export const {
|
|
2418
|
+
auth,
|
|
2419
|
+
workspace,
|
|
2420
|
+
subscription,
|
|
2421
|
+
usage,
|
|
2422
|
+
plans,
|
|
2423
|
+
invoices,
|
|
2424
|
+
users,
|
|
2425
|
+
features,
|
|
2426
|
+
settings,
|
|
2427
|
+
notification,
|
|
2428
|
+
permissions,
|
|
2429
|
+
withSession,
|
|
2430
|
+
client,
|
|
2431
|
+
} = BuildBase({
|
|
2327
2432
|
serverUrl: process.env.BUILDBASE_URL!,
|
|
2328
2433
|
orgId: process.env.BUILDBASE_ORG_ID!,
|
|
2329
2434
|
getSessionId: async () => {
|
|
2330
|
-
const c = await cookies()
|
|
2331
|
-
return c.get('bb-session-id')?.value ?? null
|
|
2435
|
+
const c = await cookies();
|
|
2436
|
+
return c.get('bb-session-id')?.value ?? null;
|
|
2332
2437
|
},
|
|
2333
|
-
})
|
|
2438
|
+
});
|
|
2334
2439
|
```
|
|
2335
2440
|
|
|
2336
2441
|
Use in API routes — session is resolved automatically:
|
|
2337
2442
|
|
|
2338
2443
|
```ts
|
|
2339
2444
|
// app/api/workspace/route.ts
|
|
2340
|
-
import { auth, workspace, subscription } from '@/lib/buildbase'
|
|
2445
|
+
import { auth, workspace, subscription } from '@/lib/buildbase';
|
|
2341
2446
|
|
|
2342
2447
|
export async function GET() {
|
|
2343
|
-
const session = await auth()
|
|
2344
|
-
if (!session) return Response.json({ error: 'Unauthorized' }, { status: 401 })
|
|
2448
|
+
const session = await auth();
|
|
2449
|
+
if (!session) return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
2345
2450
|
|
|
2346
|
-
const workspaces = await workspace.list()
|
|
2347
|
-
return Response.json({ workspaces })
|
|
2451
|
+
const workspaces = await workspace.list();
|
|
2452
|
+
return Response.json({ workspaces });
|
|
2348
2453
|
}
|
|
2349
2454
|
```
|
|
2350
2455
|
|
|
@@ -2354,31 +2459,31 @@ No `getSessionId` callback — use `withSession()` per-request instead.
|
|
|
2354
2459
|
|
|
2355
2460
|
```ts
|
|
2356
2461
|
// buildbase.ts
|
|
2357
|
-
import BuildBase from '@buildbase/sdk'
|
|
2462
|
+
import BuildBase from '@buildbase/sdk';
|
|
2358
2463
|
|
|
2359
2464
|
const bb = BuildBase({
|
|
2360
2465
|
serverUrl: process.env.BUILDBASE_URL!,
|
|
2361
2466
|
orgId: process.env.BUILDBASE_ORG_ID!,
|
|
2362
|
-
})
|
|
2467
|
+
});
|
|
2363
2468
|
|
|
2364
|
-
export const { withSession, plans } = bb
|
|
2469
|
+
export const { withSession, plans } = bb;
|
|
2365
2470
|
```
|
|
2366
2471
|
|
|
2367
2472
|
```ts
|
|
2368
2473
|
// routes
|
|
2369
2474
|
app.get('/workspaces', async (req, res) => {
|
|
2370
|
-
const { workspace } = withSession(req.headers['x-session-id'])
|
|
2371
|
-
res.json(await workspace.list())
|
|
2372
|
-
})
|
|
2475
|
+
const { workspace } = withSession(req.headers['x-session-id']);
|
|
2476
|
+
res.json(await workspace.list());
|
|
2477
|
+
});
|
|
2373
2478
|
|
|
2374
2479
|
app.post('/usage', async (req, res) => {
|
|
2375
|
-
const { usage } = withSession(req.headers['x-session-id'])
|
|
2480
|
+
const { usage } = withSession(req.headers['x-session-id']);
|
|
2376
2481
|
const result = await usage.record(req.body.workspaceId, {
|
|
2377
2482
|
quotaSlug: 'api-calls',
|
|
2378
2483
|
quantity: 1,
|
|
2379
|
-
})
|
|
2380
|
-
res.json(result)
|
|
2381
|
-
})
|
|
2484
|
+
});
|
|
2485
|
+
res.json(result);
|
|
2486
|
+
});
|
|
2382
2487
|
```
|
|
2383
2488
|
|
|
2384
2489
|
### Background Jobs / Webhooks
|
|
@@ -2386,35 +2491,35 @@ app.post('/usage', async (req, res) => {
|
|
|
2386
2491
|
For service accounts (no user session), use `withSession()` with a service token:
|
|
2387
2492
|
|
|
2388
2493
|
```ts
|
|
2389
|
-
import { withSession } from '@/lib/buildbase'
|
|
2494
|
+
import { withSession } from '@/lib/buildbase';
|
|
2390
2495
|
|
|
2391
|
-
const bb = withSession(process.env.SERVICE_SESSION_ID!)
|
|
2496
|
+
const bb = withSession(process.env.SERVICE_SESSION_ID!);
|
|
2392
2497
|
|
|
2393
2498
|
// Record usage from a webhook
|
|
2394
2499
|
await bb.usage.record(workspaceId, {
|
|
2395
2500
|
quotaSlug: 'uploads',
|
|
2396
2501
|
quantity: 1,
|
|
2397
2502
|
source: 'webhook:file.processed',
|
|
2398
|
-
})
|
|
2503
|
+
});
|
|
2399
2504
|
|
|
2400
2505
|
// Check subscription
|
|
2401
|
-
const sub = await bb.subscription.get(workspaceId)
|
|
2506
|
+
const sub = await bb.subscription.get(workspaceId);
|
|
2402
2507
|
```
|
|
2403
2508
|
|
|
2404
2509
|
### Server-Side Actions Reference
|
|
2405
2510
|
|
|
2406
|
-
| Module
|
|
2407
|
-
|
|
2408
|
-
| `workspace`
|
|
2409
|
-
| `users`
|
|
2410
|
-
| `subscription` | `get`, `checkout`, `update`, `cancel`, `resume`, `getBillingPortalUrl`
|
|
2411
|
-
| `plans`
|
|
2412
|
-
| `invoices`
|
|
2413
|
-
| `usage`
|
|
2414
|
-
| `settings`
|
|
2415
|
-
| `features`
|
|
2416
|
-
| `notification` | `send(workspaceId, event, userId?, data?)`
|
|
2417
|
-
| `permissions`
|
|
2511
|
+
| Module | Methods |
|
|
2512
|
+
| -------------- | ------------------------------------------------------------------------ |
|
|
2513
|
+
| `workspace` | `list`, `get`, `create`, `update`, `delete` |
|
|
2514
|
+
| `users` | `list`, `invite`, `remove`, `updateRole`, `getProfile`, `updateProfile` |
|
|
2515
|
+
| `subscription` | `get`, `checkout`, `update`, `cancel`, `resume`, `getBillingPortalUrl` |
|
|
2516
|
+
| `plans` | `getGroup`, `getVersions`, `getPublic`, `getVersion` |
|
|
2517
|
+
| `invoices` | `list`, `get` |
|
|
2518
|
+
| `usage` | `record`, `recordBatch`, `getQuota`, `getAll`, `getLogs` |
|
|
2519
|
+
| `settings` | `get` |
|
|
2520
|
+
| `features` | `list`, `update` |
|
|
2521
|
+
| `notification` | `send(workspaceId, event, userId?, data?)` |
|
|
2522
|
+
| `permissions` | `check(workspaceId, userId, permission)`, `resolve(workspaceId, userId)` |
|
|
2418
2523
|
|
|
2419
2524
|
### BuildBase Config Options
|
|
2420
2525
|
|