@nebulr-group/bridge-svelte 0.3.2 → 0.4.0-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.
- package/dist/client/BridgeBootstrap.d.ts +1 -1
- package/dist/client/BridgeBootstrap.js +104 -27
- package/dist/client/BridgeBootstrap.svelte +65 -4
- package/dist/client/BridgeBootstrap.svelte.d.ts +2 -0
- package/dist/client/BridgeProvider.svelte +31 -0
- package/dist/client/BridgeProvider.svelte.d.ts +8 -0
- package/dist/client/components/developer/ApiTokenManagement.svelte +1 -1
- package/dist/client/components/sdk-auth/LoginForm.svelte +10 -7
- package/dist/client/components/sdk-auth/LoginForm.svelte.d.ts +1 -1
- package/dist/client/components/sdk-auth/PasskeyLogin.svelte +1 -1
- package/dist/client/components/sdk-auth/PasskeySetup.svelte +1 -1
- package/dist/client/components/sdk-auth/SsoButton.svelte +1 -1
- package/dist/client/components/sdk-auth/SsoButton.svelte.d.ts +1 -1
- package/dist/client/components/sdk-auth/TenantSelector.svelte +1 -1
- package/dist/client/components/sdk-auth/TenantSelector.svelte.d.ts +1 -1
- package/dist/client/components/sdk-auth/WorkspaceSelector.svelte +3 -3
- package/dist/client/components/sdk-auth/WorkspaceSelector.svelte.d.ts +3 -3
- package/dist/client/components/subscription/BridgeBillingNotice.svelte +389 -0
- package/dist/client/components/subscription/BridgeBillingNotice.svelte.d.ts +19 -0
- package/dist/client/components/subscription/BridgePaywall.svelte +94 -0
- package/dist/client/components/subscription/BridgePaywall.svelte.d.ts +19 -0
- package/dist/client/components/subscription/BridgeQuotaBanner.svelte +275 -0
- package/dist/client/components/subscription/BridgeQuotaBanner.svelte.d.ts +24 -0
- package/dist/client/components/subscription/BridgeSubscriptionStatus.svelte +116 -0
- package/dist/client/components/subscription/BridgeSubscriptionStatus.svelte.d.ts +7 -0
- package/dist/client/components/subscription/PlanSelector.svelte +22 -14
- package/dist/client/components/subscription/PlanSelector.svelte.d.ts +3 -3
- package/dist/client/components/subscription/billing-notice-gate.test.d.ts +1 -0
- package/dist/client/components/subscription/billing-notice-gate.test.js +83 -0
- package/dist/client/components/team/TeamAddUserDialog.svelte +1 -1
- package/dist/client/components/team/TeamAddUserDialog.svelte.d.ts +1 -1
- package/dist/client/components/team/TeamEditUserDialog.svelte +1 -1
- package/dist/client/components/team/TeamEditUserDialog.svelte.d.ts +1 -1
- package/dist/client/components/team/TeamProfileForm.svelte +1 -1
- package/dist/client/components/team/TeamUserList.svelte +1 -1
- package/dist/client/components/team/TeamWorkspaceForm.svelte +1 -1
- package/dist/client/tracking/pii-hashing.spec.js +4 -14
- package/dist/core/bridge-instance.d.ts +1 -2
- package/dist/core/bridge-instance.js +0 -4
- package/dist/core/bridge-runtime.d.ts +101 -0
- package/dist/core/bridge-runtime.js +290 -0
- package/dist/core/bridge-runtime.test.d.ts +1 -0
- package/dist/core/bridge-runtime.test.js +203 -0
- package/dist/core/bridge.d.ts +93 -0
- package/dist/core/bridge.js +108 -0
- package/dist/core/bridge.test.d.ts +1 -0
- package/dist/core/bridge.test.js +224 -0
- package/dist/core/events.d.ts +74 -0
- package/dist/core/events.js +64 -0
- package/dist/core/events.test.d.ts +1 -0
- package/dist/core/events.test.js +116 -0
- package/dist/core/lazy-slice.d.ts +74 -0
- package/dist/core/lazy-slice.js +124 -0
- package/dist/core/lazy-slice.test.d.ts +1 -0
- package/dist/core/lazy-slice.test.js +144 -0
- package/dist/core/realtime-status.d.ts +6 -0
- package/dist/core/realtime-status.js +15 -0
- package/dist/core/snapshot-stores.d.ts +69 -0
- package/dist/core/snapshot-stores.js +64 -0
- package/dist/core/use-bridge.d.ts +13 -0
- package/dist/core/use-bridge.js +46 -0
- package/dist/core/use-bridge.test.d.ts +1 -0
- package/dist/core/use-bridge.test.js +24 -0
- package/dist/flags/FeatureFlag.svelte +57 -0
- package/dist/flags/FeatureFlag.svelte.d.ts +37 -0
- package/dist/flags/bootstrap.d.ts +72 -0
- package/dist/flags/bootstrap.js +323 -0
- package/dist/flags/bootstrap.test.d.ts +1 -0
- package/dist/flags/bootstrap.test.js +195 -0
- package/dist/flags/flag.svelte.d.ts +31 -0
- package/dist/flags/flag.svelte.js +99 -0
- package/dist/flags/index.d.ts +8 -0
- package/dist/flags/index.js +20 -0
- package/dist/flags/index.test.d.ts +1 -0
- package/dist/flags/index.test.js +81 -0
- package/dist/flags/realtime-status.d.ts +1 -0
- package/dist/flags/realtime-status.js +4 -0
- package/dist/flags/registry.d.ts +27 -0
- package/dist/flags/registry.js +63 -0
- package/dist/flags/us13-integration.test.d.ts +1 -0
- package/dist/flags/us13-integration.test.js +168 -0
- package/dist/index.d.ts +14 -1
- package/dist/index.js +31 -2
- package/dist/styles.d.ts +2 -0
- package/package.json +92 -86
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Billing 2.0 / Phase B / US-5 → US-9 — unified billing-notice component.
|
|
3
|
+
|
|
4
|
+
Drop-in component that renders the workspace's current billing notice based
|
|
5
|
+
on `useBridge().subscription`. Designed for multi-state from the start:
|
|
6
|
+
US-5 implements the `past_due` paths; US-6 adds cancellation; US-8 adds
|
|
7
|
+
trial states; US-9 adds dunning + locked. New states slot in via the
|
|
8
|
+
notice-state -> content map without restructuring.
|
|
9
|
+
|
|
10
|
+
Audience: reads the existing auth context to render admin (CTA) vs member
|
|
11
|
+
(no CTA) variants. Per locked decision, role lives where it lives today —
|
|
12
|
+
NOT on useBridge().subscription.
|
|
13
|
+
|
|
14
|
+
Not dismissible — stays until status flips back to active.
|
|
15
|
+
-->
|
|
16
|
+
<script lang="ts">
|
|
17
|
+
import { onDestroy, onMount } from 'svelte';
|
|
18
|
+
import {
|
|
19
|
+
deriveNoticeState,
|
|
20
|
+
deriveSeverity,
|
|
21
|
+
useBridge,
|
|
22
|
+
type BillingNoticeState,
|
|
23
|
+
type BillingSeverity,
|
|
24
|
+
type BillingSubscriptionSnapshot,
|
|
25
|
+
} from '@nebulr-group/bridge-auth-core';
|
|
26
|
+
import { getBridgeAuth } from '../../../core/bridge-instance.js';
|
|
27
|
+
|
|
28
|
+
type Chassis = 'bar' | 'rail' | 'card';
|
|
29
|
+
|
|
30
|
+
interface Props {
|
|
31
|
+
/** Visual chassis variant. Per `deliverables.html §01`. */
|
|
32
|
+
chassis?: Chassis;
|
|
33
|
+
/**
|
|
34
|
+
* Gate behavior. `soft` (default) always renders the inline banner. `hard`
|
|
35
|
+
* renders a full-screen blocking lockscreen when the workspace is locked
|
|
36
|
+
* (gate engaged); non-locked states still render as the inline banner.
|
|
37
|
+
*/
|
|
38
|
+
mode?: 'soft' | 'hard';
|
|
39
|
+
/** Optional class applied to the root element. */
|
|
40
|
+
class?: string;
|
|
41
|
+
/** Override the default CTA click handler (links to billing surface). */
|
|
42
|
+
onActionClick?: (state: BillingNoticeState) => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let {
|
|
46
|
+
chassis = 'rail',
|
|
47
|
+
mode = 'soft',
|
|
48
|
+
class: className = '',
|
|
49
|
+
onActionClick,
|
|
50
|
+
}: Props = $props();
|
|
51
|
+
|
|
52
|
+
let snapshot = $state<BillingSubscriptionSnapshot>(useBridge().subscription.snapshot());
|
|
53
|
+
let unsubscribe: (() => void) | undefined;
|
|
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.
|
|
58
|
+
let isBillingAdmin = $state(false);
|
|
59
|
+
|
|
60
|
+
onMount(() => {
|
|
61
|
+
unsubscribe = useBridge().subscription.subscribe((snap) => {
|
|
62
|
+
snapshot = snap;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const auth = getBridgeAuth();
|
|
66
|
+
const ctx = auth.getApiContext();
|
|
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;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!ctx.accessToken) {
|
|
80
|
+
useBridge().subscription.setError('Not authenticated');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!snapshot.state) {
|
|
84
|
+
useBridge().subscription.mount({
|
|
85
|
+
apiBaseUrl: ctx.apiBaseUrl,
|
|
86
|
+
accessToken: ctx.accessToken,
|
|
87
|
+
appId: ctx.appId,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
onDestroy(() => unsubscribe?.());
|
|
93
|
+
|
|
94
|
+
const noticeState = $derived<BillingNoticeState>(deriveNoticeState(snapshot.state));
|
|
95
|
+
const severity = $derived<BillingSeverity>(deriveSeverity(noticeState));
|
|
96
|
+
const visible = $derived(snapshot.state !== null && noticeState !== 'active');
|
|
97
|
+
const asLockscreen = $derived(mode === 'hard' && severity === 'locked');
|
|
98
|
+
|
|
99
|
+
// Copy map per `deliverables.html §06`. US-5 ships past_due copy; later
|
|
100
|
+
// stories fill in trial/cancel/dunning variants. Member variant suppresses
|
|
101
|
+
// the CTA and softens the call-to-action.
|
|
102
|
+
function getCopy(state: BillingNoticeState, admin: boolean): { title: string; body: string; cta?: string } {
|
|
103
|
+
const cardLast4 = snapshot.state?.cardLast4;
|
|
104
|
+
const endsAt = snapshot.state?.endsAt;
|
|
105
|
+
const daysLeft = snapshot.state?.daysLeft;
|
|
106
|
+
const nextRetryAt = snapshot.state?.nextRetryAt;
|
|
107
|
+
|
|
108
|
+
switch (state) {
|
|
109
|
+
case 'past_due':
|
|
110
|
+
return admin
|
|
111
|
+
? {
|
|
112
|
+
title: 'Payment failed',
|
|
113
|
+
body: cardLast4
|
|
114
|
+
? `We couldn't charge your card ending in ${cardLast4}. Update your payment method to keep using ${snapshot.state?.plan.name ?? 'your plan'}.`
|
|
115
|
+
: `We couldn't charge your card. Update your payment method to keep using ${snapshot.state?.plan.name ?? 'your plan'}.`,
|
|
116
|
+
cta: 'Update card',
|
|
117
|
+
}
|
|
118
|
+
: {
|
|
119
|
+
title: "Your workspace's payment failed",
|
|
120
|
+
body: 'Please contact your workspace owner to update the payment method.',
|
|
121
|
+
};
|
|
122
|
+
case 'past_due_trial':
|
|
123
|
+
return admin
|
|
124
|
+
? {
|
|
125
|
+
title: 'Trial ended',
|
|
126
|
+
body: 'Add a payment method to keep using your workspace.',
|
|
127
|
+
cta: 'Add card',
|
|
128
|
+
}
|
|
129
|
+
: {
|
|
130
|
+
title: "Your workspace's trial has ended",
|
|
131
|
+
body: 'Please contact your workspace owner to add a payment method.',
|
|
132
|
+
};
|
|
133
|
+
case 'trial_active':
|
|
134
|
+
return admin
|
|
135
|
+
? {
|
|
136
|
+
title: 'Trial active',
|
|
137
|
+
body: daysLeft !== undefined ? `${daysLeft} days left in your trial.` : 'Trial in progress.',
|
|
138
|
+
}
|
|
139
|
+
: {
|
|
140
|
+
title: 'Trial active',
|
|
141
|
+
body: daysLeft !== undefined ? `${daysLeft} days left.` : 'Trial in progress.',
|
|
142
|
+
};
|
|
143
|
+
case 'trial_ending_soon':
|
|
144
|
+
return admin
|
|
145
|
+
? {
|
|
146
|
+
title: 'Trial ending soon',
|
|
147
|
+
body: daysLeft !== undefined ? `${daysLeft} days left. Add a payment method to keep your access.` : 'Add a payment method to keep your access.',
|
|
148
|
+
cta: 'Add card',
|
|
149
|
+
}
|
|
150
|
+
: {
|
|
151
|
+
title: 'Trial ending soon',
|
|
152
|
+
body: daysLeft !== undefined ? `${daysLeft} days left.` : 'Contact your workspace owner.',
|
|
153
|
+
};
|
|
154
|
+
case 'cancel_at_period_end':
|
|
155
|
+
return admin
|
|
156
|
+
? {
|
|
157
|
+
title: 'Subscription ending',
|
|
158
|
+
body: endsAt
|
|
159
|
+
? `Your subscription ends ${new Date(endsAt).toLocaleDateString()}. You'll keep full access until then.`
|
|
160
|
+
: "Your subscription is ending. You'll keep access until the period ends.",
|
|
161
|
+
cta: 'Reactivate',
|
|
162
|
+
}
|
|
163
|
+
: {
|
|
164
|
+
title: 'Subscription ending',
|
|
165
|
+
body: endsAt ? `Your workspace's subscription ends ${new Date(endsAt).toLocaleDateString()}.` : "Your workspace's subscription is ending.",
|
|
166
|
+
};
|
|
167
|
+
case 'canceled':
|
|
168
|
+
return admin
|
|
169
|
+
? { title: 'Subscription canceled', body: 'Your subscription has ended. Choose a plan to continue.', cta: 'Choose plan' }
|
|
170
|
+
: { title: 'Subscription canceled', body: 'Please contact your workspace owner.' };
|
|
171
|
+
case 'dunning_active':
|
|
172
|
+
return admin
|
|
173
|
+
? {
|
|
174
|
+
title: 'Payment retry scheduled',
|
|
175
|
+
body: nextRetryAt
|
|
176
|
+
? `We'll retry your payment on ${new Date(nextRetryAt).toLocaleDateString()}. Update your card to avoid interruption.`
|
|
177
|
+
: "We'll retry your payment soon. Update your card to avoid interruption.",
|
|
178
|
+
cta: 'Update card',
|
|
179
|
+
}
|
|
180
|
+
: { title: 'Payment retry scheduled', body: 'Please contact your workspace owner to update the payment method.' };
|
|
181
|
+
case 'dunning_final_retry':
|
|
182
|
+
return admin
|
|
183
|
+
? {
|
|
184
|
+
title: 'Final payment retry',
|
|
185
|
+
body: 'This is the last automatic retry. Update your card now to avoid losing access.',
|
|
186
|
+
cta: 'Update card',
|
|
187
|
+
}
|
|
188
|
+
: { title: 'Final payment retry', body: 'Please contact your workspace owner immediately.' };
|
|
189
|
+
case 'dunning_exhausted':
|
|
190
|
+
return admin
|
|
191
|
+
? { title: 'Access locked', body: 'Payment retries have exhausted. Update your card to restore access.', cta: 'Update card' }
|
|
192
|
+
: { title: 'Access locked', body: 'Please contact your workspace owner.' };
|
|
193
|
+
default:
|
|
194
|
+
return { title: '', body: '' };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const copy = $derived(getCopy(noticeState, isBillingAdmin));
|
|
199
|
+
|
|
200
|
+
function handleAction() {
|
|
201
|
+
if (onActionClick) {
|
|
202
|
+
onActionClick(noticeState);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
// Default: open the existing billing surface. The exact destination
|
|
206
|
+
// depends on app configuration — for now, navigate to /billing on the
|
|
207
|
+
// current origin. Apps can override via `onActionClick`.
|
|
208
|
+
if (typeof window !== 'undefined') {
|
|
209
|
+
window.location.href = '/billing';
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
</script>
|
|
213
|
+
|
|
214
|
+
{#if visible}
|
|
215
|
+
{#if asLockscreen}
|
|
216
|
+
<div
|
|
217
|
+
class={`bridge-billing-lockscreen ${className}`}
|
|
218
|
+
role="alertdialog"
|
|
219
|
+
aria-modal="true"
|
|
220
|
+
aria-live="assertive"
|
|
221
|
+
>
|
|
222
|
+
<div class="bbl-panel">
|
|
223
|
+
<strong class="bbl-title">{copy.title}</strong>
|
|
224
|
+
<span class="bbl-body">{copy.body}</span>
|
|
225
|
+
{#if copy.cta && isBillingAdmin}
|
|
226
|
+
<button type="button" class="bbl-cta" onclick={handleAction}>{copy.cta}</button>
|
|
227
|
+
{/if}
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
{:else}
|
|
231
|
+
<div
|
|
232
|
+
class={`bridge-billing-notice bbn-chassis-${chassis} bbn-severity-${severity} ${className}`}
|
|
233
|
+
role={severity === 'critical' || severity === 'locked' ? 'alert' : 'status'}
|
|
234
|
+
aria-live={severity === 'critical' || severity === 'locked' ? 'assertive' : 'polite'}
|
|
235
|
+
>
|
|
236
|
+
<div class="bbn-content">
|
|
237
|
+
<strong class="bbn-title">{copy.title}</strong>
|
|
238
|
+
<span class="bbn-body">{copy.body}</span>
|
|
239
|
+
</div>
|
|
240
|
+
{#if copy.cta && isBillingAdmin}
|
|
241
|
+
<button type="button" class="bbn-cta" onclick={handleAction}>{copy.cta}</button>
|
|
242
|
+
{/if}
|
|
243
|
+
</div>
|
|
244
|
+
{/if}
|
|
245
|
+
{/if}
|
|
246
|
+
|
|
247
|
+
<style>
|
|
248
|
+
.bridge-billing-notice {
|
|
249
|
+
display: flex;
|
|
250
|
+
align-items: center;
|
|
251
|
+
justify-content: space-between;
|
|
252
|
+
gap: 1rem;
|
|
253
|
+
padding: 0.75rem 1rem;
|
|
254
|
+
border-radius: 0.5rem;
|
|
255
|
+
font: inherit;
|
|
256
|
+
transition: opacity 280ms ease, transform 320ms ease;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.bbn-content {
|
|
260
|
+
display: flex;
|
|
261
|
+
flex-direction: column;
|
|
262
|
+
gap: 0.125rem;
|
|
263
|
+
flex: 1 1 auto;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.bbn-title {
|
|
267
|
+
font-weight: 600;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.bbn-body {
|
|
271
|
+
font-size: 0.875rem;
|
|
272
|
+
opacity: 0.85;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.bbn-cta {
|
|
276
|
+
padding: 0.5rem 1rem;
|
|
277
|
+
border-radius: 0.375rem;
|
|
278
|
+
border: 1px solid currentColor;
|
|
279
|
+
background: transparent;
|
|
280
|
+
color: inherit;
|
|
281
|
+
font: inherit;
|
|
282
|
+
font-weight: 500;
|
|
283
|
+
cursor: pointer;
|
|
284
|
+
flex: 0 0 auto;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.bbn-cta:hover {
|
|
288
|
+
background: currentColor;
|
|
289
|
+
color: white;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/* Chassis variants */
|
|
293
|
+
.bbn-chassis-bar {
|
|
294
|
+
border-radius: 0;
|
|
295
|
+
width: 100%;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.bbn-chassis-rail {
|
|
299
|
+
border-left: 4px solid currentColor;
|
|
300
|
+
border-radius: 0 0.5rem 0.5rem 0;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.bbn-chassis-card {
|
|
304
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
305
|
+
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/* Severity tokens */
|
|
309
|
+
.bbn-severity-info {
|
|
310
|
+
background: #dbeafe;
|
|
311
|
+
color: #1e40af;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.bbn-severity-warn {
|
|
315
|
+
background: #fef3c7;
|
|
316
|
+
color: #92400e;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.bbn-severity-critical {
|
|
320
|
+
background: #fee2e2;
|
|
321
|
+
color: #991b1b;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.bbn-severity-locked {
|
|
325
|
+
background: #1f2937;
|
|
326
|
+
color: #f9fafb;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
@media (prefers-reduced-motion: reduce) {
|
|
330
|
+
.bridge-billing-notice {
|
|
331
|
+
transition: none;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/* Hard-mode lockscreen — full-viewport blocking overlay */
|
|
336
|
+
.bridge-billing-lockscreen {
|
|
337
|
+
position: fixed;
|
|
338
|
+
inset: 0;
|
|
339
|
+
z-index: 9999;
|
|
340
|
+
display: flex;
|
|
341
|
+
align-items: center;
|
|
342
|
+
justify-content: center;
|
|
343
|
+
padding: 1rem;
|
|
344
|
+
background: rgba(15, 23, 42, 0.72);
|
|
345
|
+
backdrop-filter: blur(2px);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.bbl-panel {
|
|
349
|
+
display: flex;
|
|
350
|
+
flex-direction: column;
|
|
351
|
+
gap: 0.75rem;
|
|
352
|
+
max-width: 28rem;
|
|
353
|
+
width: 100%;
|
|
354
|
+
padding: 1.5rem;
|
|
355
|
+
border-radius: 0.75rem;
|
|
356
|
+
background: #1f2937;
|
|
357
|
+
color: #f9fafb;
|
|
358
|
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35);
|
|
359
|
+
text-align: center;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.bbl-title {
|
|
363
|
+
font-size: 1.125rem;
|
|
364
|
+
font-weight: 600;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.bbl-body {
|
|
368
|
+
font-size: 0.9375rem;
|
|
369
|
+
opacity: 0.85;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.bbl-cta {
|
|
373
|
+
align-self: center;
|
|
374
|
+
margin-top: 0.5rem;
|
|
375
|
+
padding: 0.625rem 1.25rem;
|
|
376
|
+
border-radius: 0.5rem;
|
|
377
|
+
border: 1px solid currentColor;
|
|
378
|
+
background: transparent;
|
|
379
|
+
color: inherit;
|
|
380
|
+
font: inherit;
|
|
381
|
+
font-weight: 500;
|
|
382
|
+
cursor: pointer;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.bbl-cta:hover {
|
|
386
|
+
background: #f9fafb;
|
|
387
|
+
color: #1f2937;
|
|
388
|
+
}
|
|
389
|
+
</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type BillingNoticeState } from '@nebulr-group/bridge-auth-core';
|
|
2
|
+
type Chassis = 'bar' | 'rail' | 'card';
|
|
3
|
+
interface Props {
|
|
4
|
+
/** Visual chassis variant. Per `deliverables.html §01`. */
|
|
5
|
+
chassis?: Chassis;
|
|
6
|
+
/**
|
|
7
|
+
* Gate behavior. `soft` (default) always renders the inline banner. `hard`
|
|
8
|
+
* renders a full-screen blocking lockscreen when the workspace is locked
|
|
9
|
+
* (gate engaged); non-locked states still render as the inline banner.
|
|
10
|
+
*/
|
|
11
|
+
mode?: 'soft' | 'hard';
|
|
12
|
+
/** Optional class applied to the root element. */
|
|
13
|
+
class?: string;
|
|
14
|
+
/** Override the default CTA click handler (links to billing surface). */
|
|
15
|
+
onActionClick?: (state: BillingNoticeState) => void;
|
|
16
|
+
}
|
|
17
|
+
declare const BridgeBillingNotice: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type BridgeBillingNotice = ReturnType<typeof BridgeBillingNotice>;
|
|
19
|
+
export default BridgeBillingNotice;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { onMount } from 'svelte';
|
|
4
|
+
import type { Plan, PriceOfferSdk } from '@nebulr-group/bridge-auth-core';
|
|
5
|
+
import { subscriptionStore, loadSubscription } from '../../../core/bridge-instance.js';
|
|
6
|
+
import PlanSelector from './PlanSelector.svelte';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
/** Where to send the user after a successful Stripe payment. @default '/' */
|
|
10
|
+
successRedirect?: string;
|
|
11
|
+
/** Where to send the user if they cancel Stripe Checkout. @default '/' */
|
|
12
|
+
cancelRedirect?: string;
|
|
13
|
+
/** Called after free-plan or direct plan change (not the Stripe redirect path). Use for analytics side-effects. */
|
|
14
|
+
onSelect?: (detail: { plan: Plan; price: PriceOfferSdk }) => void;
|
|
15
|
+
/** Override the default "Choose a plan" heading. */
|
|
16
|
+
heading?: Snippet;
|
|
17
|
+
children?: Snippet;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
successRedirect = '/',
|
|
22
|
+
cancelRedirect = '/',
|
|
23
|
+
onSelect,
|
|
24
|
+
heading,
|
|
25
|
+
children,
|
|
26
|
+
}: Props = $props();
|
|
27
|
+
|
|
28
|
+
const status = $derived($subscriptionStore.status);
|
|
29
|
+
const loading = $derived($subscriptionStore.loading);
|
|
30
|
+
|
|
31
|
+
onMount(() => {
|
|
32
|
+
if (!$subscriptionStore.status && !$subscriptionStore.loading) {
|
|
33
|
+
loadSubscription();
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Show paywall only once the subscription status is known and the tenant
|
|
38
|
+
// has no plan selected. Respects paymentsAutoRedirect: false (opt-out flag).
|
|
39
|
+
const showPaywall = $derived(
|
|
40
|
+
!loading &&
|
|
41
|
+
!!status?.shouldSelectPlan &&
|
|
42
|
+
status?.paymentsAutoRedirect !== false
|
|
43
|
+
);
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
{#if showPaywall}
|
|
47
|
+
<div class="bridge-paywall" role="dialog" aria-modal="true" aria-label="Choose a plan">
|
|
48
|
+
<div class="bridge-paywall-panel">
|
|
49
|
+
{#if heading}
|
|
50
|
+
{@render heading()}
|
|
51
|
+
{:else}
|
|
52
|
+
<h2 class="bridge-paywall-heading">Choose a plan</h2>
|
|
53
|
+
{/if}
|
|
54
|
+
<PlanSelector {successRedirect} {cancelRedirect} {onSelect} />
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
{:else}
|
|
58
|
+
{@render children?.()}
|
|
59
|
+
{/if}
|
|
60
|
+
|
|
61
|
+
<style>
|
|
62
|
+
.bridge-paywall {
|
|
63
|
+
position: fixed;
|
|
64
|
+
inset: 0;
|
|
65
|
+
z-index: 9000;
|
|
66
|
+
display: flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
justify-content: center;
|
|
69
|
+
padding: 1rem;
|
|
70
|
+
background: var(--bridge-paywall-bg, rgba(15, 23, 42, 0.72));
|
|
71
|
+
backdrop-filter: blur(2px);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.bridge-paywall-panel {
|
|
75
|
+
display: flex;
|
|
76
|
+
flex-direction: column;
|
|
77
|
+
gap: 1.5rem;
|
|
78
|
+
width: 100%;
|
|
79
|
+
max-width: 42rem;
|
|
80
|
+
max-height: 90vh;
|
|
81
|
+
overflow-y: auto;
|
|
82
|
+
padding: 2rem;
|
|
83
|
+
border-radius: 0.75rem;
|
|
84
|
+
background: var(--bridge-paywall-panel-bg, #ffffff);
|
|
85
|
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.bridge-paywall-heading {
|
|
89
|
+
margin: 0;
|
|
90
|
+
font-size: 1.25rem;
|
|
91
|
+
font-weight: 600;
|
|
92
|
+
text-align: center;
|
|
93
|
+
}
|
|
94
|
+
</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { Plan, PriceOfferSdk } from '@nebulr-group/bridge-auth-core';
|
|
3
|
+
interface Props {
|
|
4
|
+
/** Where to send the user after a successful Stripe payment. @default '/' */
|
|
5
|
+
successRedirect?: string;
|
|
6
|
+
/** Where to send the user if they cancel Stripe Checkout. @default '/' */
|
|
7
|
+
cancelRedirect?: string;
|
|
8
|
+
/** Called after free-plan or direct plan change (not the Stripe redirect path). Use for analytics side-effects. */
|
|
9
|
+
onSelect?: (detail: {
|
|
10
|
+
plan: Plan;
|
|
11
|
+
price: PriceOfferSdk;
|
|
12
|
+
}) => void;
|
|
13
|
+
/** Override the default "Choose a plan" heading. */
|
|
14
|
+
heading?: Snippet;
|
|
15
|
+
children?: Snippet;
|
|
16
|
+
}
|
|
17
|
+
declare const BridgePaywall: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type BridgePaywall = ReturnType<typeof BridgePaywall>;
|
|
19
|
+
export default BridgePaywall;
|