@nebulr-group/bridge-svelte 0.9.1 → 0.9.2-beta.0

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.
@@ -7,6 +7,7 @@ import { dropFlagCache, guardCacheGeneration } from '../auth/guard-cache.js';
7
7
  import { getBridgeAuth, bridgeReadyStore, markReady, waitForBridge as _waitForBridge, } from '../core/bridge-instance.js';
8
8
  import { installBridgeAuthFetch } from '../core/bridge-runtime.js';
9
9
  import { refreshBilling, tokenStaleHandlerOf } from '../core/billing-store.js';
10
+ import { markCheckoutComplete } from './checkout-result.js';
10
11
  import { prefetchFlagDefinitions } from '../core/flag-definitions.js';
11
12
  import { useBridge, sanitizeReturnTo, stashReturnTo, takeReturnTo, withReturnTo, } from '@nebulr-group/bridge-auth-core';
12
13
  import { logger } from '../shared/logger.js';
@@ -418,6 +419,9 @@ async function handleCallbackRoute(url, kitFetch) {
418
419
  await refreshBilling();
419
420
  }
420
421
  catch { /* the landing page re-reads */ }
422
+ // TBP-813 — the landing page (the subscription page by default)
423
+ // says the checkout went through; no separate confirmation page.
424
+ markCheckoutComplete({ kind: 'checkout' });
421
425
  }
422
426
  redirect(303, redirectTo);
423
427
  }
@@ -36,8 +36,17 @@ export interface BridgeBillingRoutes {
36
36
  paywallIsDefault: boolean;
37
37
  /** Where a failed checkout confirmation lands. */
38
38
  paymentErrorRoute: string;
39
- /** Where a completed checkout lands by default: `<manageRoute>/success`. */
39
+ /**
40
+ * Where a completed checkout (or a Free pick) lands: `billing.successRoute`,
41
+ * else the subscription page, which shows the confirmation banner (TBP-813).
42
+ */
40
43
  successRoute: string;
44
+ /**
45
+ * True when `successRoute` is the default rather than the app's own choice.
46
+ * `<BridgeBillingRoutes>` then lands on its own subscription page, wherever
47
+ * its catch-all lives, instead of on `manageRoute`.
48
+ */
49
+ successIsDefault: boolean;
41
50
  }
42
51
  /**
43
52
  * Resolve the billing destinations from a `billing` config block. An unset
@@ -54,7 +54,8 @@ export function resolveBillingRoutes(billing) {
54
54
  paywallRoute: paywall === false ? null : paywall || BRIDGE_BILLING_DEFAULTS.paywallRoute,
55
55
  paywallIsDefault: paywall !== false && !paywall,
56
56
  paymentErrorRoute: billing?.paymentErrorRoute || BRIDGE_BILLING_DEFAULTS.paymentErrorRoute,
57
- successRoute: `${manageRoute.replace(/\/+$/, '')}/success`,
57
+ successRoute: billing?.successRoute || manageRoute,
58
+ successIsDefault: !billing?.successRoute,
58
59
  };
59
60
  }
60
61
  /**
@@ -0,0 +1,16 @@
1
+ import { type Readable } from 'svelte/store';
2
+ /** What just completed. */
3
+ export interface BridgeCheckoutResult {
4
+ /** `checkout`: a paid Stripe checkout returned. `plan`: a plan was set without one (a Free pick). */
5
+ kind: 'checkout' | 'plan';
6
+ /** The plan's name, when the page that set it knew it. Otherwise read the current plan. */
7
+ planName?: string;
8
+ }
9
+ /** The checkout that just completed, or `null`. Cleared once something shows it. */
10
+ export declare const checkoutResult: Readable<BridgeCheckoutResult | null>;
11
+ /** Record a completed checkout or plan pick. Called by the plugin, not by apps. */
12
+ export declare function markCheckoutComplete(value: BridgeCheckoutResult): void;
13
+ /** Read the result and clear it, so it is shown once. */
14
+ export declare function takeCheckoutResult(): BridgeCheckoutResult | null;
15
+ /** Forget the result without showing it. */
16
+ export declare function clearCheckoutResult(): void;
@@ -0,0 +1,31 @@
1
+ // TBP-813 — "a checkout just finished", for whichever page it lands on.
2
+ //
3
+ // A completed checkout used to stop on a confirmation page (`/subscription/success`)
4
+ // before going anywhere. It now lands on `billing.successRoute` (the subscription
5
+ // page by default), and this one-shot signal is how that page knows to say so:
6
+ // `<BridgeCheckoutNotice>` shows it as a banner, and an app that wants its own
7
+ // message reads `checkoutResult`.
8
+ //
9
+ // In memory only, never in the URL or storage: the checkout return redirects
10
+ // inside the running app, so the signal is still here when the landing page
11
+ // renders, and a reload (or a later visit) does not show it again. Whoever shows
12
+ // it clears it.
13
+ import { get, writable } from 'svelte/store';
14
+ const result = writable(null);
15
+ /** The checkout that just completed, or `null`. Cleared once something shows it. */
16
+ export const checkoutResult = { subscribe: result.subscribe };
17
+ /** Record a completed checkout or plan pick. Called by the plugin, not by apps. */
18
+ export function markCheckoutComplete(value) {
19
+ result.set(value);
20
+ }
21
+ /** Read the result and clear it, so it is shown once. */
22
+ export function takeCheckoutResult() {
23
+ const value = get(result);
24
+ if (value)
25
+ result.set(null);
26
+ return value;
27
+ }
28
+ /** Forget the result without showing it. */
29
+ export function clearCheckoutResult() {
30
+ result.set(null);
31
+ }
@@ -6,9 +6,10 @@
6
6
  <BridgeBillingRoutes />
7
7
 
8
8
  Serves, relative to wherever the catch-all lives:
9
- /subscription the current plan, the plan picker and "Manage billing"
9
+ /subscription the current plan, the plan picker and "Manage billing";
10
+ a completed checkout lands here, with a banner
10
11
  /subscription/plan the paywall: where a plan-less workspace is sent
11
- /subscription/success where a completed checkout lands
12
+ /subscription/success the old confirmation page, for `billing.successRoute`
12
13
  /subscription/error where a failed checkout confirmation lands
13
14
  An unknown segment gets the app's own 404 (bridgeBootstrap's load throws it —
14
15
  a component cannot).
@@ -28,7 +29,9 @@
28
29
  4. Headless: `PlanSelector`, `BridgeSubscriptionStatus`, `BillingPortalButton`
29
30
  and `subscriptionStore` are the pieces these pages are built from.
30
31
 
31
- A Free pick (like a paid checkout) lands on /subscription/success (TBP-762).
32
+ TBP-813 — a paid checkout and a Free pick land on `billing.successRoute`, by
33
+ default this catch-all's own subscription page, which shows
34
+ `<BridgeCheckoutNotice>`. Set `billing.successRoute` to land anywhere else.
32
35
  -->
33
36
  <script lang="ts">
34
37
  import type { Snippet } from 'svelte';
@@ -36,10 +39,15 @@
36
39
  import { page } from '$app/stores';
37
40
  import { ensureSubscription, loadSubscription } from '../../../core/bridge-instance.js';
38
41
  import { BRIDGE_AUTH_ROUTE_PARAM, bridgeAuthBase } from '../../auth-routes.js';
39
- import { parseBridgeBillingRoute, type BridgeBillingPage } from '../../billing-routes.js';
42
+ import { onDestroy, onMount } from 'svelte';
43
+ import { useBridge, type BillingSubscriptionSnapshot } from '@nebulr-group/bridge-auth-core';
44
+ import { billingRoutes, parseBridgeBillingRoute, type BridgeBillingPage } from '../../billing-routes.js';
45
+ import { clearCheckoutResult } from '../../checkout-result.js';
40
46
  import PlanSelector from './PlanSelector.svelte';
41
47
  import BridgeSubscriptionStatus from './BridgeSubscriptionStatus.svelte';
42
48
  import BillingPortalButton from './BillingPortalButton.svelte';
49
+ import BridgeCheckoutNotice from './BridgeCheckoutNotice.svelte';
50
+ import { subscriptionMeta } from './plan-format.js';
43
51
 
44
52
  interface Props {
45
53
  /** Everything around the content, on every page. Receives the page name and
@@ -49,15 +57,37 @@
49
57
  heading?: Snippet<[BridgeBillingPage]>;
50
58
  /** Where "Continue" on the success page goes. @default '/' */
51
59
  redirectTo?: string;
60
+ /** The "you're on Pro now" banner on the subscription page after a
61
+ * checkout. `false` hides it. @default true */
62
+ checkoutNotice?: boolean;
52
63
  }
53
64
 
54
- let { frame, heading, redirectTo = '/' }: Props = $props();
65
+ let { frame, heading, redirectTo = '/', checkoutNotice = true }: Props = $props();
55
66
 
56
67
  const rest = $derived($page.params[BRIDGE_AUTH_ROUTE_PARAM]);
57
68
  const route = $derived(parseBridgeBillingRoute(rest));
58
69
  // Links are relative to where the catch-all lives, not hard-coded to /subscription.
59
70
  const base = $derived(bridgeAuthBase($page.url.pathname, rest) || '/');
60
71
  const at = (sub: string) => (base === '/' ? `/${sub}` : `${base}/${sub}`);
72
+ // TBP-813 — a checkout lands on `billing.successRoute`; by default on this
73
+ // catch-all's own subscription page, wherever it lives.
74
+ const routes = billingRoutes();
75
+ const successTarget = $derived(routes.successIsDefault ? base : routes.successRoute);
76
+
77
+ // The current plan's renewal date and card, for the line under its name.
78
+ let snapshot = $state<BillingSubscriptionSnapshot | null>(null);
79
+ let unsubscribe: (() => void) | undefined;
80
+ onMount(() => {
81
+ try {
82
+ const sub = useBridge().subscription;
83
+ snapshot = sub.snapshot();
84
+ unsubscribe = sub.subscribe((s) => (snapshot = s));
85
+ } catch {
86
+ /* Bridge not initialised: no meta line */
87
+ }
88
+ });
89
+ onDestroy(() => unsubscribe?.());
90
+ const meta = $derived(subscriptionMeta(snapshot?.state));
61
91
 
62
92
  const DEFAULT_HEADINGS: Record<BridgeBillingPage, string> = {
63
93
  manage: 'Subscription',
@@ -77,11 +107,16 @@
77
107
  if (!current) return;
78
108
  untrack(() => {
79
109
  void (current === 'success' ? loadSubscription() : ensureSubscription());
110
+ // The old confirmation page says it all; no banner on the next page too.
111
+ if (current === 'success') clearCheckoutResult();
80
112
  });
81
113
  });
82
114
  </script>
83
115
 
84
116
  {#snippet body(p: BridgeBillingPage)}
117
+ {#if p === 'manage' && checkoutNotice}
118
+ <BridgeCheckoutNotice />
119
+ {/if}
85
120
  {#if heading}
86
121
  {@render heading(p)}
87
122
  {:else}
@@ -89,15 +124,21 @@
89
124
  {/if}
90
125
 
91
126
  {#if p === 'manage'}
92
- <div class="bridge-billing-current">
93
- <span class="bridge-billing-label">Current plan</span>
94
- <BridgeSubscriptionStatus />
127
+ <p class="bridge-billing-text">Your plan, what it includes, and billing.</p>
128
+ <div class="bridge-billing-summary" data-bridge-billing-summary>
129
+ <div class="bridge-billing-summary-plan">
130
+ <span class="bridge-billing-label">Current plan</span>
131
+ <BridgeSubscriptionStatus class="bridge-billing-summary-status" />
132
+ {#if meta}
133
+ <span class="bridge-billing-summary-meta" data-bridge-billing-meta>{meta}</span>
134
+ {/if}
135
+ </div>
95
136
  <BillingPortalButton />
96
137
  </div>
97
- <PlanSelector successRedirect={at('success')} cancelRedirect={base} />
138
+ <PlanSelector heading="Change plan" successRedirect={successTarget} cancelRedirect={base} />
98
139
  {:else if p === 'plan'}
99
140
  <p class="bridge-billing-text">Pick a plan to start using the app.</p>
100
- <PlanSelector successRedirect={at('success')} cancelRedirect={at('plan')} />
141
+ <PlanSelector successRedirect={successTarget} cancelRedirect={at('plan')} />
101
142
  {:else if p === 'success'}
102
143
  <p class="bridge-billing-text">Your plan is active.</p>
103
144
  <div class="bridge-billing-current">
@@ -152,6 +193,11 @@
152
193
  opacity: 0.8;
153
194
  }
154
195
 
196
+ /* The subtitle sits with its heading, closer than the page's gap. */
197
+ .bridge-billing-heading + .bridge-billing-text {
198
+ margin-top: -0.75rem;
199
+ }
200
+
155
201
  .bridge-billing-current {
156
202
  display: flex;
157
203
  flex-wrap: wrap;
@@ -159,6 +205,23 @@
159
205
  gap: 0.75rem;
160
206
  }
161
207
 
208
+ /* The current-plan panel's layout; its surface is in styles.css. */
209
+ .bridge-billing-summary {
210
+ display: flex;
211
+ flex-wrap: wrap;
212
+ align-items: center;
213
+ justify-content: space-between;
214
+ gap: 1rem;
215
+ }
216
+
217
+ .bridge-billing-summary-plan {
218
+ display: flex;
219
+ flex-direction: column;
220
+ align-items: flex-start;
221
+ gap: 0.25rem;
222
+ min-width: 0;
223
+ }
224
+
162
225
  .bridge-billing-label {
163
226
  font-size: 0.75rem;
164
227
  font-weight: 600;
@@ -8,6 +8,9 @@ interface Props {
8
8
  heading?: Snippet<[BridgeBillingPage]>;
9
9
  /** Where "Continue" on the success page goes. @default '/' */
10
10
  redirectTo?: string;
11
+ /** The "you're on Pro now" banner on the subscription page after a
12
+ * checkout. `false` hides it. @default true */
13
+ checkoutNotice?: boolean;
11
14
  }
12
15
  declare const BridgeBillingRoutes: import("svelte").Component<Props, {}, "">;
13
16
  type BridgeBillingRoutes = ReturnType<typeof BridgeBillingRoutes>;
@@ -0,0 +1,101 @@
1
+ <!--
2
+ TBP-813 — the "you're on Pro now" banner after a checkout.
3
+
4
+ A completed checkout (or a Free pick) lands on `billing.successRoute`, the
5
+ subscription page by default, which shows this banner itself. To land
6
+ somewhere else and still say so, put it on that page or in the layout:
7
+
8
+ <BridgeCheckoutNotice />
9
+
10
+ It renders nothing until a checkout has just completed, shows once, and is
11
+ gone after a reload. `children` replaces the text and receives the result;
12
+ for a message of your own anywhere else, read `checkoutResult`.
13
+ -->
14
+ <script lang="ts">
15
+ import type { Snippet } from 'svelte';
16
+ import { onDestroy, onMount } from 'svelte';
17
+ import { useBridge, type BillingSubscriptionSnapshot, type Plan } from '@nebulr-group/bridge-auth-core';
18
+ import { subscriptionStore } from '../../../core/bridge-instance.js';
19
+ import { checkoutResult, clearCheckoutResult, type BridgeCheckoutResult } from '../../checkout-result.js';
20
+
21
+ interface Props {
22
+ /** Optional class on the banner. */
23
+ class?: string;
24
+ /** Show a close button. @default true */
25
+ dismissible?: boolean;
26
+ /** Replaces the banner's text. Receives the result, with the plan name when known. */
27
+ children?: Snippet<[Omit<BridgeCheckoutResult, 'planName'> & { planName: string | null }]>;
28
+ }
29
+
30
+ let { class: className = '', dismissible = true, children }: Props = $props();
31
+
32
+ let shown = $state<BridgeCheckoutResult | null>(null);
33
+ let snapshot = $state<BillingSubscriptionSnapshot | null>(null);
34
+ const unsubs: Array<() => void> = [];
35
+
36
+ onMount(() => {
37
+ // Take the result as soon as it is set, so it shows once — including on a
38
+ // page that is already open when a Free pick lands back on it.
39
+ unsubs.push(
40
+ checkoutResult.subscribe((value) => {
41
+ if (!value) return;
42
+ shown = value;
43
+ queueMicrotask(clearCheckoutResult);
44
+ }),
45
+ );
46
+ try {
47
+ const sub = useBridge().subscription;
48
+ snapshot = sub.snapshot();
49
+ unsubs.push(sub.subscribe((s) => (snapshot = s)));
50
+ } catch {
51
+ /* Bridge not initialised: the banner goes without the plan name */
52
+ }
53
+ });
54
+
55
+ onDestroy(() => unsubs.forEach((u) => u()));
56
+
57
+ const planName = $derived.by<string | null>(() => {
58
+ if (shown?.planName) return shown.planName;
59
+ if (snapshot?.state?.plan.name) return snapshot.state.plan.name;
60
+ const plan = $subscriptionStore.status?.plan;
61
+ return plan && typeof plan === 'object' ? ((plan as Plan).name ?? null) : null;
62
+ });
63
+ </script>
64
+
65
+ {#if shown}
66
+ <div
67
+ class={`bridge-checkout-notice ${className}`}
68
+ data-bridge-checkout-notice
69
+ data-kind={shown.kind}
70
+ role="status"
71
+ >
72
+ <svg class="bridge-checkout-notice-icon" width="18" height="18" viewBox="0 0 20 20" fill="none" aria-hidden="true">
73
+ <circle cx="10" cy="10" r="9" stroke="currentColor" stroke-width="1.6" />
74
+ <path d="M6 10.2l2.6 2.6L14 7.4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
75
+ </svg>
76
+ <div class="bridge-checkout-notice-body">
77
+ {#if children}
78
+ {@render children({ ...shown, planName })}
79
+ {:else}
80
+ <strong class="bridge-checkout-notice-title">
81
+ {planName ? `You're on ${planName} now` : 'Your new plan is active'}
82
+ </strong>
83
+ <span class="bridge-checkout-notice-text">
84
+ {shown.kind === 'checkout'
85
+ ? 'Your payment went through and the new plan is active.'
86
+ : 'The change takes effect right away.'}
87
+ </span>
88
+ {/if}
89
+ </div>
90
+ {#if dismissible}
91
+ <button
92
+ type="button"
93
+ class="bridge-checkout-notice-close"
94
+ aria-label="Dismiss"
95
+ onclick={() => (shown = null)}
96
+ >
97
+ ×
98
+ </button>
99
+ {/if}
100
+ </div>
101
+ {/if}
@@ -0,0 +1,15 @@
1
+ import type { Snippet } from 'svelte';
2
+ import { type BridgeCheckoutResult } from '../../checkout-result.js';
3
+ interface Props {
4
+ /** Optional class on the banner. */
5
+ class?: string;
6
+ /** Show a close button. @default true */
7
+ dismissible?: boolean;
8
+ /** Replaces the banner's text. Receives the result, with the plan name when known. */
9
+ children?: Snippet<[Omit<BridgeCheckoutResult, 'planName'> & {
10
+ planName: string | null;
11
+ }]>;
12
+ }
13
+ declare const BridgeCheckoutNotice: import("svelte").Component<Props, {}, "">;
14
+ type BridgeCheckoutNotice = ReturnType<typeof BridgeCheckoutNotice>;
15
+ export default BridgeCheckoutNotice;
@@ -13,8 +13,8 @@
13
13
  page has ever been visited — that is what the config line is for. Mounted
14
14
  anywhere else, this page says so in the dev console.
15
15
 
16
- A completed checkout lands on the subscription success page
17
- (`<billing.manageRoute>/success`); a cancelled one comes back here.
16
+ A completed checkout lands on `billing.successRoute` (the subscription page,
17
+ with a confirmation banner, by default); a cancelled one comes back here.
18
18
  -->
19
19
  <script lang="ts">
20
20
  import type { Snippet } from 'svelte';
@@ -31,7 +31,7 @@
31
31
  heading?: string;
32
32
  /** Content between the heading and the plans — a welcome line, a checklist. */
33
33
  children?: Snippet;
34
- /** Where a completed checkout lands. @default `<billing.manageRoute>/success` */
34
+ /** Where a completed checkout lands. @default `billing.successRoute`, else the subscription page */
35
35
  successRedirect?: string;
36
36
  /** Where a cancelled checkout lands. @default this page */
37
37
  cancelRedirect?: string;
@@ -5,7 +5,7 @@ interface Props {
5
5
  heading?: string;
6
6
  /** Content between the heading and the plans — a welcome line, a checklist. */
7
7
  children?: Snippet;
8
- /** Where a completed checkout lands. @default `<billing.manageRoute>/success` */
8
+ /** Where a completed checkout lands. @default `billing.successRoute`, else the subscription page */
9
9
  successRedirect?: string;
10
10
  /** Where a cancelled checkout lands. @default this page */
11
11
  cancelRedirect?: string;
@@ -7,6 +7,8 @@
7
7
  import { ensureSubscription, getBridgeAuth, loadSubscription, subscriptionStore } from '../../../core/bridge-instance.js';
8
8
  import { refreshBilling } from '../../../core/billing-store.js';
9
9
  import { pickPlan } from './plan-pick.js';
10
+ import { formatAmount, intervalSuffix, minAmount, pickKind, pickLabel } from './plan-format.js';
11
+ import { markCheckoutComplete } from '../../checkout-result.js';
10
12
  import { getConfig } from '../../stores/config.store.js';
11
13
  import Alert from '../sdk-auth/shared/Alert.svelte';
12
14
  import Spinner from '../sdk-auth/shared/Spinner.svelte';
@@ -14,6 +16,8 @@
14
16
  type BillingInterval = PriceOfferSdk['recurrenceInterval'];
15
17
 
16
18
  interface Props extends HTMLAttributes<HTMLDivElement> {
19
+ /** A heading above the plans, beside the Monthly/Yearly switch (TBP-813). */
20
+ heading?: string;
17
21
  successRedirect?: string;
18
22
  cancelRedirect?: string;
19
23
  /**
@@ -36,6 +40,7 @@
36
40
  }
37
41
 
38
42
  let {
43
+ heading,
39
44
  successRedirect = '/subscription',
40
45
  cancelRedirect = '/subscription',
41
46
  defaultInterval = 'year',
@@ -135,11 +140,6 @@
135
140
  return (plan as Plan & { features?: ReadonlyArray<{ key: string; name: string }> }).features ?? [];
136
141
  }
137
142
 
138
- function minAmount(plan: Plan): number {
139
- const amounts = (plan.prices ?? []).map((p) => p.amount);
140
- return amounts.length > 0 ? Math.min(...amounts) : Number.POSITIVE_INFINITY;
141
- }
142
-
143
143
  type UiState = 'idle' | 'payment-failed' | 'setup-payments' | 'select-plan' | 'active' | 'trial';
144
144
 
145
145
  const uiState = $derived<UiState>((() => {
@@ -187,15 +187,13 @@
187
187
  let successNotice = $state<string | null>(null);
188
188
  let successTimer: ReturnType<typeof setTimeout> | undefined;
189
189
 
190
- const currentPlanName = $derived.by(() => {
191
- const found = (plans ?? []).find((p) => p.key === currentPlanKey);
192
- return found?.name ?? currentPlanKey ?? 'your current plan';
193
- });
190
+ const currentPlan = $derived((plans ?? []).find((p) => p.key === currentPlanKey) ?? null);
191
+ const currentPlanName = $derived(currentPlan?.name ?? currentPlanKey ?? 'your current plan');
194
192
 
195
193
  function formatPrice(price: PriceOfferSdk): string {
196
194
  return price.amount === 0
197
195
  ? 'Free'
198
- : `${price.amount} ${price.currency.toUpperCase()} / ${price.recurrenceInterval}`;
196
+ : `${formatAmount(price.amount, price.currency)} ${intervalSuffix(price)}`;
199
197
  }
200
198
 
201
199
  function showSuccess(message: string): void {
@@ -244,6 +242,7 @@
244
242
  leave: (url) => {
245
243
  window.location.href = url;
246
244
  },
245
+ complete: (picked) => markCheckoutComplete({ kind: 'plan', planName: picked.name }),
247
246
  },
248
247
  );
249
248
  if (outcome === 'confirm') {
@@ -320,7 +319,7 @@
320
319
  </div>
321
320
  {/if}
322
321
  {:else if plans}
323
- {#if showIntervalTabs}
322
+ {#snippet intervalTabs()}
324
323
  <div
325
324
  class="bridge-plan-interval-tabs"
326
325
  data-bridge-plan-interval-tabs
@@ -339,6 +338,15 @@
339
338
  </button>
340
339
  {/each}
341
340
  </div>
341
+ {/snippet}
342
+
343
+ {#if heading}
344
+ <div class="bridge-plan-head" data-bridge-plan-head>
345
+ <h2 class="bridge-plan-heading">{heading}</h2>
346
+ {#if showIntervalTabs}{@render intervalTabs()}{/if}
347
+ </div>
348
+ {:else if showIntervalTabs}
349
+ {@render intervalTabs()}
342
350
  {/if}
343
351
 
344
352
  <div class="bridge-plan-cards" data-bridge-plan-cards>
@@ -346,6 +354,7 @@
346
354
  {@const isCurrent = plan.key === currentPlanKey}
347
355
  {@const onPick = (price: PriceOfferSdk) => handlePick(plan, price)}
348
356
  {@const visiblePrices = pricesForInterval(plan)}
357
+ {@const kind = pickKind(plan, currentPlan)}
349
358
 
350
359
  {#if planCard}
351
360
  {@render planCard({ plan, prices: plan.prices, isCurrent, interval: selectedInterval, onPick })}
@@ -354,8 +363,12 @@
354
363
  data-bridge-plan-card
355
364
  data-current={isCurrent}
356
365
  data-trial={plan.trial}
366
+ data-pick={kind}
357
367
  class="bridge-plan-card"
358
368
  >
369
+ {#if isCurrent}
370
+ <span class="bridge-plan-current-flag" data-bridge-plan-current-flag>Your plan</span>
371
+ {/if}
359
372
  <div class="bridge-plan-card-header">
360
373
  <h3 class="bridge-plan-name">{plan.name}</h3>
361
374
  {#if plan.trial && (plan.trialDays ?? 0) > 0}
@@ -363,6 +376,13 @@
363
376
  {/if}
364
377
  </div>
365
378
 
379
+ {#if visiblePrices[0]}
380
+ <div class="bridge-plan-price" data-bridge-plan-price>
381
+ <span class="bridge-plan-price-amount">{formatAmount(visiblePrices[0].amount, visiblePrices[0].currency)}</span>
382
+ <span class="bridge-plan-price-interval">{intervalSuffix(visiblePrices[0])}</span>
383
+ </div>
384
+ {/if}
385
+
366
386
  {#if planDescription}
367
387
  {@render planDescription({ plan, isCurrent })}
368
388
  {:else if plan.description}
@@ -372,7 +392,12 @@
372
392
  {#if planFeatures(plan).length > 0}
373
393
  <ul class="bridge-plan-features" data-bridge-plan-features aria-label={`Included in ${plan.name}`}>
374
394
  {#each planFeatures(plan) as feature (feature.key)}
375
- <li class="bridge-plan-feature" data-feature={feature.key}>{feature.name}</li>
395
+ <li class="bridge-plan-feature" data-feature={feature.key}>
396
+ <svg class="bridge-plan-feature-check" width="14" height="14" viewBox="0 0 16 16" aria-hidden="true">
397
+ <path d="M3 8.5l3.2 3L13 4.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
398
+ </svg>
399
+ <span>{feature.name}</span>
400
+ </li>
376
401
  {/each}
377
402
  </ul>
378
403
  {/if}
@@ -380,27 +405,21 @@
380
405
  <div class="bridge-plan-prices">
381
406
  {#each visiblePrices as price (price.recurrenceInterval + price.currency)}
382
407
  <button
383
- class="bridge-btn-primary bridge-plan-select-btn"
408
+ class={`${kind === 'current' || kind === 'switch' ? 'bridge-btn-secondary' : 'bridge-btn-primary'} bridge-plan-select-btn`}
384
409
  disabled={isCurrent || picking}
385
410
  onclick={() => onPick(price)}
386
411
  >
387
- {#if isCurrent}
388
- Current plan
389
- {:else if price.amount === 0}
390
- Select free plan
391
- {:else}
392
- {price.amount} {price.currency.toUpperCase()} / {price.recurrenceInterval}
393
- {/if}
412
+ {pickLabel(kind, plan)}{#if visiblePrices.length > 1 && !isCurrent}{` · ${formatAmount(price.amount, price.currency)}`}{/if}
394
413
  </button>
395
414
  {/each}
396
415
 
397
416
  {#if plan.prices.length === 0}
398
417
  <button
399
- class="bridge-btn-primary bridge-plan-select-btn"
418
+ class={`${kind === 'current' || kind === 'switch' ? 'bridge-btn-secondary' : 'bridge-btn-primary'} bridge-plan-select-btn`}
400
419
  disabled={isCurrent || picking}
401
420
  onclick={() => handlePick(plan, { id: '', amount: 0, currency: 'usd', recurrenceInterval: 'month' })}
402
421
  >
403
- {isCurrent ? 'Current plan' : 'Select plan'}
422
+ {pickLabel(kind, plan)}
404
423
  </button>
405
424
  {:else if visiblePrices.length === 0}
406
425
  <p class="bridge-plan-unavailable" data-bridge-plan-unavailable>
@@ -3,6 +3,8 @@ import type { HTMLAttributes } from 'svelte/elements';
3
3
  import type { Plan, PriceOfferSdk } from '@nebulr-group/bridge-auth-core';
4
4
  type BillingInterval = PriceOfferSdk['recurrenceInterval'];
5
5
  interface Props extends HTMLAttributes<HTMLDivElement> {
6
+ /** A heading above the plans, beside the Monthly/Yearly switch (TBP-813). */
7
+ heading?: string;
6
8
  successRedirect?: string;
7
9
  cancelRedirect?: string;
8
10
  /**
@@ -0,0 +1,17 @@
1
+ import type { BillingSubscriptionState, Plan, PriceOfferSdk } from '@nebulr-group/bridge-auth-core';
2
+ /** `29` + `usd` → `$29`; `9.5` → `$9.50`. An unknown currency code falls back to `29 XYZ`. */
3
+ export declare function formatAmount(amount: number, currency: string, locale?: string): string;
4
+ /** The cheapest price a plan has in any interval; plans with none sort last. */
5
+ export declare function minAmount(plan: Plan): number;
6
+ /** What picking a plan does, relative to the current one. */
7
+ export type PickKind = 'current' | 'choose' | 'upgrade' | 'switch';
8
+ export declare function pickKind(plan: Plan, current: Plan | null | undefined): PickKind;
9
+ /** The button text for a pick. */
10
+ export declare function pickLabel(kind: PickKind, plan: Plan): string;
11
+ /** `/ month`, after a price. */
12
+ export declare function intervalSuffix(price: PriceOfferSdk): string;
13
+ /**
14
+ * The line under the current plan's name: renewal or end date, and the card on
15
+ * file. Only what the billing state actually carries; empty when it has none.
16
+ */
17
+ export declare function subscriptionMeta(state: BillingSubscriptionState | null | undefined, locale?: string): string;
@@ -0,0 +1,81 @@
1
+ // TBP-813 — how the plan cards and the current-plan panel word things, as pure
2
+ // functions. Prices are whole currency units (29 = 29 USD), as Bridge stores them.
3
+ /** `29` + `usd` → `$29`; `9.5` → `$9.50`. An unknown currency code falls back to `29 XYZ`. */
4
+ export function formatAmount(amount, currency, locale) {
5
+ const code = currency.toUpperCase();
6
+ try {
7
+ return new Intl.NumberFormat(locale, {
8
+ style: 'currency',
9
+ currency: code,
10
+ minimumFractionDigits: Number.isInteger(amount) ? 0 : 2,
11
+ maximumFractionDigits: 2,
12
+ }).format(amount);
13
+ }
14
+ catch {
15
+ return `${amount} ${code}`;
16
+ }
17
+ }
18
+ /** The cheapest price a plan has in any interval; plans with none sort last. */
19
+ export function minAmount(plan) {
20
+ const amounts = (plan.prices ?? []).map((p) => p.amount);
21
+ return amounts.length > 0 ? Math.min(...amounts) : Number.POSITIVE_INFINITY;
22
+ }
23
+ export function pickKind(plan, current) {
24
+ if (current && current.key === plan.key)
25
+ return 'current';
26
+ if (!current)
27
+ return 'choose';
28
+ return minAmount(plan) > minAmount(current) ? 'upgrade' : 'switch';
29
+ }
30
+ /** The button text for a pick. */
31
+ export function pickLabel(kind, plan) {
32
+ switch (kind) {
33
+ case 'current':
34
+ return 'Current plan';
35
+ case 'upgrade':
36
+ return `Upgrade to ${plan.name}`;
37
+ case 'switch':
38
+ return `Switch to ${plan.name}`;
39
+ default:
40
+ return `Choose ${plan.name}`;
41
+ }
42
+ }
43
+ /** `/ month`, after a price. */
44
+ export function intervalSuffix(price) {
45
+ return `/ ${price.recurrenceInterval}`;
46
+ }
47
+ function formatDate(iso, locale) {
48
+ if (!iso)
49
+ return null;
50
+ const d = new Date(iso);
51
+ if (Number.isNaN(d.getTime()))
52
+ return null;
53
+ return d.toLocaleDateString(locale, { year: 'numeric', month: 'short', day: 'numeric' });
54
+ }
55
+ /**
56
+ * The line under the current plan's name: renewal or end date, and the card on
57
+ * file. Only what the billing state actually carries; empty when it has none.
58
+ */
59
+ export function subscriptionMeta(state, locale) {
60
+ if (!state)
61
+ return '';
62
+ const parts = [];
63
+ const renews = formatDate(state.renewsAt, locale);
64
+ const ends = formatDate(state.endsAt, locale);
65
+ if (state.status === 'trial') {
66
+ if (ends)
67
+ parts.push(`Trial ends ${ends}`);
68
+ else if (typeof state.daysLeft === 'number')
69
+ parts.push(`${state.daysLeft} days left in trial`);
70
+ }
71
+ else if (state.status === 'cancel_at_period_end' || state.status === 'canceled') {
72
+ if (ends)
73
+ parts.push(`Ends ${ends}`);
74
+ }
75
+ else if (renews) {
76
+ parts.push(`Renews ${renews}`);
77
+ }
78
+ if (state.cardLast4)
79
+ parts.push(`Card ending ${state.cardLast4}`);
80
+ return parts.join(' · ');
81
+ }
@@ -29,6 +29,8 @@ export interface PlanPickDeps {
29
29
  navigate: (url: string) => unknown;
30
30
  /** Leave the app (Stripe checkout). */
31
31
  leave: (url: string) => void;
32
+ /** TBP-813 — record that the plan changed, for the landing page's banner. */
33
+ complete?: (plan: Plan) => void;
32
34
  }
33
35
  /** `selected`: the plan changed here. `confirm`: ask first. `checkout`: off to Stripe. */
34
36
  export type PlanPickOutcome = 'selected' | 'confirm' | 'checkout';
@@ -5,7 +5,8 @@
5
5
  // the page passed `onSelect`: the person was left on the plan picker, looking
6
6
  // as if nothing had happened. It now goes on exactly like a paid pick does —
7
7
  // to `successRedirect`, where a Stripe checkout returns — unless the page took
8
- // over with `onSelect`. Every pick that changed the plan re-reads the billing
8
+ // over with `onSelect`. TBP-813: it also records the pick, so the landing page
9
+ // shows the same confirmation banner a checkout gets. Every pick that changed the plan re-reads the billing
9
10
  // store (plan list, current plan, billing state) first, so the next page shows
10
11
  // the new plan.
11
12
  /** A $0 price on a plan with no metered cost is picked without checkout. */
@@ -43,5 +44,6 @@ async function settle(plan, price, opts, deps) {
43
44
  opts.onSelect({ plan, price });
44
45
  return;
45
46
  }
47
+ deps.complete?.(plan);
46
48
  await deps.navigate(opts.successRedirect);
47
49
  }
package/dist/index.d.ts CHANGED
@@ -53,6 +53,9 @@ export { default as PlanSelector } from './client/components/subscription/PlanSe
53
53
  export { default as BridgeBillingRoutes } from './client/components/subscription/BridgeBillingRoutes.svelte';
54
54
  export { default as BridgePaywallPage } from './client/components/subscription/BridgePaywallPage.svelte';
55
55
  export { default as BillingPortalButton } from './client/components/subscription/BillingPortalButton.svelte';
56
+ export { default as BridgeCheckoutNotice } from './client/components/subscription/BridgeCheckoutNotice.svelte';
57
+ export { checkoutResult, takeCheckoutResult, clearCheckoutResult } from './client/checkout-result.js';
58
+ export type { BridgeCheckoutResult } from './client/checkout-result.js';
56
59
  export { BRIDGE_BILLING_PAGES, BRIDGE_BILLING_DEFAULTS, parseBridgeBillingRoute } from './client/billing-routes.js';
57
60
  export type { BridgeBillingPage, BridgeBillingRoute } from './client/billing-routes.js';
58
61
  export { default as BridgeSubscriptionStatus } from './client/components/subscription/BridgeSubscriptionStatus.svelte';
package/dist/index.js CHANGED
@@ -78,6 +78,10 @@ export { default as PlanSelector } from './client/components/subscription/PlanSe
78
78
  export { default as BridgeBillingRoutes } from './client/components/subscription/BridgeBillingRoutes.svelte';
79
79
  export { default as BridgePaywallPage } from './client/components/subscription/BridgePaywallPage.svelte';
80
80
  export { default as BillingPortalButton } from './client/components/subscription/BillingPortalButton.svelte';
81
+ // TBP-813 — a completed checkout lands on `billing.successRoute` (the
82
+ // subscription page by default); this banner, or `checkoutResult`, says so.
83
+ export { default as BridgeCheckoutNotice } from './client/components/subscription/BridgeCheckoutNotice.svelte';
84
+ export { checkoutResult, takeCheckoutResult, clearCheckoutResult } from './client/checkout-result.js';
81
85
  export { BRIDGE_BILLING_PAGES, BRIDGE_BILLING_DEFAULTS, parseBridgeBillingRoute } from './client/billing-routes.js';
82
86
  // Billing 2.0 (Phase A / US-2) — canonical-model drop-in.
83
87
  // Parallel to PlanSelector (which consumes the Stripe-direct path); these
@@ -38,10 +38,17 @@ export interface BridgeConfig extends BridgeAuthConfig {
38
38
  * @default '/subscription/error' */
39
39
  paymentErrorRoute?: string;
40
40
  /** The subscription page — the default destination of the Upgrade/Manage
41
- * CTA in <BridgeQuotaBanner> and <BridgeBillingNotice>. A completed
42
- * checkout lands on `<manageRoute>/success` by default.
41
+ * CTA in <BridgeQuotaBanner> and <BridgeBillingNotice>, and where a
42
+ * completed checkout lands unless `successRoute` says otherwise.
43
43
  * @default '/subscription' */
44
44
  manageRoute?: string;
45
+ /** Where a completed checkout, or a Free plan pick, lands: any page of your
46
+ * app, e.g. `'/dashboard'`. The subscription page shows a confirmation
47
+ * banner; anywhere else, render `<BridgeCheckoutNotice />` for the same
48
+ * banner, or read `checkoutResult` for your own message. The old
49
+ * confirmation page is `'/subscription/success'`.
50
+ * @default the subscription page (`manageRoute`) */
51
+ successRoute?: string;
45
52
  /** The dialog `<BridgeBootstrap />` opens when your backend refuses a
46
53
  * request because a plan limit is reached — a `402` whose JSON body has
47
54
  * `code: 'QUOTA_EXCEEDED'`, which bridge-nestjs's `@RequireQuota` sends
package/dist/styles.css CHANGED
@@ -30,6 +30,8 @@
30
30
  * --bridge-muted-bg subtle fills: tab tracks, hovers, code boxes
31
31
  * --bridge-border input, table, card and secondary button borders
32
32
  * --bridge-border-radius corners of inputs, buttons, alerts, cards, dialogs
33
+ * --bridge-card-bg plan cards and the current-plan panel (defaults to --bridge-bg)
34
+ * --bridge-card-shadow their shadow; `none` for a flat look (dark apps often want it)
33
35
  * --bridge-overlay backdrop behind dialogs
34
36
  * --bridge-alert-error-{bg,fg,border} errors, critical billing notices
35
37
  * --bridge-alert-success-{bg,fg,border} confirmations, active plan badge
@@ -1044,6 +1046,26 @@
1044
1046
  margin-top: 0.75rem;
1045
1047
  }
1046
1048
 
1049
+ /* TBP-813 — a heading beside the interval switch (`heading` prop). */
1050
+ .bridge-plan-head {
1051
+ display: flex;
1052
+ flex-wrap: wrap;
1053
+ align-items: center;
1054
+ justify-content: space-between;
1055
+ gap: 0.75rem;
1056
+ margin-bottom: 1.25rem;
1057
+ }
1058
+
1059
+ .bridge-plan-heading {
1060
+ margin: 0;
1061
+ font-size: 1.05rem;
1062
+ font-weight: 600;
1063
+ }
1064
+
1065
+ .bridge-plan-head .bridge-plan-interval-tabs {
1066
+ margin: 0;
1067
+ }
1068
+
1047
1069
  .bridge-plan-interval-tabs {
1048
1070
  /* Centered above the plan cards; flex + fit-content (not inline-flex) so
1049
1071
  auto margins can center it regardless of the host page's text alignment. */
@@ -1055,7 +1077,7 @@
1055
1077
  padding: 0.25rem;
1056
1078
  border: 1px solid var(--bridge-border);
1057
1079
  border-radius: var(--bridge-border-radius);
1058
- background: var(--bridge-muted-bg, var(--bridge-bg-muted, #f3f4f6));
1080
+ background: var(--bridge-card-bg, var(--bridge-bg));
1059
1081
  }
1060
1082
 
1061
1083
  .bridge-plan-interval-tab {
@@ -1137,19 +1159,61 @@
1137
1159
 
1138
1160
  .bridge-plan-cards {
1139
1161
  display: grid;
1140
- grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
1162
+ grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
1141
1163
  gap: 1rem;
1164
+ align-items: stretch;
1165
+ }
1166
+
1167
+ /* TBP-813 — cards, and the current-plan panel, are surfaces of their own, so
1168
+ they read on any app background and take the app's tokens. */
1169
+ [data-bridge-plan-card],
1170
+ .bridge-billing-summary {
1171
+ background: var(--bridge-card-bg, var(--bridge-bg));
1172
+ color: var(--bridge-foreground);
1173
+ border: 1px solid var(--bridge-border);
1174
+ border-radius: calc(var(--bridge-border-radius) * 1.5);
1175
+ box-shadow: var(--bridge-card-shadow, 0 1px 2px rgb(16 24 40 / 0.06), 0 1px 3px rgb(16 24 40 / 0.04));
1176
+ }
1177
+
1178
+ .bridge-billing-summary {
1179
+ padding: 1.1rem 1.25rem;
1180
+ background: color-mix(in srgb, var(--bridge-primary) 6%, var(--bridge-card-bg, var(--bridge-bg)));
1181
+ }
1182
+
1183
+ .bridge-billing-summary .bss-plan {
1184
+ font-size: 1.2rem;
1185
+ font-weight: 700;
1186
+ }
1187
+
1188
+ .bridge-billing-summary-meta {
1189
+ font-size: 0.85rem;
1190
+ color: var(--bridge-muted);
1191
+ font-variant-numeric: tabular-nums;
1142
1192
  }
1143
1193
 
1144
1194
  [data-bridge-plan-card] {
1145
- padding: 1.25rem;
1195
+ position: relative;
1196
+ padding: 1.35rem 1.25rem 1.25rem;
1146
1197
  display: flex;
1147
1198
  flex-direction: column;
1148
- gap: 0.75rem;
1199
+ gap: 0.9rem;
1149
1200
  }
1150
1201
 
1151
1202
  [data-bridge-plan-card][data-current="true"] {
1152
- outline: 2px solid currentColor;
1203
+ border: 2px solid var(--bridge-primary);
1204
+ padding: calc(1.35rem - 1px) calc(1.25rem - 1px) calc(1.25rem - 1px);
1205
+ }
1206
+
1207
+ .bridge-plan-current-flag {
1208
+ position: absolute;
1209
+ top: -0.7rem;
1210
+ left: 1.1rem;
1211
+ padding: 0.15rem 0.55rem;
1212
+ border-radius: 9999px;
1213
+ font-size: 0.7rem;
1214
+ font-weight: 600;
1215
+ background: var(--bridge-primary);
1216
+ color: var(--bridge-primary-fg, var(--bridge-primary-foreground, #ffffff));
1153
1217
  }
1154
1218
 
1155
1219
  .bridge-plan-card-header {
@@ -1169,22 +1233,62 @@
1169
1233
  display: inline-block;
1170
1234
  padding: 0.125rem 0.5rem;
1171
1235
  font-size: 0.7rem;
1236
+ font-weight: 600;
1172
1237
  white-space: nowrap;
1238
+ border-radius: 9999px;
1239
+ border: 1px solid var(--bridge-alert-info-border);
1240
+ background: var(--bridge-alert-info-bg);
1241
+ color: var(--bridge-alert-info-fg);
1242
+ }
1243
+
1244
+ .bridge-plan-price {
1245
+ display: flex;
1246
+ align-items: baseline;
1247
+ flex-wrap: wrap;
1248
+ gap: 0.3rem;
1249
+ font-variant-numeric: tabular-nums;
1250
+ }
1251
+
1252
+ .bridge-plan-price-amount {
1253
+ font-size: 1.9rem;
1254
+ font-weight: 700;
1255
+ letter-spacing: -0.02em;
1256
+ line-height: 1.1;
1257
+ }
1258
+
1259
+ .bridge-plan-price-interval {
1260
+ font-size: 0.85rem;
1261
+ color: var(--bridge-muted);
1173
1262
  }
1174
1263
 
1175
1264
  .bridge-plan-description {
1176
1265
  margin: 0;
1177
1266
  font-size: 0.875rem;
1267
+ color: var(--bridge-muted);
1178
1268
  }
1179
1269
 
1180
1270
  /* TBP-755 — the features a plan includes. */
1181
1271
  .bridge-plan-features {
1182
1272
  margin: 0;
1183
- padding-left: 1.125rem;
1273
+ padding: 0.9rem 0 0;
1274
+ border-top: 1px solid var(--bridge-border);
1275
+ list-style: none;
1184
1276
  font-size: 0.875rem;
1185
1277
  display: flex;
1186
1278
  flex-direction: column;
1187
- gap: 0.25rem;
1279
+ gap: 0.45rem;
1280
+ }
1281
+
1282
+ .bridge-plan-feature {
1283
+ display: flex;
1284
+ align-items: flex-start;
1285
+ gap: 0.5rem;
1286
+ }
1287
+
1288
+ .bridge-plan-feature-check {
1289
+ flex: none;
1290
+ margin-top: 0.2rem;
1291
+ color: var(--bridge-primary);
1188
1292
  }
1189
1293
 
1190
1294
  .bridge-plan-prices {
@@ -1199,6 +1303,59 @@
1199
1303
  text-align: center;
1200
1304
  }
1201
1305
 
1306
+ [data-bridge-plan-card][data-current="true"] .bridge-plan-select-btn:disabled {
1307
+ background: transparent;
1308
+ color: var(--bridge-muted);
1309
+ opacity: 1;
1310
+ }
1311
+
1312
+ /* ─── BridgeCheckoutNotice (TBP-813) ─────────────────────────────────────── */
1313
+
1314
+ .bridge-checkout-notice {
1315
+ display: flex;
1316
+ align-items: flex-start;
1317
+ gap: 0.75rem;
1318
+ padding: 0.85rem 1rem;
1319
+ border: 1px solid var(--bridge-alert-success-border);
1320
+ border-radius: var(--bridge-border-radius);
1321
+ background: var(--bridge-alert-success-bg);
1322
+ color: var(--bridge-alert-success-fg);
1323
+ font-size: 0.875rem;
1324
+ }
1325
+
1326
+ .bridge-checkout-notice-icon {
1327
+ flex: none;
1328
+ margin-top: 0.1rem;
1329
+ }
1330
+
1331
+ .bridge-checkout-notice-body {
1332
+ flex: 1;
1333
+ min-width: 0;
1334
+ }
1335
+
1336
+ .bridge-checkout-notice-title {
1337
+ display: block;
1338
+ font-weight: 600;
1339
+ }
1340
+
1341
+ .bridge-checkout-notice-close {
1342
+ appearance: none;
1343
+ border: 0;
1344
+ background: none;
1345
+ color: inherit;
1346
+ cursor: pointer;
1347
+ font: inherit;
1348
+ font-size: 1.15rem;
1349
+ line-height: 1;
1350
+ padding: 0 0.2rem;
1351
+ opacity: 0.7;
1352
+ }
1353
+
1354
+ .bridge-checkout-notice-close:hover,
1355
+ .bridge-checkout-notice-close:focus-visible {
1356
+ opacity: 1;
1357
+ }
1358
+
1202
1359
  /* ─── WorkspaceSelector ─────────────────────────────────────────────── */
1203
1360
 
1204
1361
  [data-bridge-workspace-selector] {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nebulr-group/bridge-svelte",
3
- "version": "0.9.1",
3
+ "version": "0.9.2-beta.0",
4
4
  "description": "The Bridge for SvelteKit: sign-in, workspaces and roles, feature flags, Stripe subscriptions and plan limits in one .env line and three files.",
5
5
  "author": "Iman Pouya",
6
6
  "license": "MIT",