@sparkvault/sdk 1.21.10 → 1.23.1

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.
@@ -2344,6 +2344,250 @@ function getStyles(options) {
2344
2344
  flex: 1;
2345
2345
  }
2346
2346
 
2347
+ /* ========================================
2348
+ SERVICE UNAVAILABLE VIEW
2349
+ ======================================== */
2350
+
2351
+ .sv-unavailable-view {
2352
+ display: flex;
2353
+ flex-direction: column;
2354
+ align-items: center;
2355
+ text-align: center;
2356
+ }
2357
+
2358
+ .sv-unavailable-icon-container {
2359
+ margin-bottom: 16px;
2360
+ }
2361
+
2362
+ .sv-unavailable-icon {
2363
+ width: 48px;
2364
+ height: 48px;
2365
+ }
2366
+
2367
+ .sv-unavailable-content {
2368
+ margin-bottom: 24px;
2369
+ }
2370
+
2371
+ .sv-unavailable-title {
2372
+ font-size: 18px;
2373
+ font-weight: 600;
2374
+ letter-spacing: -0.02em;
2375
+ margin: 0 0 8px 0;
2376
+ color: ${tokens.textPrimary};
2377
+ }
2378
+
2379
+ .sv-unavailable-message {
2380
+ font-size: 14px;
2381
+ line-height: 1.5;
2382
+ color: ${tokens.textSecondary};
2383
+ margin: 0;
2384
+ }
2385
+
2386
+ .sv-unavailable-actions {
2387
+ display: flex;
2388
+ gap: 10px;
2389
+ width: 100%;
2390
+ }
2391
+
2392
+ .sv-unavailable-actions .sv-btn {
2393
+ flex: 1;
2394
+ }
2395
+
2396
+ @media (max-width: 480px) {
2397
+ .sv-unavailable-actions {
2398
+ flex-direction: column;
2399
+ }
2400
+ }
2401
+
2402
+ /* ========================================
2403
+ MINIMAL CHROME (full-dialog views)
2404
+ ======================================== */
2405
+
2406
+ .sv-modal-minimal {
2407
+ position: relative;
2408
+ }
2409
+
2410
+ .sv-modal-minimal > .sv-header {
2411
+ display: none;
2412
+ }
2413
+
2414
+ .sv-modal-minimal > .sv-body {
2415
+ padding: 36px 32px 28px;
2416
+ }
2417
+
2418
+ .sv-floating-close {
2419
+ position: absolute;
2420
+ top: 14px;
2421
+ right: 14px;
2422
+ width: 32px;
2423
+ height: 32px;
2424
+ display: flex;
2425
+ align-items: center;
2426
+ justify-content: center;
2427
+ background: transparent;
2428
+ border: none;
2429
+ border-radius: 8px;
2430
+ cursor: pointer;
2431
+ color: ${tokens.textMuted};
2432
+ transition: background 0.15s ease, color 0.15s ease;
2433
+ z-index: 1;
2434
+ }
2435
+
2436
+ .sv-floating-close:hover {
2437
+ background: ${tokens.bgHover};
2438
+ color: ${tokens.textPrimary};
2439
+ }
2440
+
2441
+ .sv-floating-close:focus-visible {
2442
+ outline: 2px solid ${tokens.borderFocus};
2443
+ outline-offset: 2px;
2444
+ }
2445
+
2446
+ .sv-floating-close:active {
2447
+ transform: scale(0.96);
2448
+ }
2449
+
2450
+ /* ========================================
2451
+ APP NOT ACTIVATED VIEW
2452
+ ======================================== */
2453
+
2454
+ .sv-not-activated-view {
2455
+ display: flex;
2456
+ flex-direction: column;
2457
+ align-items: center;
2458
+ text-align: center;
2459
+ }
2460
+
2461
+ .sv-not-activated-eyebrow {
2462
+ display: inline-flex;
2463
+ align-items: center;
2464
+ gap: 8px;
2465
+ padding: 5px 10px;
2466
+ border-radius: 999px;
2467
+ background: ${tokens.bgHover};
2468
+ color: ${tokens.textSecondary};
2469
+ font-size: 11px;
2470
+ font-weight: 600;
2471
+ letter-spacing: 0.04em;
2472
+ text-transform: uppercase;
2473
+ margin-bottom: 20px;
2474
+ }
2475
+
2476
+ .sv-not-activated-mark {
2477
+ display: inline-flex;
2478
+ align-items: center;
2479
+ gap: 5px;
2480
+ color: ${tokens.textPrimary};
2481
+ font-weight: 700;
2482
+ letter-spacing: -0.01em;
2483
+ text-transform: none;
2484
+ font-size: 12px;
2485
+ }
2486
+
2487
+ .sv-not-activated-eyebrow-divider {
2488
+ color: ${tokens.textMuted};
2489
+ opacity: 0.6;
2490
+ }
2491
+
2492
+ .sv-not-activated-eyebrow-label {
2493
+ color: ${tokens.textSecondary};
2494
+ }
2495
+
2496
+ .sv-not-activated-badge {
2497
+ margin-bottom: 20px;
2498
+ }
2499
+
2500
+ .sv-not-activated-badge-svg {
2501
+ display: block;
2502
+ filter: drop-shadow(0 6px 14px rgba(245, 158, 11, 0.18));
2503
+ }
2504
+
2505
+ .sv-not-activated-title {
2506
+ font-size: 22px;
2507
+ font-weight: 700;
2508
+ letter-spacing: -0.02em;
2509
+ line-height: 1.2;
2510
+ margin: 0 0 10px 0;
2511
+ color: ${tokens.textPrimary};
2512
+ }
2513
+
2514
+ .sv-not-activated-message {
2515
+ font-size: 14px;
2516
+ line-height: 1.55;
2517
+ color: ${tokens.textSecondary};
2518
+ margin: 0 0 22px 0;
2519
+ max-width: 320px;
2520
+ }
2521
+
2522
+ .sv-not-activated-owner {
2523
+ width: 100%;
2524
+ display: flex;
2525
+ flex-direction: column;
2526
+ gap: 4px;
2527
+ padding: 14px 16px;
2528
+ border: 1px solid ${tokens.border};
2529
+ border-radius: 12px;
2530
+ background: ${tokens.bgSubtle};
2531
+ text-align: left;
2532
+ margin-bottom: 22px;
2533
+ }
2534
+
2535
+ .sv-not-activated-owner-label {
2536
+ font-size: 13px;
2537
+ font-weight: 600;
2538
+ color: ${tokens.textPrimary};
2539
+ }
2540
+
2541
+ .sv-not-activated-owner-hint {
2542
+ font-size: 12.5px;
2543
+ line-height: 1.5;
2544
+ color: ${tokens.textSecondary};
2545
+ }
2546
+
2547
+ .sv-not-activated-actions {
2548
+ display: flex;
2549
+ flex-direction: column;
2550
+ align-items: stretch;
2551
+ gap: 10px;
2552
+ width: 100%;
2553
+ }
2554
+
2555
+ .sv-not-activated-cta {
2556
+ display: inline-flex;
2557
+ align-items: center;
2558
+ justify-content: center;
2559
+ gap: 8px;
2560
+ width: 100%;
2561
+ text-decoration: none;
2562
+ height: 44px;
2563
+ }
2564
+
2565
+ .sv-not-activated-cta-icon {
2566
+ opacity: 0.85;
2567
+ }
2568
+
2569
+ .sv-not-activated-dismiss {
2570
+ background: transparent;
2571
+ border: none;
2572
+ cursor: pointer;
2573
+ padding: 8px;
2574
+ font-size: 13px;
2575
+ font-weight: 500;
2576
+ color: ${tokens.textMuted};
2577
+ transition: color 0.15s ease;
2578
+ align-self: center;
2579
+ }
2580
+
2581
+ .sv-not-activated-dismiss:hover {
2582
+ color: ${tokens.textPrimary};
2583
+ }
2584
+
2585
+ .sv-not-activated-dismiss:focus-visible {
2586
+ outline: 2px solid ${tokens.borderFocus};
2587
+ outline-offset: 2px;
2588
+ border-radius: 6px;
2589
+ }
2590
+
2347
2591
  /* ========================================
2348
2592
  TOTP VIEW
2349
2593
  ======================================== */
@@ -3011,6 +3255,8 @@ class ModalContainer {
3011
3255
  this.backdropBlur = true;
3012
3256
  this.effectiveTheme = 'light';
3013
3257
  this.headerElement = null;
3258
+ this.floatingCloseBtn = null;
3259
+ this.floatingCloseHandler = null;
3014
3260
  }
3015
3261
  /**
3016
3262
  * Create and show the modal immediately with loading state.
@@ -3038,6 +3284,39 @@ class ModalContainer {
3038
3284
  }
3039
3285
  this.headerElement = newHeader;
3040
3286
  }
3287
+ /**
3288
+ * Toggle minimal-chrome mode. In minimal mode the header (logo + close)
3289
+ * is hidden and a floating close button is positioned over the modal so
3290
+ * the body view can render edge-to-edge as its own full-dialog design.
3291
+ */
3292
+ setMinimalChrome(enabled) {
3293
+ if (!this.elements)
3294
+ return;
3295
+ const { modal } = this.elements;
3296
+ if (enabled) {
3297
+ modal.classList.add('sv-modal-minimal');
3298
+ if (!this.floatingCloseBtn) {
3299
+ const btn = document.createElement('button');
3300
+ btn.className = 'sv-floating-close';
3301
+ btn.type = 'button';
3302
+ btn.setAttribute('aria-label', 'Close');
3303
+ btn.appendChild(createCloseIcon());
3304
+ this.floatingCloseHandler = () => this.handleClose();
3305
+ btn.addEventListener('click', this.floatingCloseHandler);
3306
+ this.floatingCloseBtn = btn;
3307
+ modal.appendChild(btn);
3308
+ }
3309
+ }
3310
+ else {
3311
+ modal.classList.remove('sv-modal-minimal');
3312
+ if (this.floatingCloseBtn && this.floatingCloseHandler) {
3313
+ this.floatingCloseBtn.removeEventListener('click', this.floatingCloseHandler);
3314
+ this.floatingCloseBtn.remove();
3315
+ this.floatingCloseBtn = null;
3316
+ this.floatingCloseHandler = null;
3317
+ }
3318
+ }
3319
+ }
3041
3320
  /**
3042
3321
  * Update the backdrop blur setting.
3043
3322
  * Called after SDK config loads with resolved value (client override > server config > default).
@@ -3202,6 +3481,12 @@ class ModalContainer {
3202
3481
  this.closeBtnClickHandler = null;
3203
3482
  this.closeBtn = null;
3204
3483
  }
3484
+ // Clean up floating close button (used in minimal-chrome mode)
3485
+ if (this.floatingCloseBtn && this.floatingCloseHandler) {
3486
+ this.floatingCloseBtn.removeEventListener('click', this.floatingCloseHandler);
3487
+ this.floatingCloseBtn = null;
3488
+ this.floatingCloseHandler = null;
3489
+ }
3205
3490
  // Remove modal from DOM
3206
3491
  if (this.elements) {
3207
3492
  this.elements.overlay.remove();
@@ -4422,6 +4707,257 @@ class ErrorView {
4422
4707
  }
4423
4708
  }
4424
4709
 
4710
+ /**
4711
+ * Service Unavailable View
4712
+ *
4713
+ * Shown when the identity config endpoint is unreachable.
4714
+ * Clean, professional, branded — no error codes or technical jargon.
4715
+ */
4716
+ function createShieldIcon() {
4717
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
4718
+ svg.setAttribute('viewBox', '0 0 48 48');
4719
+ svg.setAttribute('width', '48');
4720
+ svg.setAttribute('height', '48');
4721
+ svg.setAttribute('fill', 'none');
4722
+ svg.classList.add('sv-unavailable-icon');
4723
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
4724
+ circle.setAttribute('cx', '24');
4725
+ circle.setAttribute('cy', '24');
4726
+ circle.setAttribute('r', '22');
4727
+ circle.setAttribute('fill', '#EEF2FF');
4728
+ circle.setAttribute('stroke', '#E0E7FF');
4729
+ circle.setAttribute('stroke-width', '1');
4730
+ svg.appendChild(circle);
4731
+ const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
4732
+ group.setAttribute('transform', 'translate(12, 10)');
4733
+ // Shield shape
4734
+ const shield = document.createElementNS('http://www.w3.org/2000/svg', 'path');
4735
+ shield.setAttribute('d', 'M12 2L3 6v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V6l-9-4z');
4736
+ shield.setAttribute('fill', '#A5B4FC');
4737
+ group.appendChild(shield);
4738
+ // Pause bars (indicates temporary)
4739
+ const bar1 = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
4740
+ bar1.setAttribute('x', '9');
4741
+ bar1.setAttribute('y', '9');
4742
+ bar1.setAttribute('width', '2.5');
4743
+ bar1.setAttribute('height', '8');
4744
+ bar1.setAttribute('rx', '1');
4745
+ bar1.setAttribute('fill', '#FFFFFF');
4746
+ group.appendChild(bar1);
4747
+ const bar2 = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
4748
+ bar2.setAttribute('x', '12.5');
4749
+ bar2.setAttribute('y', '9');
4750
+ bar2.setAttribute('width', '2.5');
4751
+ bar2.setAttribute('height', '8');
4752
+ bar2.setAttribute('rx', '1');
4753
+ bar2.setAttribute('fill', '#FFFFFF');
4754
+ group.appendChild(bar2);
4755
+ svg.appendChild(group);
4756
+ return svg;
4757
+ }
4758
+ class ServiceUnavailableView {
4759
+ constructor(props) {
4760
+ this.retryButton = null;
4761
+ this.closeButton = null;
4762
+ this.props = props;
4763
+ this.boundHandleRetry = () => this.props.onRetry();
4764
+ this.boundHandleClose = () => this.props.onClose();
4765
+ }
4766
+ render() {
4767
+ const container = div('sv-unavailable-view');
4768
+ // Icon
4769
+ const iconContainer = div('sv-unavailable-icon-container');
4770
+ iconContainer.appendChild(createShieldIcon());
4771
+ container.appendChild(iconContainer);
4772
+ // Content
4773
+ const content = div('sv-unavailable-content');
4774
+ const title = document.createElement('h3');
4775
+ title.className = 'sv-unavailable-title';
4776
+ title.textContent = 'Temporarily Unavailable';
4777
+ content.appendChild(title);
4778
+ const message = document.createElement('p');
4779
+ message.className = 'sv-unavailable-message';
4780
+ message.textContent = 'Authentication is experiencing a brief interruption. Please try again in a moment.';
4781
+ content.appendChild(message);
4782
+ container.appendChild(content);
4783
+ // Actions
4784
+ const actions = div('sv-unavailable-actions');
4785
+ this.retryButton = document.createElement('button');
4786
+ this.retryButton.className = 'sv-btn sv-btn-primary';
4787
+ this.retryButton.textContent = 'Try Again';
4788
+ this.retryButton.addEventListener('click', this.boundHandleRetry);
4789
+ actions.appendChild(this.retryButton);
4790
+ this.closeButton = document.createElement('button');
4791
+ this.closeButton.className = 'sv-btn sv-btn-secondary';
4792
+ this.closeButton.textContent = 'Close';
4793
+ this.closeButton.addEventListener('click', this.boundHandleClose);
4794
+ actions.appendChild(this.closeButton);
4795
+ container.appendChild(actions);
4796
+ return container;
4797
+ }
4798
+ destroy() {
4799
+ if (this.retryButton) {
4800
+ this.retryButton.removeEventListener('click', this.boundHandleRetry);
4801
+ }
4802
+ if (this.closeButton) {
4803
+ this.closeButton.removeEventListener('click', this.boundHandleClose);
4804
+ }
4805
+ }
4806
+ }
4807
+
4808
+ /**
4809
+ * App Not Activated View
4810
+ *
4811
+ * Full-dialog terminal screen shown when the SparkVault Identity app has not
4812
+ * been activated for the account that the JS SDK is configured against. The
4813
+ * audience is the website owner / developer — this is a misconfiguration
4814
+ * surface, not an end-user error — so the dialog drops customer branding and
4815
+ * presents itself as an unmistakable SparkVault notice.
4816
+ */
4817
+ const ACTIVATION_URL = 'https://app.sparkvault.com/apps';
4818
+ function createBadge() {
4819
+ const badge = div('sv-not-activated-badge');
4820
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
4821
+ svg.setAttribute('viewBox', '0 0 56 56');
4822
+ svg.setAttribute('width', '72');
4823
+ svg.setAttribute('height', '72');
4824
+ svg.setAttribute('fill', 'none');
4825
+ svg.classList.add('sv-not-activated-badge-svg');
4826
+ // Soft outer ring
4827
+ const ring = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
4828
+ ring.setAttribute('cx', '28');
4829
+ ring.setAttribute('cy', '28');
4830
+ ring.setAttribute('r', '27');
4831
+ ring.setAttribute('fill', '#FFFBEB');
4832
+ ring.setAttribute('stroke', '#FDE68A');
4833
+ ring.setAttribute('stroke-width', '1');
4834
+ svg.appendChild(ring);
4835
+ // Shield body
4836
+ const shield = document.createElementNS('http://www.w3.org/2000/svg', 'path');
4837
+ shield.setAttribute('d', 'M28 13l-10 4.2v8.6c0 6.7 4.27 12.95 10 14.4 5.73-1.45 10-7.7 10-14.4v-8.6L28 13z');
4838
+ shield.setAttribute('fill', '#F59E0B');
4839
+ svg.appendChild(shield);
4840
+ // Exclamation bar
4841
+ const bar = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
4842
+ bar.setAttribute('x', '26.6');
4843
+ bar.setAttribute('y', '20');
4844
+ bar.setAttribute('width', '2.8');
4845
+ bar.setAttribute('height', '10');
4846
+ bar.setAttribute('rx', '1.4');
4847
+ bar.setAttribute('fill', '#FFFFFF');
4848
+ svg.appendChild(bar);
4849
+ // Exclamation dot
4850
+ const dot = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
4851
+ dot.setAttribute('cx', '28');
4852
+ dot.setAttribute('cy', '34');
4853
+ dot.setAttribute('r', '1.6');
4854
+ dot.setAttribute('fill', '#FFFFFF');
4855
+ svg.appendChild(dot);
4856
+ badge.appendChild(svg);
4857
+ return badge;
4858
+ }
4859
+ function createExternalLinkIcon() {
4860
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
4861
+ svg.setAttribute('viewBox', '0 0 16 16');
4862
+ svg.setAttribute('width', '14');
4863
+ svg.setAttribute('height', '14');
4864
+ svg.setAttribute('fill', 'none');
4865
+ svg.classList.add('sv-not-activated-cta-icon');
4866
+ const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
4867
+ arrow.setAttribute('d', 'M6 3h7v7M13 3l-8 8');
4868
+ arrow.setAttribute('stroke', 'currentColor');
4869
+ arrow.setAttribute('stroke-width', '2');
4870
+ arrow.setAttribute('stroke-linecap', 'round');
4871
+ arrow.setAttribute('stroke-linejoin', 'round');
4872
+ svg.appendChild(arrow);
4873
+ return svg;
4874
+ }
4875
+ function createSparkVaultMark() {
4876
+ const mark = div('sv-not-activated-mark');
4877
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
4878
+ svg.setAttribute('viewBox', '0 0 16 16');
4879
+ svg.setAttribute('width', '14');
4880
+ svg.setAttribute('height', '14');
4881
+ svg.setAttribute('fill', 'none');
4882
+ const flame = document.createElementNS('http://www.w3.org/2000/svg', 'path');
4883
+ flame.setAttribute('d', 'M8 1.5c.6 1.8 2 2.7 2 4.4 0 .9-.5 1.6-1.2 1.6-.6 0-1-.4-1-.9 0-.4.2-.8.2-1.2 0-.7-.5-1.1-1-1.1-1.4 0-2.5 1.5-2.5 3.4 0 2.4 1.9 4.3 4.5 4.3s4.5-1.9 4.5-4.3c0-3.6-3-4.6-5.5-6.2z');
4884
+ flame.setAttribute('fill', 'currentColor');
4885
+ svg.appendChild(flame);
4886
+ const label = document.createElement('span');
4887
+ label.textContent = 'SparkVault';
4888
+ mark.appendChild(svg);
4889
+ mark.appendChild(label);
4890
+ return mark;
4891
+ }
4892
+ class AppNotActivatedView {
4893
+ constructor(props) {
4894
+ this.dismissButton = null;
4895
+ this.props = props;
4896
+ this.boundHandleClose = () => this.props.onClose();
4897
+ }
4898
+ render() {
4899
+ const container = div('sv-not-activated-view');
4900
+ const eyebrow = div('sv-not-activated-eyebrow');
4901
+ eyebrow.appendChild(createSparkVaultMark());
4902
+ const eyebrowDivider = document.createElement('span');
4903
+ eyebrowDivider.className = 'sv-not-activated-eyebrow-divider';
4904
+ eyebrowDivider.textContent = '•';
4905
+ eyebrow.appendChild(eyebrowDivider);
4906
+ const eyebrowLabel = document.createElement('span');
4907
+ eyebrowLabel.className = 'sv-not-activated-eyebrow-label';
4908
+ eyebrowLabel.textContent = 'Identity';
4909
+ eyebrow.appendChild(eyebrowLabel);
4910
+ container.appendChild(eyebrow);
4911
+ const badge = createBadge();
4912
+ container.appendChild(badge);
4913
+ const title = document.createElement('h2');
4914
+ title.className = 'sv-not-activated-title';
4915
+ title.id = 'sv-modal-title';
4916
+ title.textContent = 'Identity app not activated';
4917
+ container.appendChild(title);
4918
+ const message = document.createElement('p');
4919
+ message.className = 'sv-not-activated-message';
4920
+ message.textContent =
4921
+ 'Sign-in is unavailable because the site owner hasn’t activated SparkVault Identity for this account. Activate the app to enable passwordless authentication.';
4922
+ container.appendChild(message);
4923
+ const ownerCard = div('sv-not-activated-owner');
4924
+ const ownerLabel = document.createElement('span');
4925
+ ownerLabel.className = 'sv-not-activated-owner-label';
4926
+ ownerLabel.textContent = 'Are you the site owner?';
4927
+ const ownerHint = document.createElement('span');
4928
+ ownerHint.className = 'sv-not-activated-owner-hint';
4929
+ ownerHint.textContent =
4930
+ 'Sign in to your SparkVault account and install the Identity app to start accepting sign-ins.';
4931
+ ownerCard.appendChild(ownerLabel);
4932
+ ownerCard.appendChild(ownerHint);
4933
+ container.appendChild(ownerCard);
4934
+ const actions = div('sv-not-activated-actions');
4935
+ const cta = document.createElement('a');
4936
+ cta.className = 'sv-btn sv-btn-primary sv-not-activated-cta';
4937
+ cta.href = ACTIVATION_URL;
4938
+ cta.target = '_blank';
4939
+ cta.rel = 'noopener noreferrer';
4940
+ const ctaLabel = document.createElement('span');
4941
+ ctaLabel.textContent = 'Activate on SparkVault';
4942
+ cta.appendChild(ctaLabel);
4943
+ cta.appendChild(createExternalLinkIcon());
4944
+ actions.appendChild(cta);
4945
+ this.dismissButton = document.createElement('button');
4946
+ this.dismissButton.className = 'sv-not-activated-dismiss';
4947
+ this.dismissButton.type = 'button';
4948
+ this.dismissButton.textContent = 'Dismiss';
4949
+ this.dismissButton.addEventListener('click', this.boundHandleClose);
4950
+ actions.appendChild(this.dismissButton);
4951
+ container.appendChild(actions);
4952
+ return container;
4953
+ }
4954
+ destroy() {
4955
+ if (this.dismissButton) {
4956
+ this.dismissButton.removeEventListener('click', this.boundHandleClose);
4957
+ }
4958
+ }
4959
+ }
4960
+
4425
4961
  /**
4426
4962
  * Identity View Renderer
4427
4963
  *
@@ -4478,14 +5014,23 @@ class IdentityRenderer {
4478
5014
  let config = null;
4479
5015
  if (this.api.isConfigPreloaded()) {
4480
5016
  // Config fetch already started - race it against a 50ms timeout
4481
- // If config is ready (or almost ready), we skip the loading flash
5017
+ // If config is ready (or almost ready), we skip the loading flash.
5018
+ // A rejected preload (e.g. app_not_installed) must not abort start() —
5019
+ // swallow here and let the in-modal try/catch below route to the
5020
+ // correct error view.
4482
5021
  const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(null), 50));
4483
- const result = await Promise.race([
4484
- this.api.getConfig().then((c) => c),
4485
- timeoutPromise,
4486
- ]);
4487
- if (result !== null) {
4488
- config = result;
5022
+ try {
5023
+ const result = await Promise.race([
5024
+ this.api.getConfig().then((c) => c),
5025
+ timeoutPromise,
5026
+ ]);
5027
+ if (result !== null) {
5028
+ config = result;
5029
+ }
5030
+ }
5031
+ catch {
5032
+ // Preload failed - fall through to fetchConfigWithRetry which will
5033
+ // surface the real error inside the modal.
4489
5034
  }
4490
5035
  }
4491
5036
  // Create modal - if we have config, skip loading state entirely
@@ -4519,9 +5064,21 @@ class IdentityRenderer {
4519
5064
  }
4520
5065
  }
4521
5066
  catch (error) {
4522
- this.handleErrorWithRecovery(error, 'config_error');
5067
+ this.setState(this.viewStateForConfigError(error));
4523
5068
  }
4524
5069
  }
5070
+ /**
5071
+ * Map a config-fetch error to the appropriate terminal view.
5072
+ * `app_not_installed` is a misconfiguration (account hasn't installed the
5073
+ * Identity app) — show a developer-targeted "Not Activated" screen with a
5074
+ * link to SparkVault.com instead of the generic service-unavailable view.
5075
+ */
5076
+ viewStateForConfigError(error) {
5077
+ if (error instanceof IdentityApiError && error.code === 'app_not_installed') {
5078
+ return { view: 'app-not-activated' };
5079
+ }
5080
+ return { view: 'service-unavailable' };
5081
+ }
4525
5082
  /**
4526
5083
  * Fetch config with a single retry on network errors.
4527
5084
  * Handles transient failures common during subdomain switches
@@ -4543,6 +5100,35 @@ class IdentityRenderer {
4543
5100
  return this.api.getConfig();
4544
5101
  }
4545
5102
  }
5103
+ /**
5104
+ * Retry loading config after a service-unavailable error.
5105
+ * Shows loading state, re-fetches config, then proceeds normally.
5106
+ */
5107
+ async retryConfigLoad() {
5108
+ this.setState({ view: 'loading' });
5109
+ try {
5110
+ const config = await this.fetchConfigWithRetry();
5111
+ this.verificationState.setConfig(config);
5112
+ if (config.branding) {
5113
+ this.container.updateBranding(config.branding);
5114
+ }
5115
+ const resolvedBackdropBlur = this.options.backdropBlur !== undefined
5116
+ ? this.options.backdropBlur
5117
+ : config.branding?.backdrop_blur !== undefined
5118
+ ? config.branding.backdrop_blur
5119
+ : true;
5120
+ this.container.updateBackdropBlur(resolvedBackdropBlur);
5121
+ if (this.recipient) {
5122
+ this.showMethodSelect();
5123
+ }
5124
+ else {
5125
+ this.showIdentityInput();
5126
+ }
5127
+ }
5128
+ catch (error) {
5129
+ this.setState(this.viewStateForConfigError(error));
5130
+ }
5131
+ }
4546
5132
  /**
4547
5133
  * Close the modal and clean up.
4548
5134
  * Cancels all pending API requests to prevent orphaned operations.
@@ -4574,6 +5160,10 @@ class IdentityRenderer {
4574
5160
  if (!body)
4575
5161
  return;
4576
5162
  this.destroyCurrentView();
5163
+ // Full-dialog terminal screens own the entire modal; toggle the container
5164
+ // chrome accordingly so the customer header doesn't bleed into a
5165
+ // SparkVault-owned message.
5166
+ this.container.setMinimalChrome(this.viewState.view === 'app-not-activated');
4577
5167
  const view = this.createViewForState(this.viewState);
4578
5168
  this.currentView = view;
4579
5169
  clearChildren(body);
@@ -4670,6 +5260,15 @@ class IdentityRenderer {
4670
5260
  onRetry: () => this.showIdentityInput(),
4671
5261
  onClose: () => this.handleClose(),
4672
5262
  });
5263
+ case 'service-unavailable':
5264
+ return new ServiceUnavailableView({
5265
+ onRetry: () => this.retryConfigLoad(),
5266
+ onClose: () => this.handleClose(),
5267
+ });
5268
+ case 'app-not-activated':
5269
+ return new AppNotActivatedView({
5270
+ onClose: () => this.handleClose(),
5271
+ });
4673
5272
  }
4674
5273
  }
4675
5274
  showIdentityInput(error) {
@@ -5445,6 +6044,13 @@ class InlineContainer {
5445
6044
  updateBackdropBlur(_enabled) {
5446
6045
  // No-op - inline containers don't have backdrop blur
5447
6046
  }
6047
+ /**
6048
+ * Minimal-chrome toggle (no-op for inline container).
6049
+ * Inline containers don't own page-level chrome to hide.
6050
+ */
6051
+ setMinimalChrome(_enabled) {
6052
+ // No-op
6053
+ }
5448
6054
  /**
5449
6055
  * Get the body element for content rendering.
5450
6056
  */
@@ -5808,15 +6414,19 @@ class IdentityModule {
5808
6414
  }
5809
6415
  return new InlineContainer(element);
5810
6416
  }
5811
- // Deprecated methods for backwards compatibility
6417
+ // Public-API aliases. The documented method names in the customer-facing
6418
+ // SDK README and the API docs are pop() and render(); verify() is the
6419
+ // underlying primitive. Both remain canonical entry points.
5812
6420
  /**
5813
- * @deprecated Use `verify()` instead. Will be removed in v2.0.
6421
+ * Open the identity verification modal as a popup. Equivalent to
6422
+ * `verify(options)`.
5814
6423
  */
5815
6424
  async pop(options = {}) {
5816
6425
  return this.verify(options);
5817
6426
  }
5818
6427
  /**
5819
- * @deprecated Use `verify({ target })` instead. Will be removed in v2.0.
6428
+ * Render the identity verification UI inline into a target element.
6429
+ * Equivalent to `verify({ ...options, target })`.
5820
6430
  */
5821
6431
  async render(options) {
5822
6432
  return this.verify(options);
@@ -8848,16 +9458,14 @@ class VaultUploadModule {
8848
9458
  }
8849
9459
  return new UploadInlineContainer(element);
8850
9460
  }
8851
- // Deprecated methods for backwards compatibility
8852
- /**
8853
- * @deprecated Use `upload()` instead. Will be removed in v2.0.
8854
- */
9461
+ // Public-API aliases. The web vault-edit screen instructs customers to
9462
+ // call sv.vaults.upload.pop(vaultId) / .render(vaultId, target) — both
9463
+ // are documented entry points that defer to upload() under the hood.
9464
+ /** Open the upload widget as a popup. Equivalent to `upload(options)`. */
8855
9465
  async pop(options) {
8856
9466
  return this.upload(options);
8857
9467
  }
8858
- /**
8859
- * @deprecated Use `upload({ target })` instead. Will be removed in v2.0.
8860
- */
9468
+ /** Render the upload widget inline. Equivalent to `upload({ ...options, target })`. */
8861
9469
  async render(options) {
8862
9470
  return this.upload(options);
8863
9471
  }
@@ -9259,7 +9867,6 @@ if (typeof window !== 'undefined') {
9259
9867
  // Auto-initialize from script tag data-account-id attribute
9260
9868
  const instance = autoInit(SparkVault);
9261
9869
  // Expose instance (if auto-init succeeded) or class (for manual init)
9262
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
9263
9870
  window.SparkVault = instance ?? SparkVault;
9264
9871
  }
9265
9872