@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.
@@ -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 — read from existing auth context. The exact field
56
- // name varies by SDK; we look up `billing` privilege on the current user.
57
- // Defaults to non-admin if unavailable.
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
- // Best-effort role read. Existing auth-core role probing varies by app;
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: any authenticated user is
68
- // treated as a billing admin for US-11 until the role/privilege API is
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
- const auth = getBridgeAuth();
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 ? snapshot.used > snapshot.limit : false,
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
- const visible = $derived(snapshot !== undefined && warningLevel !== null);
109
+ // Metered never blocks, so it never renders as 'critical' — it's informational.
103
110
  const severity = $derived<Severity>(
104
- warningLevel === 'critical' || overCap ? 'critical' : 'warn',
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
- const over = snap.used > snap.limit;
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
- <div class="bqb-meter" aria-hidden="true">
182
- <div class="bqb-meter-fill" style={`width: ${Math.min(100, percent)}%`}></div>
183
- </div>
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.6",
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": ">=10.0.0",
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.7"
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": "^5.0.0",
80
+ "typescript": "^6.0.0",
81
81
  "vite": "^6.2.6",
82
82
  "vitest": "^4.1.4"
83
83
  },