@sparkvault/sdk 1.21.7 → 1.21.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -710,15 +710,28 @@ class IdentityApi {
710
710
  return (json.data ?? json);
711
711
  }
712
712
  catch (error) {
713
- // Convert AbortError to a more descriptive error
714
- if (error instanceof Error && error.name === 'AbortError') {
713
+ // Convert AbortError to a more descriptive error.
714
+ // Check error.name directly (DOMException may not extend Error in all environments).
715
+ if (error != null && typeof error === 'object' && 'name' in error && error.name === 'AbortError') {
715
716
  // Check which signal triggered the abort
716
717
  if (this.closeController.signal.aborted) {
717
718
  throw new IdentityApiError('Request cancelled', 'cancelled', 0);
718
719
  }
719
720
  throw new IdentityApiError('Request timed out', 'timeout', 0);
720
721
  }
721
- throw error;
722
+ // Convert TypeError to a user-friendly network error.
723
+ // WebKit (Safari/iOS) throws TypeError("Load failed"),
724
+ // Chrome/Firefox throw TypeError("Failed to fetch")
725
+ // when fetch() fails at the browser level (network error, CORS, DNS, etc.).
726
+ if (error instanceof TypeError) {
727
+ throw new IdentityApiError('Unable to connect. Please check your connection and try again.', 'network_error', 0);
728
+ }
729
+ // Re-throw IdentityApiError as-is (already has user-friendly message)
730
+ if (error instanceof IdentityApiError) {
731
+ throw error;
732
+ }
733
+ // Convert any other unexpected errors to a structured error
734
+ throw new IdentityApiError(error instanceof Error ? error.message : 'An unexpected error occurred', 'unknown_error', 0);
722
735
  }
723
736
  finally {
724
737
  clearTimeout(timeoutId);
@@ -727,21 +740,34 @@ class IdentityApi {
727
740
  /**
728
741
  * Fetch SDK configuration (branding, enabled methods).
729
742
  * Uses caching to avoid redundant requests - safe to call multiple times.
743
+ * If a cached request failed (e.g., transient network error during preload),
744
+ * clears the cache and retries to avoid permanently caching a rejected promise.
730
745
  */
731
746
  async getConfig() {
732
747
  if (!this.configCache) {
733
748
  this.configCache = this.request('GET', '/config');
734
749
  }
735
- return this.configCache;
750
+ try {
751
+ return await this.configCache;
752
+ }
753
+ catch (error) {
754
+ // Clear failed cache so the next call retries instead of returning
755
+ // the same rejected promise. This handles transient network failures
756
+ // (e.g., "Load failed" in Safari) that occurred during preloadConfig().
757
+ this.configCache = null;
758
+ throw error;
759
+ }
736
760
  }
737
761
  /**
738
762
  * Preload the SDK configuration in the background.
739
763
  * Called on SDK init when preloadConfig is enabled.
740
764
  * The result is cached and used when verify() is called.
765
+ * If preload fails, getConfig() will clear the cache and retry.
741
766
  */
742
767
  preloadConfig() {
743
768
  if (!this.configCache) {
744
- // Fire and forget - errors are handled when getConfig() is awaited
769
+ // Fire and forget - errors are handled when getConfig() is awaited.
770
+ // If this fails, getConfig() clears the cache and retries.
745
771
  this.configCache = this.request('GET', '/config');
746
772
  }
747
773
  }
@@ -4470,7 +4496,7 @@ class IdentityRenderer {
4470
4496
  if (!config) {
4471
4497
  // Config not ready yet - show loading and await it
4472
4498
  this.setState({ view: 'loading' });
4473
- config = await this.api.getConfig();
4499
+ config = await this.fetchConfigWithRetry();
4474
4500
  }
4475
4501
  this.verificationState.setConfig(config);
4476
4502
  // Update modal header with branding if provided
@@ -4496,6 +4522,27 @@ class IdentityRenderer {
4496
4522
  this.handleErrorWithRecovery(error, 'config_error');
4497
4523
  }
4498
4524
  }
4525
+ /**
4526
+ * Fetch config with a single retry on network errors.
4527
+ * Handles transient failures common during subdomain switches
4528
+ * (DNS resolution, connection setup, Safari ITP).
4529
+ */
4530
+ async fetchConfigWithRetry() {
4531
+ try {
4532
+ return await this.api.getConfig();
4533
+ }
4534
+ catch (error) {
4535
+ // Only retry on network errors (not API errors like 404, 500, etc.)
4536
+ const isNetworkError = error instanceof IdentityApiError && error.code === 'network_error';
4537
+ if (!isNetworkError) {
4538
+ throw error;
4539
+ }
4540
+ // Wait briefly before retry (exponential backoff: 1s)
4541
+ await new Promise((resolve) => setTimeout(resolve, 1000));
4542
+ // Retry once - getConfig() will clear the failed cache and make a fresh request
4543
+ return this.api.getConfig();
4544
+ }
4545
+ }
4499
4546
  /**
4500
4547
  * Close the modal and clean up.
4501
4548
  * Cancels all pending API requests to prevent orphaned operations.