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