@nebulr-group/bridge-svelte 0.4.0-beta.6 → 0.4.0-beta.8
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/dist/client/components/sdk-auth/PasskeyLogin.svelte +1 -1
- package/dist/client/components/sdk-auth/PasskeySetup.svelte +1 -1
- package/dist/client/components/subscription/BridgeBillingNotice.svelte +4 -12
- package/dist/client/components/subscription/BridgeQuotaBanner.svelte +73 -15
- package/dist/client/components/subscription/PlanSelector.svelte +7 -2
- package/package.json +4 -4
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
|
|
41
41
|
let authResponse: any;
|
|
42
42
|
if (typeof window !== 'undefined' && (window as any).__simpleWebAuthn?.startAuthentication) {
|
|
43
|
-
authResponse = await (window as any).__simpleWebAuthn.startAuthentication(options, autofill);
|
|
43
|
+
authResponse = await (window as any).__simpleWebAuthn.startAuthentication({ optionsJSON: options, useBrowserAutofill: autofill });
|
|
44
44
|
} else {
|
|
45
45
|
// @ts-ignore — @simplewebauthn/browser is an optional peer dependency
|
|
46
46
|
const mod = await import('@simplewebauthn/browser');
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
|
|
55
55
|
let regResponse: any;
|
|
56
56
|
if (typeof window !== 'undefined' && (window as any).__simpleWebAuthn?.startRegistration) {
|
|
57
|
-
regResponse = await (window as any).__simpleWebAuthn.startRegistration(options);
|
|
57
|
+
regResponse = await (window as any).__simpleWebAuthn.startRegistration({ optionsJSON: options });
|
|
58
58
|
} else {
|
|
59
59
|
// @ts-ignore — @simplewebauthn/browser is an optional peer dependency
|
|
60
60
|
const mod = await import('@simplewebauthn/browser');
|
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
let snapshot = $state<BillingSubscriptionSnapshot>(useBridge().subscription.snapshot());
|
|
53
53
|
let unsubscribe: (() => void) | undefined;
|
|
54
54
|
|
|
55
|
-
// Admin/member variant
|
|
56
|
-
//
|
|
57
|
-
//
|
|
55
|
+
// Admin/member variant. v1 policy: workspace owner only, via
|
|
56
|
+
// canManageBilling() (immutable OWNER role key). Defaults to non-admin if
|
|
57
|
+
// unavailable.
|
|
58
58
|
let isBillingAdmin = $state(false);
|
|
59
59
|
|
|
60
60
|
onMount(() => {
|
|
@@ -65,15 +65,7 @@
|
|
|
65
65
|
const auth = getBridgeAuth();
|
|
66
66
|
const ctx = auth.getApiContext();
|
|
67
67
|
if (ctx.accessToken) {
|
|
68
|
-
|
|
69
|
-
// defensive cast so the component never crashes on missing fields.
|
|
70
|
-
const tokenManager = (auth as unknown as { tokenManager?: { getTokens?: () => { accessToken?: string } } }).tokenManager;
|
|
71
|
-
void tokenManager; // intentionally unused — placeholder until role API is finalized
|
|
72
|
-
// For US-5 default: assume admin if a JWT is present. Subsequent
|
|
73
|
-
// stories will read the exact privilege claim. Keeping permissive so
|
|
74
|
-
// the CTA shows by default; member variant lands when the role check
|
|
75
|
-
// is wired through.
|
|
76
|
-
isBillingAdmin = true;
|
|
68
|
+
isBillingAdmin = auth.canManageBilling();
|
|
77
69
|
}
|
|
78
70
|
|
|
79
71
|
if (!ctx.accessToken) {
|
|
@@ -64,9 +64,8 @@
|
|
|
64
64
|
let snapshot = $state<QuotaSnapshot | undefined>(useBridge().quota(metric));
|
|
65
65
|
let unsubscribe: (() => void) | undefined;
|
|
66
66
|
|
|
67
|
-
// Role variant mirrors BridgeBillingNotice:
|
|
68
|
-
//
|
|
69
|
-
// finalized. Member variant lands when the role probe is wired through.
|
|
67
|
+
// Role variant mirrors BridgeBillingNotice: CTA only for the workspace
|
|
68
|
+
// owner (v1 canManageBilling() policy). Member variant renders otherwise.
|
|
70
69
|
let isBillingAdmin = $state(false);
|
|
71
70
|
|
|
72
71
|
onMount(() => {
|
|
@@ -78,11 +77,7 @@
|
|
|
78
77
|
snapshot = useBridge().quota(metric);
|
|
79
78
|
|
|
80
79
|
try {
|
|
81
|
-
|
|
82
|
-
const ctx = auth.getApiContext();
|
|
83
|
-
if (ctx.accessToken) {
|
|
84
|
-
isBillingAdmin = true;
|
|
85
|
-
}
|
|
80
|
+
isBillingAdmin = getBridgeAuth().canManageBilling();
|
|
86
81
|
} catch {
|
|
87
82
|
// No BridgeAuth instance — render the member variant.
|
|
88
83
|
}
|
|
@@ -96,12 +91,28 @@
|
|
|
96
91
|
});
|
|
97
92
|
|
|
98
93
|
const warningLevel = $derived(snapshot?.warningLevel ?? null);
|
|
94
|
+
// TBP-275 — `metered` quotas bill overage instead of blocking. Prefer the
|
|
95
|
+
// server-authoritative `overcap` flag (handles limit === 0 pure-per-unit)
|
|
96
|
+
// over a UI-derived `used > limit`.
|
|
97
|
+
const isMetered = $derived(snapshot?.policy === 'metered');
|
|
99
98
|
const overCap = $derived(
|
|
100
|
-
snapshot
|
|
99
|
+
snapshot
|
|
100
|
+
? (snapshot.overcap ?? snapshot.used > snapshot.limit)
|
|
101
|
+
: false,
|
|
102
|
+
);
|
|
103
|
+
// Hard caps show only at the warning thresholds. Metered quotas also show
|
|
104
|
+
// once billing has engaged (overCap), even with no warningLevel — that's the
|
|
105
|
+
// live "you're now being billed for overage" state.
|
|
106
|
+
const visible = $derived(
|
|
107
|
+
snapshot !== undefined && (warningLevel !== null || (isMetered && overCap)),
|
|
101
108
|
);
|
|
102
|
-
|
|
109
|
+
// Metered never blocks, so it never renders as 'critical' — it's informational.
|
|
103
110
|
const severity = $derived<Severity>(
|
|
104
|
-
|
|
111
|
+
isMetered
|
|
112
|
+
? 'warn'
|
|
113
|
+
: warningLevel === 'critical' || overCap
|
|
114
|
+
? 'critical'
|
|
115
|
+
: 'warn',
|
|
105
116
|
);
|
|
106
117
|
const displayLabel = $derived(label ?? snapshot?.label ?? metric);
|
|
107
118
|
const percent = $derived(
|
|
@@ -109,13 +120,58 @@
|
|
|
109
120
|
? Math.min(100, Math.round((snapshot.used / snapshot.limit) * 100))
|
|
110
121
|
: 0,
|
|
111
122
|
);
|
|
123
|
+
// Meter bar is meaningless for pure per-unit metered (limit 0) — hide it there.
|
|
124
|
+
const showMeter = $derived(!!snapshot && snapshot.limit > 0);
|
|
125
|
+
|
|
126
|
+
/** Format an estimated cost like "$1.00" / "1.00 SEK". */
|
|
127
|
+
function formatCost(amount: number | undefined, currency: string | undefined): string {
|
|
128
|
+
if (amount === undefined) return '';
|
|
129
|
+
const cur = (currency ?? '').toUpperCase();
|
|
130
|
+
try {
|
|
131
|
+
return new Intl.NumberFormat(undefined, {
|
|
132
|
+
style: 'currency',
|
|
133
|
+
currency: cur || 'USD',
|
|
134
|
+
}).format(amount);
|
|
135
|
+
} catch {
|
|
136
|
+
// Unknown currency code → fall back to "<amount> <CUR>".
|
|
137
|
+
return `${amount.toFixed(2)}${cur ? ` ${cur}` : ''}`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
112
140
|
|
|
113
141
|
function getCopy(
|
|
114
142
|
snap: QuotaSnapshot | undefined,
|
|
115
143
|
admin: boolean,
|
|
116
144
|
): { title: string; body: string; cta?: string } {
|
|
117
145
|
if (!snap) return { title: '', body: '' };
|
|
118
|
-
|
|
146
|
+
|
|
147
|
+
// TBP-275 — metered: live usage + projected cost, never a blocking message.
|
|
148
|
+
if (snap.policy === 'metered') {
|
|
149
|
+
const overUnits = snap.limit > 0 ? Math.max(0, snap.used - snap.limit) : snap.used;
|
|
150
|
+
const cost = formatCost(snap.overageEstimate, snap.currency);
|
|
151
|
+
const costSuffix = cost ? ` · ~${cost} estimated this period` : '';
|
|
152
|
+
if (snap.limit > 0) {
|
|
153
|
+
// included allotment + overage
|
|
154
|
+
if (overUnits > 0) {
|
|
155
|
+
return {
|
|
156
|
+
title: `${displayLabel} overage`,
|
|
157
|
+
body: `${overUnits.toLocaleString()} over your ${snap.limit.toLocaleString()} included${costSuffix}.`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
// approaching the included allotment (warningLevel drove visibility)
|
|
161
|
+
const unit = formatCost(snap.unitAmount, snap.currency);
|
|
162
|
+
return {
|
|
163
|
+
title: `${displayLabel} approaching included limit`,
|
|
164
|
+
body: `You've used ${snap.used.toLocaleString()} of ${snap.limit.toLocaleString()} included${unit ? ` — extra usage is billed at ${unit}/unit` : ''}.`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
// pure per-unit (limit 0) — billed from unit 1
|
|
168
|
+
return {
|
|
169
|
+
title: `${displayLabel} usage`,
|
|
170
|
+
body: `${snap.used.toLocaleString()} ${displayLabel}${costSuffix}.`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const over = snap.overcap ?? snap.used > snap.limit;
|
|
119
175
|
const remaining = Math.max(0, snap.remaining);
|
|
120
176
|
if (over) {
|
|
121
177
|
return admin
|
|
@@ -178,9 +234,11 @@
|
|
|
178
234
|
<div class="bqb-content">
|
|
179
235
|
<strong class="bqb-title">{copy.title}</strong>
|
|
180
236
|
<span class="bqb-body">{copy.body}</span>
|
|
181
|
-
|
|
182
|
-
<div class="bqb-meter
|
|
183
|
-
|
|
237
|
+
{#if showMeter}
|
|
238
|
+
<div class="bqb-meter" aria-hidden="true">
|
|
239
|
+
<div class="bqb-meter-fill" style={`width: ${Math.min(100, percent)}%`}></div>
|
|
240
|
+
</div>
|
|
241
|
+
{/if}
|
|
184
242
|
</div>
|
|
185
243
|
{#if copy.cta && isBillingAdmin}
|
|
186
244
|
<button type="button" class="bqb-cta" onclick={handleAction}>{copy.cta}</button>
|
|
@@ -121,8 +121,13 @@
|
|
|
121
121
|
picking = true;
|
|
122
122
|
pickError = null;
|
|
123
123
|
try {
|
|
124
|
-
if (price.amount === 0) {
|
|
125
|
-
// Free plan — select directly
|
|
124
|
+
if (price.amount === 0 && !plan.hasCost) {
|
|
125
|
+
// Free plan — select directly. TBP-275: guard on `!plan.hasCost` so a
|
|
126
|
+
// $0-base plan that carries METERED pricing is NOT treated as free —
|
|
127
|
+
// it falls through to checkout/changePlan below, capturing a payment
|
|
128
|
+
// method so per-unit overage can be billed (US-C). Without this guard a
|
|
129
|
+
// metered $0-base plan would hit selectFreePlan, which the backend now
|
|
130
|
+
// rejects ("has a cost — use the checkout endpoint instead").
|
|
126
131
|
await getBridgeAuth().selectFreePlan(plan.key);
|
|
127
132
|
await loadSubscription();
|
|
128
133
|
onSelect?.({ plan, price });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nebulr-group/bridge-svelte",
|
|
3
|
-
"version": "0.4.0-beta.
|
|
3
|
+
"version": "0.4.0-beta.8",
|
|
4
4
|
"description": "Bridge Svelte library, This library helps you to add bridge authentication and feature flags, and payments to your svelte application.",
|
|
5
5
|
"author": "Iman Pouya",
|
|
6
6
|
"license": "MIT",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"svelte": "^5.0.0",
|
|
57
57
|
"@sveltejs/kit": "^2.0.0",
|
|
58
|
-
"@simplewebauthn/browser": "
|
|
58
|
+
"@simplewebauthn/browser": "^13",
|
|
59
59
|
"@stripe/stripe-js": ">=4.0.0"
|
|
60
60
|
},
|
|
61
61
|
"peerDependenciesMeta": {
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
}
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
|
-
"@nebulr-group/bridge-auth-core": "0.4.0-beta.
|
|
70
|
+
"@nebulr-group/bridge-auth-core": "0.4.0-beta.9"
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
73
73
|
"@sveltejs/kit": "^2.16.0",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"publint": "^0.3.2",
|
|
78
78
|
"svelte": "^5.0.0",
|
|
79
79
|
"svelte-check": "^4.0.0",
|
|
80
|
-
"typescript": "^
|
|
80
|
+
"typescript": "^6.0.0",
|
|
81
81
|
"vite": "^6.2.6",
|
|
82
82
|
"vitest": "^4.1.4"
|
|
83
83
|
},
|