@cloudsignal/pwa-sdk 1.0.0 → 1.1.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.
package/dist/index.js CHANGED
@@ -1506,18 +1506,36 @@ var PushNotificationManager = class {
1506
1506
  };
1507
1507
 
1508
1508
  // src/HeartbeatManager.ts
1509
+ var DEFAULT_INTERVALS = {
1510
+ "4g": 3e4,
1511
+ // 30 seconds
1512
+ "3g": 6e4,
1513
+ // 60 seconds
1514
+ "2g": 12e4,
1515
+ // 2 minutes
1516
+ "slow-2g": 18e4,
1517
+ // 3 minutes
1518
+ "unknown": 3e4
1519
+ // Default
1520
+ };
1509
1521
  var HeartbeatManager = class {
1510
1522
  constructor(options) {
1511
1523
  this.registrationId = null;
1512
1524
  this.intervalId = null;
1513
1525
  this.isRunning = false;
1526
+ this.isPausedForBattery = false;
1514
1527
  this.visibilityHandler = null;
1528
+ this.connectionChangeHandler = null;
1529
+ this.batteryManager = null;
1515
1530
  this.serviceUrl = options.serviceUrl;
1516
1531
  this.organizationId = options.organizationId;
1517
1532
  this.organizationSecret = options.organizationSecret;
1518
1533
  this.debug = options.debug ?? false;
1519
1534
  this.onHeartbeatSent = options.onHeartbeatSent;
1520
1535
  this.onHeartbeatError = options.onHeartbeatError;
1536
+ this.onIntervalChanged = options.onIntervalChanged;
1537
+ this.onPausedForBattery = options.onPausedForBattery;
1538
+ this.onResumedFromBattery = options.onResumedFromBattery;
1521
1539
  this.config = {
1522
1540
  enabled: options.config?.enabled ?? true,
1523
1541
  interval: options.config?.interval ?? 3e4,
@@ -1525,6 +1543,16 @@ var HeartbeatManager = class {
1525
1543
  autoStart: options.config?.autoStart ?? true,
1526
1544
  stopOnHidden: options.config?.stopOnHidden ?? true
1527
1545
  };
1546
+ this.adaptiveConfig = {
1547
+ adaptToNetwork: options.adaptiveConfig?.adaptToNetwork ?? true,
1548
+ adaptToBattery: options.adaptiveConfig?.adaptToBattery ?? true,
1549
+ batteryPauseThreshold: options.adaptiveConfig?.batteryPauseThreshold ?? 0.15,
1550
+ intervals: {
1551
+ ...DEFAULT_INTERVALS,
1552
+ ...options.adaptiveConfig?.intervals
1553
+ }
1554
+ };
1555
+ this.currentInterval = this.config.interval;
1528
1556
  }
1529
1557
  /**
1530
1558
  * Set the registration ID for heartbeat requests
@@ -1552,124 +1580,1391 @@ var HeartbeatManager = class {
1552
1580
  return;
1553
1581
  }
1554
1582
  this.isRunning = true;
1555
- this.log(`Starting heartbeat with interval ${this.config.interval}ms`);
1583
+ this.updateIntervalForNetwork();
1584
+ this.log(`Starting heartbeat with interval ${this.currentInterval}ms`);
1556
1585
  this.sendHeartbeat();
1557
- this.intervalId = setInterval(() => {
1558
- this.sendHeartbeat();
1559
- }, this.config.interval);
1586
+ this.startInterval();
1560
1587
  if (this.config.stopOnHidden) {
1561
1588
  this.setupVisibilityHandler();
1562
1589
  }
1590
+ if (this.adaptiveConfig.adaptToNetwork) {
1591
+ this.setupNetworkChangeHandler();
1592
+ }
1593
+ if (this.adaptiveConfig.adaptToBattery) {
1594
+ this.setupBatteryMonitoring();
1595
+ }
1596
+ }
1597
+ /**
1598
+ * Stop sending heartbeats
1599
+ */
1600
+ stop() {
1601
+ if (!this.isRunning) {
1602
+ return;
1603
+ }
1604
+ this.isRunning = false;
1605
+ this.isPausedForBattery = false;
1606
+ if (this.intervalId) {
1607
+ clearInterval(this.intervalId);
1608
+ this.intervalId = null;
1609
+ }
1610
+ if (this.visibilityHandler) {
1611
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
1612
+ this.visibilityHandler = null;
1613
+ }
1614
+ if (this.connectionChangeHandler) {
1615
+ const connection = navigator.connection;
1616
+ if (connection) {
1617
+ connection.removeEventListener("change", this.connectionChangeHandler);
1618
+ }
1619
+ this.connectionChangeHandler = null;
1620
+ }
1621
+ this.log("Heartbeat stopped");
1622
+ }
1623
+ /**
1624
+ * Check if heartbeat is running
1625
+ */
1626
+ isHeartbeatRunning() {
1627
+ return this.isRunning;
1628
+ }
1629
+ /**
1630
+ * Send a single heartbeat
1631
+ */
1632
+ async sendHeartbeat() {
1633
+ if (!this.registrationId || !isValidUUID(this.registrationId)) {
1634
+ this.log("Cannot send heartbeat: no valid registration ID", "warn");
1635
+ return false;
1636
+ }
1637
+ try {
1638
+ const url = `${this.serviceUrl}/api/v1/registration/heartbeat/${this.registrationId}`;
1639
+ const response = await makeAuthenticatedRequest(
1640
+ this.organizationId,
1641
+ this.organizationSecret,
1642
+ "POST",
1643
+ url
1644
+ );
1645
+ if (!response.ok) {
1646
+ throw new Error(`Heartbeat failed: ${response.status}`);
1647
+ }
1648
+ this.log("Heartbeat sent successfully");
1649
+ this.onHeartbeatSent?.();
1650
+ return true;
1651
+ } catch (error) {
1652
+ const err = error instanceof Error ? error : new Error(String(error));
1653
+ this.log(`Heartbeat failed: ${err.message}`, "error");
1654
+ this.onHeartbeatError?.(err);
1655
+ return false;
1656
+ }
1657
+ }
1658
+ /**
1659
+ * Update heartbeat configuration
1660
+ */
1661
+ updateConfig(config) {
1662
+ const wasRunning = this.isRunning;
1663
+ if (wasRunning) {
1664
+ this.stop();
1665
+ }
1666
+ this.config = {
1667
+ ...this.config,
1668
+ ...config
1669
+ };
1670
+ if (wasRunning && this.config.enabled) {
1671
+ this.start();
1672
+ }
1673
+ }
1674
+ /**
1675
+ * Get current network connection info
1676
+ */
1677
+ getNetworkInfo() {
1678
+ const connection = navigator.connection;
1679
+ if (!connection) {
1680
+ return {};
1681
+ }
1682
+ return {
1683
+ effectiveType: connection.effectiveType,
1684
+ saveData: connection.saveData,
1685
+ downlink: connection.downlink,
1686
+ rtt: connection.rtt
1687
+ };
1688
+ }
1689
+ /**
1690
+ * Get current battery info (if available)
1691
+ */
1692
+ async getBatteryInfo() {
1693
+ try {
1694
+ if (!("getBattery" in navigator)) {
1695
+ return null;
1696
+ }
1697
+ const battery = await navigator.getBattery();
1698
+ return {
1699
+ level: battery.level,
1700
+ charging: battery.charging
1701
+ };
1702
+ } catch {
1703
+ return null;
1704
+ }
1705
+ }
1706
+ /**
1707
+ * Get current heartbeat interval
1708
+ */
1709
+ getCurrentInterval() {
1710
+ return this.currentInterval;
1711
+ }
1712
+ /**
1713
+ * Check if heartbeat is paused due to low battery
1714
+ */
1715
+ isPausedDueToBattery() {
1716
+ return this.isPausedForBattery;
1717
+ }
1718
+ // ============================================
1719
+ // Private interval management
1720
+ // ============================================
1721
+ /**
1722
+ * Start the heartbeat interval
1723
+ */
1724
+ startInterval() {
1725
+ if (this.intervalId) {
1726
+ clearInterval(this.intervalId);
1727
+ }
1728
+ this.intervalId = setInterval(() => {
1729
+ this.sendHeartbeat();
1730
+ }, this.currentInterval);
1731
+ }
1732
+ /**
1733
+ * Update interval based on network conditions
1734
+ */
1735
+ updateIntervalForNetwork() {
1736
+ if (!this.adaptiveConfig.adaptToNetwork) {
1737
+ this.currentInterval = this.config.interval;
1738
+ return;
1739
+ }
1740
+ const connection = navigator.connection;
1741
+ if (!connection) {
1742
+ this.currentInterval = this.config.interval;
1743
+ return;
1744
+ }
1745
+ if (connection.saveData) {
1746
+ const newInterval2 = Math.max(this.currentInterval, 12e4);
1747
+ if (newInterval2 !== this.currentInterval) {
1748
+ this.currentInterval = newInterval2;
1749
+ this.log(`Interval increased to ${newInterval2}ms (saveData enabled)`);
1750
+ this.onIntervalChanged?.(newInterval2, "saveData");
1751
+ }
1752
+ return;
1753
+ }
1754
+ const effectiveType = connection.effectiveType || "unknown";
1755
+ const newInterval = this.adaptiveConfig.intervals[effectiveType] || this.config.interval;
1756
+ if (newInterval !== this.currentInterval) {
1757
+ const oldInterval = this.currentInterval;
1758
+ this.currentInterval = newInterval;
1759
+ this.log(`Interval changed from ${oldInterval}ms to ${newInterval}ms (${effectiveType})`);
1760
+ this.onIntervalChanged?.(newInterval, effectiveType);
1761
+ }
1762
+ }
1763
+ /**
1764
+ * Set up network change handler
1765
+ */
1766
+ setupNetworkChangeHandler() {
1767
+ const connection = navigator.connection;
1768
+ if (!connection) {
1769
+ return;
1770
+ }
1771
+ this.connectionChangeHandler = () => {
1772
+ const oldInterval = this.currentInterval;
1773
+ this.updateIntervalForNetwork();
1774
+ if (oldInterval !== this.currentInterval && this.isRunning && !this.isPausedForBattery) {
1775
+ this.startInterval();
1776
+ }
1777
+ };
1778
+ connection.addEventListener("change", this.connectionChangeHandler);
1779
+ }
1780
+ /**
1781
+ * Set up battery monitoring
1782
+ */
1783
+ async setupBatteryMonitoring() {
1784
+ try {
1785
+ if (!("getBattery" in navigator)) {
1786
+ return;
1787
+ }
1788
+ this.batteryManager = await navigator.getBattery();
1789
+ const checkBattery = () => {
1790
+ if (!this.batteryManager) return;
1791
+ const level = this.batteryManager.level;
1792
+ const charging = this.batteryManager.charging;
1793
+ if (level <= this.adaptiveConfig.batteryPauseThreshold && !charging) {
1794
+ if (!this.isPausedForBattery && this.isRunning) {
1795
+ this.isPausedForBattery = true;
1796
+ if (this.intervalId) {
1797
+ clearInterval(this.intervalId);
1798
+ this.intervalId = null;
1799
+ }
1800
+ this.log(`Heartbeat paused (battery at ${Math.round(level * 100)}%)`);
1801
+ this.onPausedForBattery?.();
1802
+ }
1803
+ } else {
1804
+ if (this.isPausedForBattery && this.isRunning) {
1805
+ this.isPausedForBattery = false;
1806
+ this.startInterval();
1807
+ this.log(`Heartbeat resumed (battery at ${Math.round(level * 100)}%)`);
1808
+ this.onResumedFromBattery?.();
1809
+ }
1810
+ }
1811
+ };
1812
+ checkBattery();
1813
+ this.batteryManager.addEventListener("levelchange", checkBattery);
1814
+ this.batteryManager.addEventListener("chargingchange", checkBattery);
1815
+ } catch {
1816
+ }
1817
+ }
1818
+ /**
1819
+ * Set up visibility change handler
1820
+ */
1821
+ setupVisibilityHandler() {
1822
+ this.visibilityHandler = () => {
1823
+ if (document.hidden) {
1824
+ if (this.intervalId) {
1825
+ clearInterval(this.intervalId);
1826
+ this.intervalId = null;
1827
+ }
1828
+ this.log("Heartbeat paused (page hidden)");
1829
+ } else {
1830
+ if (this.isRunning && !this.intervalId && !this.isPausedForBattery) {
1831
+ this.sendHeartbeat();
1832
+ this.startInterval();
1833
+ this.log("Heartbeat resumed (page visible)");
1834
+ }
1835
+ }
1836
+ };
1837
+ document.addEventListener("visibilitychange", this.visibilityHandler);
1838
+ }
1839
+ /**
1840
+ * Log message if debug is enabled
1841
+ */
1842
+ log(message, level = "log") {
1843
+ if (!this.debug) return;
1844
+ const prefix = "[CloudSignal PWA Heartbeat]";
1845
+ console[level](`${prefix} ${message}`);
1846
+ }
1847
+ };
1848
+
1849
+ // src/WakeLockManager.ts
1850
+ var WakeLockManager = class {
1851
+ constructor(options = {}) {
1852
+ this.wakeLock = null;
1853
+ this.visibilityHandler = null;
1854
+ this.releaseHandler = null;
1855
+ this.shouldReacquire = false;
1856
+ this.debug = options.debug ?? false;
1857
+ this.onAcquired = options.onAcquired;
1858
+ this.onReleased = options.onReleased;
1859
+ this.onError = options.onError;
1860
+ this.config = {
1861
+ enabled: options.config?.enabled ?? true,
1862
+ autoReacquire: options.config?.autoReacquire ?? true
1863
+ };
1864
+ }
1865
+ /**
1866
+ * Check if Screen Wake Lock API is supported
1867
+ */
1868
+ isSupported() {
1869
+ return "wakeLock" in navigator;
1870
+ }
1871
+ /**
1872
+ * Get current wake lock state
1873
+ */
1874
+ getState() {
1875
+ return {
1876
+ isActive: this.wakeLock !== null && !this.wakeLock.released,
1877
+ isSupported: this.isSupported(),
1878
+ type: "screen",
1879
+ acquiredAt: this.wakeLock && !this.wakeLock.released ? Date.now() : void 0
1880
+ };
1881
+ }
1882
+ /**
1883
+ * Acquire a screen wake lock
1884
+ * Prevents the screen from dimming or locking
1885
+ */
1886
+ async acquire() {
1887
+ if (!this.config.enabled) {
1888
+ this.log("Wake lock is disabled");
1889
+ return false;
1890
+ }
1891
+ if (!this.isSupported()) {
1892
+ this.log("Wake Lock API not supported");
1893
+ this.onError?.({
1894
+ timestamp: Date.now(),
1895
+ error: "Wake Lock API not supported",
1896
+ errorName: "NotSupportedError"
1897
+ });
1898
+ return false;
1899
+ }
1900
+ if (this.wakeLock && !this.wakeLock.released) {
1901
+ this.log("Wake lock already active");
1902
+ return true;
1903
+ }
1904
+ try {
1905
+ this.wakeLock = await navigator.wakeLock.request("screen");
1906
+ this.shouldReacquire = true;
1907
+ this.releaseHandler = () => {
1908
+ this.handleRelease("system");
1909
+ };
1910
+ this.wakeLock.addEventListener("release", this.releaseHandler);
1911
+ if (this.config.autoReacquire) {
1912
+ this.setupVisibilityHandler();
1913
+ }
1914
+ this.log("Wake lock acquired");
1915
+ this.onAcquired?.({ timestamp: Date.now() });
1916
+ return true;
1917
+ } catch (error) {
1918
+ const err = error instanceof Error ? error : new Error(String(error));
1919
+ this.log(`Failed to acquire wake lock: ${err.message}`, "error");
1920
+ this.onError?.({
1921
+ timestamp: Date.now(),
1922
+ error: err.message,
1923
+ errorName: err.name
1924
+ });
1925
+ return false;
1926
+ }
1927
+ }
1928
+ /**
1929
+ * Release the screen wake lock
1930
+ */
1931
+ async release() {
1932
+ this.shouldReacquire = false;
1933
+ if (!this.wakeLock) {
1934
+ return;
1935
+ }
1936
+ try {
1937
+ if (this.releaseHandler) {
1938
+ this.wakeLock.removeEventListener("release", this.releaseHandler);
1939
+ this.releaseHandler = null;
1940
+ }
1941
+ await this.wakeLock.release();
1942
+ this.wakeLock = null;
1943
+ this.log("Wake lock released manually");
1944
+ this.onReleased?.({ timestamp: Date.now(), reason: "manual" });
1945
+ } catch (error) {
1946
+ const err = error instanceof Error ? error : new Error(String(error));
1947
+ this.log(`Failed to release wake lock: ${err.message}`, "error");
1948
+ }
1949
+ this.removeVisibilityHandler();
1950
+ }
1951
+ /**
1952
+ * Toggle wake lock on/off
1953
+ */
1954
+ async toggle() {
1955
+ if (this.wakeLock && !this.wakeLock.released) {
1956
+ await this.release();
1957
+ return false;
1958
+ } else {
1959
+ return this.acquire();
1960
+ }
1961
+ }
1962
+ /**
1963
+ * Check if wake lock is currently active
1964
+ */
1965
+ isActive() {
1966
+ return this.wakeLock !== null && !this.wakeLock.released;
1967
+ }
1968
+ /**
1969
+ * Destroy the manager and release any held locks
1970
+ */
1971
+ async destroy() {
1972
+ await this.release();
1973
+ this.removeVisibilityHandler();
1974
+ }
1975
+ // ============================================
1976
+ // Private methods
1977
+ // ============================================
1978
+ /**
1979
+ * Handle wake lock release (from system)
1980
+ */
1981
+ handleRelease(reason) {
1982
+ this.wakeLock = null;
1983
+ this.log(`Wake lock released by ${reason}`);
1984
+ this.onReleased?.({ timestamp: Date.now(), reason });
1985
+ }
1986
+ /**
1987
+ * Set up visibility change handler for auto-reacquire
1988
+ */
1989
+ setupVisibilityHandler() {
1990
+ if (this.visibilityHandler) {
1991
+ return;
1992
+ }
1993
+ this.visibilityHandler = async () => {
1994
+ if (document.visibilityState === "visible" && this.shouldReacquire) {
1995
+ this.log("Page visible, attempting to reacquire wake lock");
1996
+ await this.acquire();
1997
+ } else if (document.visibilityState === "hidden" && this.wakeLock) {
1998
+ this.log("Page hidden, wake lock will be released");
1999
+ }
2000
+ };
2001
+ document.addEventListener("visibilitychange", this.visibilityHandler);
2002
+ }
2003
+ /**
2004
+ * Remove visibility change handler
2005
+ */
2006
+ removeVisibilityHandler() {
2007
+ if (this.visibilityHandler) {
2008
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
2009
+ this.visibilityHandler = null;
2010
+ }
2011
+ }
2012
+ /**
2013
+ * Log message if debug is enabled
2014
+ */
2015
+ log(message, level = "log") {
2016
+ if (!this.debug && level === "log") return;
2017
+ const prefix = "[CloudSignal PWA WakeLock]";
2018
+ console[level](`${prefix} ${message}`);
2019
+ }
2020
+ };
2021
+
2022
+ // src/OfflineQueueManager.ts
2023
+ var DB_NAME = "CloudSignalPWA";
2024
+ var DB_VERSION = 2;
2025
+ var STORE_NAME = "offlineQueue";
2026
+ var OfflineQueueManager = class {
2027
+ constructor(options = {}) {
2028
+ this.db = null;
2029
+ this.isProcessing = false;
2030
+ this.onlineHandler = null;
2031
+ this.debug = options.debug ?? false;
2032
+ this.onRequestQueued = options.onRequestQueued;
2033
+ this.onRequestProcessed = options.onRequestProcessed;
2034
+ this.onQueueEmpty = options.onQueueEmpty;
2035
+ this.onError = options.onError;
2036
+ this.config = {
2037
+ enabled: options.config?.enabled ?? true,
2038
+ maxQueueSize: options.config?.maxQueueSize ?? 100,
2039
+ maxAge: options.config?.maxAge ?? 24 * 60 * 60 * 1e3,
2040
+ // 24 hours
2041
+ baseRetryDelay: options.config?.baseRetryDelay ?? 1e3,
2042
+ maxRetryDelay: options.config?.maxRetryDelay ?? 6e4,
2043
+ defaultMaxRetries: options.config?.defaultMaxRetries ?? 5,
2044
+ processOnOnline: options.config?.processOnOnline ?? true
2045
+ };
2046
+ }
2047
+ /**
2048
+ * Initialize the offline queue manager
2049
+ */
2050
+ async initialize() {
2051
+ if (!this.config.enabled) {
2052
+ this.log("Offline queue is disabled");
2053
+ return;
2054
+ }
2055
+ try {
2056
+ await this.openDatabase();
2057
+ if (this.config.processOnOnline) {
2058
+ this.onlineHandler = () => {
2059
+ this.log("Network online, processing queue");
2060
+ this.processQueue();
2061
+ };
2062
+ window.addEventListener("online", this.onlineHandler);
2063
+ }
2064
+ await this.cleanupOldRequests();
2065
+ this.log("Offline queue manager initialized");
2066
+ } catch (error) {
2067
+ const err = error instanceof Error ? error : new Error(String(error));
2068
+ this.log(`Failed to initialize: ${err.message}`, "error");
2069
+ this.onError?.(err);
2070
+ }
2071
+ }
2072
+ /**
2073
+ * Destroy the manager and clean up listeners
2074
+ */
2075
+ destroy() {
2076
+ if (this.onlineHandler) {
2077
+ window.removeEventListener("online", this.onlineHandler);
2078
+ this.onlineHandler = null;
2079
+ }
2080
+ if (this.db) {
2081
+ this.db.close();
2082
+ this.db = null;
2083
+ }
2084
+ }
2085
+ /**
2086
+ * Add a request to the queue
2087
+ */
2088
+ async queueRequest(url, method, options = {}) {
2089
+ if (!this.config.enabled || !this.db) {
2090
+ return null;
2091
+ }
2092
+ try {
2093
+ const stats = await this.getStats();
2094
+ if (stats.totalQueued >= this.config.maxQueueSize) {
2095
+ this.log("Queue is full, removing oldest request");
2096
+ await this.removeOldestRequest();
2097
+ }
2098
+ const request = {
2099
+ url,
2100
+ method,
2101
+ headers: options.headers,
2102
+ body: options.body,
2103
+ queuedAt: Date.now(),
2104
+ retryCount: 0,
2105
+ maxRetries: options.maxRetries ?? this.config.defaultMaxRetries,
2106
+ requestType: options.requestType ?? "custom",
2107
+ priority: options.priority ?? 0,
2108
+ metadata: options.metadata
2109
+ };
2110
+ const id = await this.addToStore(request);
2111
+ request.id = id;
2112
+ this.log(`Queued request: ${method} ${url} (id: ${id})`);
2113
+ this.onRequestQueued?.(request);
2114
+ return id;
2115
+ } catch (error) {
2116
+ const err = error instanceof Error ? error : new Error(String(error));
2117
+ this.log(`Failed to queue request: ${err.message}`, "error");
2118
+ this.onError?.(err);
2119
+ return null;
2120
+ }
2121
+ }
2122
+ /**
2123
+ * Process all queued requests
2124
+ */
2125
+ async processQueue() {
2126
+ if (!this.config.enabled || !this.db || this.isProcessing) {
2127
+ return [];
2128
+ }
2129
+ if (!navigator.onLine) {
2130
+ this.log("Offline, skipping queue processing");
2131
+ return [];
2132
+ }
2133
+ this.isProcessing = true;
2134
+ const results = [];
2135
+ try {
2136
+ const requests = await this.getAllRequests();
2137
+ requests.sort((a, b) => {
2138
+ if (a.priority !== b.priority) {
2139
+ return b.priority - a.priority;
2140
+ }
2141
+ return a.queuedAt - b.queuedAt;
2142
+ });
2143
+ this.log(`Processing ${requests.length} queued requests`);
2144
+ for (const request of requests) {
2145
+ if (!navigator.onLine) {
2146
+ this.log("Went offline during processing, stopping");
2147
+ break;
2148
+ }
2149
+ const result = await this.processRequest(request);
2150
+ results.push(result);
2151
+ if (result.success || !result.shouldRetry) {
2152
+ await this.removeFromStore(request.id);
2153
+ } else {
2154
+ await this.updateRetryCount(request);
2155
+ }
2156
+ this.onRequestProcessed?.(result);
2157
+ await this.delay(100);
2158
+ }
2159
+ const remainingCount = await this.getQueueCount();
2160
+ if (remainingCount === 0) {
2161
+ this.onQueueEmpty?.();
2162
+ }
2163
+ this.log(`Processed ${results.length} requests, ${remainingCount} remaining`);
2164
+ } catch (error) {
2165
+ const err = error instanceof Error ? error : new Error(String(error));
2166
+ this.log(`Error processing queue: ${err.message}`, "error");
2167
+ this.onError?.(err);
2168
+ } finally {
2169
+ this.isProcessing = false;
2170
+ }
2171
+ return results;
2172
+ }
2173
+ /**
2174
+ * Process a single queued request
2175
+ */
2176
+ async processRequest(request) {
2177
+ const result = {
2178
+ id: request.id,
2179
+ success: false,
2180
+ shouldRetry: false
2181
+ };
2182
+ try {
2183
+ const response = await fetch(request.url, {
2184
+ method: request.method,
2185
+ headers: request.headers,
2186
+ body: request.body
2187
+ });
2188
+ result.statusCode = response.status;
2189
+ result.success = response.ok;
2190
+ if (!response.ok) {
2191
+ result.shouldRetry = this.shouldRetryStatus(response.status, request);
2192
+ result.error = `HTTP ${response.status}`;
2193
+ }
2194
+ this.log(`Request ${request.id}: ${result.success ? "success" : "failed"} (${response.status})`);
2195
+ } catch (error) {
2196
+ const err = error instanceof Error ? error : new Error(String(error));
2197
+ result.error = err.message;
2198
+ result.shouldRetry = request.retryCount < request.maxRetries;
2199
+ this.log(`Request ${request.id} failed: ${err.message}`, "error");
2200
+ }
2201
+ return result;
2202
+ }
2203
+ /**
2204
+ * Determine if a request should be retried based on status code
2205
+ */
2206
+ shouldRetryStatus(status, request) {
2207
+ if (request.retryCount >= request.maxRetries) {
2208
+ return false;
2209
+ }
2210
+ if (status >= 500) return true;
2211
+ if (status === 408) return true;
2212
+ if (status === 429) return true;
2213
+ if (status === 0) return true;
2214
+ return false;
2215
+ }
2216
+ /**
2217
+ * Update retry count and schedule next retry
2218
+ */
2219
+ async updateRetryCount(request) {
2220
+ request.retryCount++;
2221
+ const transaction = this.db.transaction([STORE_NAME], "readwrite");
2222
+ const store = transaction.objectStore(STORE_NAME);
2223
+ await this.promisifyRequest(store.put(request));
2224
+ }
2225
+ /**
2226
+ * Get queue statistics
2227
+ */
2228
+ async getStats() {
2229
+ const stats = {
2230
+ totalQueued: 0,
2231
+ byType: {
2232
+ registration: 0,
2233
+ heartbeat: 0,
2234
+ analytics: 0,
2235
+ preferences: 0,
2236
+ unregister: 0,
2237
+ custom: 0
2238
+ }
2239
+ };
2240
+ if (!this.db) {
2241
+ return stats;
2242
+ }
2243
+ try {
2244
+ const requests = await this.getAllRequests();
2245
+ stats.totalQueued = requests.length;
2246
+ for (const request of requests) {
2247
+ stats.byType[request.requestType]++;
2248
+ if (!stats.oldestRequest || request.queuedAt < stats.oldestRequest) {
2249
+ stats.oldestRequest = request.queuedAt;
2250
+ }
2251
+ if (!stats.newestRequest || request.queuedAt > stats.newestRequest) {
2252
+ stats.newestRequest = request.queuedAt;
2253
+ }
2254
+ }
2255
+ } catch (error) {
2256
+ this.log(`Failed to get stats: ${error}`, "error");
2257
+ }
2258
+ return stats;
2259
+ }
2260
+ /**
2261
+ * Clear all queued requests
2262
+ */
2263
+ async clearQueue() {
2264
+ if (!this.db) return;
2265
+ const transaction = this.db.transaction([STORE_NAME], "readwrite");
2266
+ const store = transaction.objectStore(STORE_NAME);
2267
+ await this.promisifyRequest(store.clear());
2268
+ this.log("Queue cleared");
2269
+ }
2270
+ /**
2271
+ * Check if queue has pending requests
2272
+ */
2273
+ async hasPendingRequests() {
2274
+ const count = await this.getQueueCount();
2275
+ return count > 0;
2276
+ }
2277
+ /**
2278
+ * Get number of queued requests
2279
+ */
2280
+ async getQueueCount() {
2281
+ if (!this.db) return 0;
2282
+ const transaction = this.db.transaction([STORE_NAME], "readonly");
2283
+ const store = transaction.objectStore(STORE_NAME);
2284
+ return this.promisifyRequest(store.count());
2285
+ }
2286
+ // ============================================
2287
+ // Private IndexedDB methods
2288
+ // ============================================
2289
+ async openDatabase() {
2290
+ return new Promise((resolve, reject) => {
2291
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
2292
+ request.onerror = () => reject(request.error);
2293
+ request.onsuccess = () => {
2294
+ this.db = request.result;
2295
+ resolve();
2296
+ };
2297
+ request.onupgradeneeded = (event) => {
2298
+ const db = event.target.result;
2299
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
2300
+ const store = db.createObjectStore(STORE_NAME, {
2301
+ keyPath: "id",
2302
+ autoIncrement: true
2303
+ });
2304
+ store.createIndex("queuedAt", "queuedAt", { unique: false });
2305
+ store.createIndex("requestType", "requestType", { unique: false });
2306
+ store.createIndex("priority", "priority", { unique: false });
2307
+ }
2308
+ if (!db.objectStoreNames.contains("badge")) {
2309
+ db.createObjectStore("badge");
2310
+ }
2311
+ if (!db.objectStoreNames.contains("notifications")) {
2312
+ const notificationStore = db.createObjectStore("notifications", {
2313
+ keyPath: "id",
2314
+ autoIncrement: true
2315
+ });
2316
+ notificationStore.createIndex("timestamp", "timestamp", { unique: false });
2317
+ notificationStore.createIndex("read", "read", { unique: false });
2318
+ }
2319
+ if (!db.objectStoreNames.contains("userPreferences")) {
2320
+ db.createObjectStore("userPreferences");
2321
+ }
2322
+ if (!db.objectStoreNames.contains("syncQueue")) {
2323
+ const syncStore = db.createObjectStore("syncQueue", {
2324
+ keyPath: "id",
2325
+ autoIncrement: true
2326
+ });
2327
+ syncStore.createIndex("timestamp", "timestamp", { unique: false });
2328
+ }
2329
+ };
2330
+ });
2331
+ }
2332
+ async addToStore(request) {
2333
+ const transaction = this.db.transaction([STORE_NAME], "readwrite");
2334
+ const store = transaction.objectStore(STORE_NAME);
2335
+ return this.promisifyRequest(store.add(request));
2336
+ }
2337
+ async removeFromStore(id) {
2338
+ const transaction = this.db.transaction([STORE_NAME], "readwrite");
2339
+ const store = transaction.objectStore(STORE_NAME);
2340
+ await this.promisifyRequest(store.delete(id));
2341
+ }
2342
+ async getAllRequests() {
2343
+ const transaction = this.db.transaction([STORE_NAME], "readonly");
2344
+ const store = transaction.objectStore(STORE_NAME);
2345
+ return this.promisifyRequest(store.getAll());
2346
+ }
2347
+ async removeOldestRequest() {
2348
+ const transaction = this.db.transaction([STORE_NAME], "readwrite");
2349
+ const store = transaction.objectStore(STORE_NAME);
2350
+ const index = store.index("queuedAt");
2351
+ const cursor = index.openCursor();
2352
+ return new Promise((resolve, reject) => {
2353
+ cursor.onsuccess = (event) => {
2354
+ const result = event.target.result;
2355
+ if (result) {
2356
+ store.delete(result.primaryKey);
2357
+ resolve();
2358
+ } else {
2359
+ resolve();
2360
+ }
2361
+ };
2362
+ cursor.onerror = () => reject(cursor.error);
2363
+ });
2364
+ }
2365
+ async cleanupOldRequests() {
2366
+ const cutoffTime = Date.now() - this.config.maxAge;
2367
+ const transaction = this.db.transaction([STORE_NAME], "readwrite");
2368
+ const store = transaction.objectStore(STORE_NAME);
2369
+ const index = store.index("queuedAt");
2370
+ const range = IDBKeyRange.upperBound(cutoffTime);
2371
+ const cursor = index.openCursor(range);
2372
+ let deletedCount = 0;
2373
+ return new Promise((resolve) => {
2374
+ cursor.onsuccess = (event) => {
2375
+ const result = event.target.result;
2376
+ if (result) {
2377
+ store.delete(result.primaryKey);
2378
+ deletedCount++;
2379
+ result.continue();
2380
+ } else {
2381
+ if (deletedCount > 0) {
2382
+ this.log(`Cleaned up ${deletedCount} old requests`);
2383
+ }
2384
+ resolve();
2385
+ }
2386
+ };
2387
+ cursor.onerror = () => resolve();
2388
+ });
2389
+ }
2390
+ promisifyRequest(request) {
2391
+ return new Promise((resolve, reject) => {
2392
+ request.onsuccess = () => resolve(request.result);
2393
+ request.onerror = () => reject(request.error);
2394
+ });
2395
+ }
2396
+ delay(ms) {
2397
+ return new Promise((resolve) => setTimeout(resolve, ms));
2398
+ }
2399
+ log(message, level = "log") {
2400
+ if (!this.debug && level === "log") return;
2401
+ const prefix = "[CloudSignal PWA OfflineQueue]";
2402
+ console[level](`${prefix} ${message}`);
2403
+ }
2404
+ };
2405
+
2406
+ // src/IOSInstallBannerI18n.ts
2407
+ var IOS_BANNER_TRANSLATIONS = {
2408
+ en: {
2409
+ title: "Install this app",
2410
+ description: "Add this app to your home screen for the best experience.",
2411
+ buttonText: "Show me how",
2412
+ closeText: "Not now",
2413
+ step1: "Tap the Share button at the bottom of Safari",
2414
+ step2: 'Scroll down and tap "Add to Home Screen"',
2415
+ step3: 'Tap "Add" in the top right corner',
2416
+ shareIconHint: "Look for the Share icon",
2417
+ shareIconDescription: "It's the square with an arrow pointing up"
2418
+ },
2419
+ es: {
2420
+ title: "Instalar esta aplicaci\xF3n",
2421
+ description: "A\xF1ade esta aplicaci\xF3n a tu pantalla de inicio para una mejor experiencia.",
2422
+ buttonText: "Mostrar c\xF3mo",
2423
+ closeText: "Ahora no",
2424
+ step1: "Toca el bot\xF3n Compartir en la parte inferior de Safari",
2425
+ step2: 'Despl\xE1zate hacia abajo y toca "A\xF1adir a pantalla de inicio"',
2426
+ step3: 'Toca "A\xF1adir" en la esquina superior derecha',
2427
+ shareIconHint: "Busca el icono de Compartir",
2428
+ shareIconDescription: "Es el cuadrado con una flecha apuntando hacia arriba"
2429
+ },
2430
+ fr: {
2431
+ title: "Installer cette application",
2432
+ description: "Ajoutez cette application \xE0 votre \xE9cran d'accueil pour une meilleure exp\xE9rience.",
2433
+ buttonText: "Montrez-moi comment",
2434
+ closeText: "Pas maintenant",
2435
+ step1: "Appuyez sur le bouton Partager en bas de Safari",
2436
+ step2: `Faites d\xE9filer vers le bas et appuyez sur "Sur l'\xE9cran d'accueil"`,
2437
+ step3: 'Appuyez sur "Ajouter" dans le coin sup\xE9rieur droit',
2438
+ shareIconHint: "Recherchez l'ic\xF4ne Partager",
2439
+ shareIconDescription: "C'est le carr\xE9 avec une fl\xE8che pointant vers le haut"
2440
+ },
2441
+ de: {
2442
+ title: "Diese App installieren",
2443
+ description: "F\xFCgen Sie diese App zu Ihrem Startbildschirm hinzu f\xFCr das beste Erlebnis.",
2444
+ buttonText: "Zeig mir wie",
2445
+ closeText: "Nicht jetzt",
2446
+ step1: "Tippen Sie auf die Teilen-Schaltfl\xE4che unten in Safari",
2447
+ step2: 'Scrollen Sie nach unten und tippen Sie auf "Zum Home-Bildschirm"',
2448
+ step3: 'Tippen Sie oben rechts auf "Hinzuf\xFCgen"',
2449
+ shareIconHint: "Suchen Sie das Teilen-Symbol",
2450
+ shareIconDescription: "Es ist das Quadrat mit einem Pfeil nach oben"
2451
+ },
2452
+ it: {
2453
+ title: "Installa questa app",
2454
+ description: "Aggiungi questa app alla schermata Home per un'esperienza migliore.",
2455
+ buttonText: "Mostrami come",
2456
+ closeText: "Non ora",
2457
+ step1: "Tocca il pulsante Condividi nella parte inferiore di Safari",
2458
+ step2: 'Scorri verso il basso e tocca "Aggiungi a Home"',
2459
+ step3: `Tocca "Aggiungi" nell'angolo in alto a destra`,
2460
+ shareIconHint: "Cerca l'icona Condividi",
2461
+ shareIconDescription: "\xC8 il quadrato con una freccia che punta verso l'alto"
2462
+ },
2463
+ pt: {
2464
+ title: "Instalar este aplicativo",
2465
+ description: "Adicione este aplicativo \xE0 tela inicial para a melhor experi\xEAncia.",
2466
+ buttonText: "Mostre-me como",
2467
+ closeText: "Agora n\xE3o",
2468
+ step1: "Toque no bot\xE3o Compartilhar na parte inferior do Safari",
2469
+ step2: 'Role para baixo e toque em "Adicionar \xE0 Tela de In\xEDcio"',
2470
+ step3: 'Toque em "Adicionar" no canto superior direito',
2471
+ shareIconHint: "Procure o \xEDcone Compartilhar",
2472
+ shareIconDescription: "\xC9 o quadrado com uma seta apontando para cima"
2473
+ },
2474
+ nl: {
2475
+ title: "Installeer deze app",
2476
+ description: "Voeg deze app toe aan je beginscherm voor de beste ervaring.",
2477
+ buttonText: "Laat me zien hoe",
2478
+ closeText: "Niet nu",
2479
+ step1: "Tik op de Deel-knop onderaan Safari",
2480
+ step2: 'Scroll naar beneden en tik op "Zet op beginscherm"',
2481
+ step3: 'Tik rechtsboven op "Voeg toe"',
2482
+ shareIconHint: "Zoek het Deel-icoon",
2483
+ shareIconDescription: "Het is het vierkant met een pijl omhoog"
2484
+ },
2485
+ ru: {
2486
+ title: "\u0423\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435",
2487
+ description: "\u0414\u043E\u0431\u0430\u0432\u044C\u0442\u0435 \u044D\u0442\u043E \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u043D\u0430 \u0433\u043B\u0430\u0432\u043D\u044B\u0439 \u044D\u043A\u0440\u0430\u043D \u0434\u043B\u044F \u043B\u0443\u0447\u0448\u0435\u0433\u043E \u043E\u043F\u044B\u0442\u0430.",
2488
+ buttonText: "\u041F\u043E\u043A\u0430\u0436\u0438\u0442\u0435 \u043A\u0430\u043A",
2489
+ closeText: "\u041D\u0435 \u0441\u0435\u0439\u0447\u0430\u0441",
2490
+ step1: '\u041D\u0430\u0436\u043C\u0438\u0442\u0435 \u043A\u043D\u043E\u043F\u043A\u0443 "\u041F\u043E\u0434\u0435\u043B\u0438\u0442\u044C\u0441\u044F" \u0432\u043D\u0438\u0437\u0443 Safari',
2491
+ step2: '\u041F\u0440\u043E\u043A\u0440\u0443\u0442\u0438\u0442\u0435 \u0432\u043D\u0438\u0437 \u0438 \u043D\u0430\u0436\u043C\u0438\u0442\u0435 "\u041D\u0430 \u044D\u043A\u0440\u0430\u043D \u0414\u043E\u043C\u043E\u0439"',
2492
+ step3: '\u041D\u0430\u0436\u043C\u0438\u0442\u0435 "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C" \u0432 \u043F\u0440\u0430\u0432\u043E\u043C \u0432\u0435\u0440\u0445\u043D\u0435\u043C \u0443\u0433\u043B\u0443',
2493
+ shareIconHint: '\u041D\u0430\u0439\u0434\u0438\u0442\u0435 \u0437\u043D\u0430\u0447\u043E\u043A "\u041F\u043E\u0434\u0435\u043B\u0438\u0442\u044C\u0441\u044F"',
2494
+ shareIconDescription: "\u042D\u0442\u043E \u043A\u0432\u0430\u0434\u0440\u0430\u0442 \u0441\u043E \u0441\u0442\u0440\u0435\u043B\u043A\u043E\u0439 \u0432\u0432\u0435\u0440\u0445"
2495
+ },
2496
+ zh: {
2497
+ title: "\u5B89\u88C5\u6B64\u5E94\u7528",
2498
+ description: "\u5C06\u6B64\u5E94\u7528\u6DFB\u52A0\u5230\u4E3B\u5C4F\u5E55\u4EE5\u83B7\u5F97\u6700\u4F73\u4F53\u9A8C\u3002",
2499
+ buttonText: "\u663E\u793A\u65B9\u6CD5",
2500
+ closeText: "\u6682\u65F6\u4E0D\u8981",
2501
+ step1: "\u70B9\u51FBSafari\u5E95\u90E8\u7684\u5206\u4EAB\u6309\u94AE",
2502
+ step2: '\u5411\u4E0B\u6EDA\u52A8\u5E76\u70B9\u51FB"\u6DFB\u52A0\u5230\u4E3B\u5C4F\u5E55"',
2503
+ step3: '\u70B9\u51FB\u53F3\u4E0A\u89D2\u7684"\u6DFB\u52A0"',
2504
+ shareIconHint: "\u627E\u5230\u5206\u4EAB\u56FE\u6807",
2505
+ shareIconDescription: "\u662F\u4E00\u4E2A\u5E26\u6709\u5411\u4E0A\u7BAD\u5934\u7684\u65B9\u6846"
2506
+ },
2507
+ ja: {
2508
+ title: "\u3053\u306E\u30A2\u30D7\u30EA\u3092\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB",
2509
+ description: "\u30DB\u30FC\u30E0\u753B\u9762\u306B\u8FFD\u52A0\u3057\u3066\u6700\u9AD8\u306E\u4F53\u9A13\u3092\u3002",
2510
+ buttonText: "\u65B9\u6CD5\u3092\u898B\u308B",
2511
+ closeText: "\u4ECA\u306F\u3057\u306A\u3044",
2512
+ step1: "Safari\u306E\u4E0B\u90E8\u306B\u3042\u308B\u5171\u6709\u30DC\u30BF\u30F3\u3092\u30BF\u30C3\u30D7",
2513
+ step2: "\u4E0B\u306B\u30B9\u30AF\u30ED\u30FC\u30EB\u3057\u3066\u300C\u30DB\u30FC\u30E0\u753B\u9762\u306B\u8FFD\u52A0\u300D\u3092\u30BF\u30C3\u30D7",
2514
+ step3: "\u53F3\u4E0A\u306E\u300C\u8FFD\u52A0\u300D\u3092\u30BF\u30C3\u30D7",
2515
+ shareIconHint: "\u5171\u6709\u30A2\u30A4\u30B3\u30F3\u3092\u63A2\u3057\u3066\u304F\u3060\u3055\u3044",
2516
+ shareIconDescription: "\u4E0A\u5411\u304D\u77E2\u5370\u306E\u3042\u308B\u56DB\u89D2\u5F62\u3067\u3059"
2517
+ },
2518
+ ko: {
2519
+ title: "\uC774 \uC571 \uC124\uCE58",
2520
+ description: "\uCD5C\uACE0\uC758 \uACBD\u9A13\uC744 \uC704\uD574 \uD648 \uD654\uBA74\uC5D0 \uCD94\uAC00\uD558\uC138\uC694.",
2521
+ buttonText: "\uBC29\uBC95 \uBCF4\uAE30",
2522
+ closeText: "\uB098\uC911\uC5D0",
2523
+ step1: "Safari \uD558\uB2E8\uC758 \uACF5\uC720 \uBC84\uD2BC\uC744 \uD0ED\uD558\uC138\uC694",
2524
+ step2: '\uC544\uB798\uB85C \uC2A4\uD06C\uB864\uD558\uC5EC "\uD648 \uD654\uBA74\uC5D0 \uCD94\uAC00"\uB97C \uD0ED\uD558\uC138\uC694',
2525
+ step3: '\uC624\uB978\uCABD \uC0C1\uB2E8\uC758 "\uCD94\uAC00"\uB97C \uD0ED\uD558\uC138\uC694',
2526
+ shareIconHint: "\uACF5\uC720 \uC544\uC774\uCF58\uC744 \uCC3E\uC73C\uC138\uC694",
2527
+ shareIconDescription: "\uC704\uCABD \uD654\uC0B4\uD45C\uAC00 \uC788\uB294 \uC0AC\uAC01\uD615\uC785\uB2C8\uB2E4"
2528
+ },
2529
+ ar: {
2530
+ title: "\u062A\u062B\u0628\u064A\u062A \u0647\u0630\u0627 \u0627\u0644\u062A\u0637\u0628\u064A\u0642",
2531
+ description: "\u0623\u0636\u0641 \u0647\u0630\u0627 \u0627\u0644\u062A\u0637\u0628\u064A\u0642 \u0625\u0644\u0649 \u0627\u0644\u0634\u0627\u0634\u0629 \u0627\u0644\u0631\u0626\u064A\u0633\u064A\u0629 \u0644\u0644\u062D\u0635\u0648\u0644 \u0639\u0644\u0649 \u0623\u0641\u0636\u0644 \u062A\u062C\u0631\u0628\u0629.",
2532
+ buttonText: "\u0623\u0631\u0646\u064A \u0643\u064A\u0641",
2533
+ closeText: "\u0644\u064A\u0633 \u0627\u0644\u0622\u0646",
2534
+ step1: "\u0627\u0636\u063A\u0637 \u0639\u0644\u0649 \u0632\u0631 \u0627\u0644\u0645\u0634\u0627\u0631\u0643\u0629 \u0641\u064A \u0623\u0633\u0641\u0644 Safari",
2535
+ step2: '\u0645\u0631\u0631 \u0644\u0623\u0633\u0641\u0644 \u0648\u0627\u0636\u063A\u0637 \u0639\u0644\u0649 "\u0625\u0636\u0627\u0641\u0629 \u0625\u0644\u0649 \u0627\u0644\u0634\u0627\u0634\u0629 \u0627\u0644\u0631\u0626\u064A\u0633\u064A\u0629"',
2536
+ step3: '\u0627\u0636\u063A\u0637 \u0639\u0644\u0649 "\u0625\u0636\u0627\u0641\u0629" \u0641\u064A \u0627\u0644\u0632\u0627\u0648\u064A\u0629 \u0627\u0644\u0639\u0644\u0648\u064A\u0629 \u0627\u0644\u064A\u0645\u0646\u0649',
2537
+ shareIconHint: "\u0627\u0628\u062D\u062B \u0639\u0646 \u0623\u064A\u0642\u0648\u0646\u0629 \u0627\u0644\u0645\u0634\u0627\u0631\u0643\u0629",
2538
+ shareIconDescription: "\u0647\u0648 \u0645\u0631\u0628\u0639 \u0645\u0639 \u0633\u0647\u0645 \u064A\u0634\u064A\u0631 \u0644\u0623\u0639\u0644\u0649"
2539
+ },
2540
+ he: {
2541
+ title: "\u05D4\u05EA\u05E7\u05DF \u05D0\u05E4\u05DC\u05D9\u05E7\u05E6\u05D9\u05D4 \u05D6\u05D5",
2542
+ description: "\u05D4\u05D5\u05E1\u05E3 \u05D0\u05E4\u05DC\u05D9\u05E7\u05E6\u05D9\u05D4 \u05D6\u05D5 \u05DC\u05DE\u05E1\u05DA \u05D4\u05D1\u05D9\u05EA \u05DC\u05D7\u05D5\u05D5\u05D9\u05D4 \u05D4\u05D8\u05D5\u05D1\u05D4 \u05D1\u05D9\u05D5\u05EA\u05E8.",
2543
+ buttonText: "\u05D4\u05E8\u05D0\u05D4 \u05DC\u05D9 \u05D0\u05D9\u05DA",
2544
+ closeText: "\u05DC\u05D0 \u05E2\u05DB\u05E9\u05D9\u05D5",
2545
+ step1: "\u05D4\u05E7\u05E9 \u05E2\u05DC \u05DB\u05E4\u05EA\u05D5\u05E8 \u05D4\u05E9\u05D9\u05EA\u05D5\u05E3 \u05D1\u05EA\u05D7\u05EA\u05D9\u05EA Safari",
2546
+ step2: '\u05D2\u05DC\u05D5\u05DC \u05DC\u05DE\u05D8\u05D4 \u05D5\u05D4\u05E7\u05E9 \u05E2\u05DC "\u05D4\u05D5\u05E1\u05E3 \u05DC\u05DE\u05E1\u05DA \u05D4\u05D1\u05D9\u05EA"',
2547
+ step3: '\u05D4\u05E7\u05E9 \u05E2\u05DC "\u05D4\u05D5\u05E1\u05E3" \u05D1\u05E4\u05D9\u05E0\u05D4 \u05D4\u05D9\u05DE\u05E0\u05D9\u05EA \u05D4\u05E2\u05DC\u05D9\u05D5\u05E0\u05D4',
2548
+ shareIconHint: "\u05D7\u05E4\u05E9 \u05D0\u05EA \u05E1\u05DE\u05DC \u05D4\u05E9\u05D9\u05EA\u05D5\u05E3",
2549
+ shareIconDescription: "\u05D6\u05D4 \u05E8\u05D9\u05D1\u05D5\u05E2 \u05E2\u05DD \u05D7\u05E5 \u05DE\u05E6\u05D1\u05D9\u05E2 \u05DC\u05DE\u05E2\u05DC\u05D4"
2550
+ },
2551
+ hi: {
2552
+ title: "\u092F\u0939 \u0910\u092A \u0907\u0902\u0938\u094D\u091F\u0949\u0932 \u0915\u0930\u0947\u0902",
2553
+ description: "\u092C\u0947\u0939\u0924\u0930 \u0905\u0928\u0941\u092D\u0935 \u0915\u0947 \u0932\u093F\u090F \u0907\u0938 \u0910\u092A \u0915\u094B \u0939\u094B\u092E \u0938\u094D\u0915\u094D\u0930\u0940\u0928 \u092A\u0930 \u091C\u094B\u0921\u093C\u0947\u0902\u0964",
2554
+ buttonText: "\u092E\u0941\u091D\u0947 \u0926\u093F\u0916\u093E\u0913 \u0915\u0948\u0938\u0947",
2555
+ closeText: "\u0905\u092D\u0940 \u0928\u0939\u0940\u0902",
2556
+ step1: "Safari \u0915\u0947 \u0928\u0940\u091A\u0947 \u0936\u0947\u092F\u0930 \u092C\u091F\u0928 \u092A\u0930 \u091F\u0948\u092A \u0915\u0930\u0947\u0902",
2557
+ step2: '\u0928\u0940\u091A\u0947 \u0938\u094D\u0915\u094D\u0930\u0949\u0932 \u0915\u0930\u0947\u0902 \u0914\u0930 "\u0939\u094B\u092E \u0938\u094D\u0915\u094D\u0930\u0940\u0928 \u092E\u0947\u0902 \u091C\u094B\u0921\u093C\u0947\u0902" \u092A\u0930 \u091F\u0948\u092A \u0915\u0930\u0947\u0902',
2558
+ step3: '\u090A\u092A\u0930 \u0926\u093E\u090F\u0902 \u0915\u094B\u0928\u0947 \u092E\u0947\u0902 "\u091C\u094B\u0921\u093C\u0947\u0902" \u092A\u0930 \u091F\u0948\u092A \u0915\u0930\u0947\u0902',
2559
+ shareIconHint: "\u0936\u0947\u092F\u0930 \u0906\u0907\u0915\u0928 \u0916\u094B\u091C\u0947\u0902",
2560
+ shareIconDescription: "\u092F\u0939 \u090A\u092A\u0930 \u0915\u0940 \u0913\u0930 \u0924\u0940\u0930 \u0935\u093E\u0932\u093E \u0935\u0930\u094D\u0917 \u0939\u0948"
2561
+ },
2562
+ tr: {
2563
+ title: "Bu uygulamay\u0131 y\xFCkle",
2564
+ description: "En iyi deneyim i\xE7in bu uygulamay\u0131 ana ekran\u0131n\u0131za ekleyin.",
2565
+ buttonText: "Nas\u0131l yap\u0131l\u0131r g\xF6ster",
2566
+ closeText: "\u015Eimdi de\u011Fil",
2567
+ step1: "Safari'nin alt\u0131ndaki Payla\u015F d\xFC\u011Fmesine dokunun",
2568
+ step2: 'A\u015Fa\u011F\u0131 kayd\u0131r\u0131n ve "Ana Ekrana Ekle"ye dokunun',
2569
+ step3: 'Sa\u011F \xFCst k\xF6\u015Fedeki "Ekle"ye dokunun',
2570
+ shareIconHint: "Payla\u015F simgesini aray\u0131n",
2571
+ shareIconDescription: "Yukar\u0131 bakan oklu bir karedir"
2572
+ },
2573
+ pl: {
2574
+ title: "Zainstaluj t\u0119 aplikacj\u0119",
2575
+ description: "Dodaj t\u0119 aplikacj\u0119 do ekranu g\u0142\xF3wnego, aby uzyska\u0107 najlepsze wra\u017Cenia.",
2576
+ buttonText: "Poka\u017C mi jak",
2577
+ closeText: "Nie teraz",
2578
+ step1: "Dotknij przycisku Udost\u0119pnij na dole Safari",
2579
+ step2: 'Przewi\u0144 w d\xF3\u0142 i dotknij "Dodaj do ekranu pocz\u0105tkowego"',
2580
+ step3: 'Dotknij "Dodaj" w prawym g\xF3rnym rogu',
2581
+ shareIconHint: "Poszukaj ikony Udost\u0119pnij",
2582
+ shareIconDescription: "To kwadrat ze strza\u0142k\u0105 skierowan\u0105 w g\xF3r\u0119"
2583
+ },
2584
+ sv: {
2585
+ title: "Installera denna app",
2586
+ description: "L\xE4gg till denna app p\xE5 hemsk\xE4rmen f\xF6r b\xE4sta upplevelse.",
2587
+ buttonText: "Visa mig hur",
2588
+ closeText: "Inte nu",
2589
+ step1: "Tryck p\xE5 Dela-knappen l\xE4ngst ner i Safari",
2590
+ step2: 'Scrolla ner och tryck p\xE5 "L\xE4gg till p\xE5 hemsk\xE4rmen"',
2591
+ step3: 'Tryck p\xE5 "L\xE4gg till" i \xF6vre h\xF6gra h\xF6rnet',
2592
+ shareIconHint: "Leta efter Dela-ikonen",
2593
+ shareIconDescription: "Det \xE4r kvadraten med en pil som pekar upp\xE5t"
2594
+ },
2595
+ da: {
2596
+ title: "Installer denne app",
2597
+ description: "F\xF8j denne app til din startsk\xE6rm for den bedste oplevelse.",
2598
+ buttonText: "Vis mig hvordan",
2599
+ closeText: "Ikke nu",
2600
+ step1: "Tryk p\xE5 Del-knappen nederst i Safari",
2601
+ step2: 'Rul ned og tryk p\xE5 "F\xF8j til hjemmesk\xE6rm"',
2602
+ step3: 'Tryk p\xE5 "Tilf\xF8j" i \xF8verste h\xF8jre hj\xF8rne',
2603
+ shareIconHint: "Kig efter Del-ikonet",
2604
+ shareIconDescription: "Det er firkanten med en pil, der peger opad"
2605
+ },
2606
+ no: {
2607
+ title: "Installer denne appen",
2608
+ description: "Legg til denne appen p\xE5 startskjermen for best opplevelse.",
2609
+ buttonText: "Vis meg hvordan",
2610
+ closeText: "Ikke n\xE5",
2611
+ step1: "Trykk p\xE5 Del-knappen nederst i Safari",
2612
+ step2: 'Rull ned og trykk p\xE5 "Legg til p\xE5 Hjem-skjerm"',
2613
+ step3: 'Trykk p\xE5 "Legg til" \xF8verst til h\xF8yre',
2614
+ shareIconHint: "Se etter Del-ikonet",
2615
+ shareIconDescription: "Det er firkanten med en pil som peker oppover"
2616
+ },
2617
+ fi: {
2618
+ title: "Asenna t\xE4m\xE4 sovellus",
2619
+ description: "Lis\xE4\xE4 t\xE4m\xE4 sovellus aloitusn\xE4yt\xF6lle parhaan kokemuksen saamiseksi.",
2620
+ buttonText: "N\xE4yt\xE4 miten",
2621
+ closeText: "Ei nyt",
2622
+ step1: "Napauta Jaa-painiketta Safarin alareunassa",
2623
+ step2: 'Vierit\xE4 alas ja napauta "Lis\xE4\xE4 Koti-valikkoon"',
2624
+ step3: 'Napauta "Lis\xE4\xE4" oikeassa yl\xE4kulmassa',
2625
+ shareIconHint: "Etsi Jaa-kuvake",
2626
+ shareIconDescription: "Se on neli\xF6, jossa on yl\xF6sp\xE4in osoittava nuoli"
2627
+ }
2628
+ };
2629
+ function detectBrowserLanguage() {
2630
+ const browserLang = navigator.language?.toLowerCase() || "en";
2631
+ const shortLang = browserLang.split("-")[0];
2632
+ if (shortLang in IOS_BANNER_TRANSLATIONS) {
2633
+ return shortLang;
2634
+ }
2635
+ if (browserLang.startsWith("zh")) return "zh";
2636
+ if (browserLang.startsWith("pt")) return "pt";
2637
+ if (browserLang.startsWith("no") || browserLang.startsWith("nb") || browserLang.startsWith("nn")) return "no";
2638
+ return "en";
2639
+ }
2640
+
2641
+ // src/IOSInstallBanner.ts
2642
+ var STORAGE_KEY = "cloudsignal_pwa_ios_banner_dismissed";
2643
+ var BANNER_ID = "cloudsignal-ios-install-banner";
2644
+ var DEFAULT_STYLES = {
2645
+ backgroundColor: "#ffffff",
2646
+ textColor: "#1a1a1a",
2647
+ buttonBackgroundColor: "#007AFF",
2648
+ buttonTextColor: "#ffffff",
2649
+ borderRadius: "16px",
2650
+ boxShadow: "0 4px 24px rgba(0, 0, 0, 0.15)"
2651
+ };
2652
+ var IOSInstallBanner = class {
2653
+ constructor(options = {}) {
2654
+ this.bannerElement = null;
2655
+ this.overlayElement = null;
2656
+ this.debug = options.debug ?? false;
2657
+ this.onShow = options.onShow;
2658
+ this.onDismiss = options.onDismiss;
2659
+ this.onInstallClick = options.onInstallClick;
2660
+ this.language = options.language ?? detectBrowserLanguage();
2661
+ const baseStrings = IOS_BANNER_TRANSLATIONS[this.language];
2662
+ this.strings = {
2663
+ ...baseStrings,
2664
+ ...options.customStrings
2665
+ };
2666
+ this.config = {
2667
+ enabled: options.config?.enabled ?? true,
2668
+ title: options.config?.title ?? this.strings.title,
2669
+ description: options.config?.description ?? this.strings.description,
2670
+ buttonText: options.config?.buttonText ?? this.strings.buttonText,
2671
+ closeText: options.config?.closeText ?? this.strings.closeText,
2672
+ iconUrl: options.config?.iconUrl,
2673
+ customStyles: options.config?.customStyles ?? {},
2674
+ showDelay: options.config?.showDelay ?? 3e3,
2675
+ dismissalDays: options.config?.dismissalDays ?? 7,
2676
+ position: options.config?.position ?? "bottom"
2677
+ };
2678
+ this.styles = { ...DEFAULT_STYLES, ...this.config.customStyles };
2679
+ this.log(`Initialized with language: ${this.language}`);
2680
+ }
2681
+ /**
2682
+ * Get current language
2683
+ */
2684
+ getLanguage() {
2685
+ return this.language;
2686
+ }
2687
+ /**
2688
+ * Get current strings
2689
+ */
2690
+ getStrings() {
2691
+ return this.strings;
2692
+ }
2693
+ /**
2694
+ * Set language and update strings
2695
+ */
2696
+ setLanguage(language) {
2697
+ this.language = language;
2698
+ const baseStrings = IOS_BANNER_TRANSLATIONS[language];
2699
+ this.strings = { ...baseStrings };
2700
+ this.config.title = this.strings.title;
2701
+ this.config.description = this.strings.description;
2702
+ this.config.buttonText = this.strings.buttonText;
2703
+ this.config.closeText = this.strings.closeText;
2704
+ this.log(`Language changed to: ${language}`);
2705
+ }
2706
+ /**
2707
+ * Get current banner state
2708
+ */
2709
+ getState() {
2710
+ return {
2711
+ isVisible: this.bannerElement !== null,
2712
+ wasDismissed: this.wasDismissed(),
2713
+ isEligible: this.isEligible(),
2714
+ isInstalled: this.isInstalled()
2715
+ };
2716
+ }
2717
+ /**
2718
+ * Check if device is iOS Safari (eligible for banner)
2719
+ */
2720
+ isEligible() {
2721
+ const userAgent = navigator.userAgent;
2722
+ const isIOS = /iPad|iPhone|iPod/.test(userAgent) && !window.MSStream;
2723
+ if (!isIOS) return false;
2724
+ const isSafari = /Safari/i.test(userAgent) && !/Chrome|CriOS|FxiOS|EdgiOS/i.test(userAgent);
2725
+ if (!isSafari) return false;
2726
+ if (this.isInstalled()) return false;
2727
+ return true;
2728
+ }
2729
+ /**
2730
+ * Check if PWA is already installed (running in standalone mode)
2731
+ */
2732
+ isInstalled() {
2733
+ if (navigator.standalone === true) return true;
2734
+ if (window.matchMedia("(display-mode: standalone)").matches) return true;
2735
+ return false;
2736
+ }
2737
+ /**
2738
+ * Check if banner was previously dismissed
2739
+ */
2740
+ wasDismissed() {
2741
+ try {
2742
+ const dismissed = localStorage.getItem(STORAGE_KEY);
2743
+ if (!dismissed) return false;
2744
+ const dismissedTime = parseInt(dismissed, 10);
2745
+ const daysSinceDismissal = (Date.now() - dismissedTime) / (1e3 * 60 * 60 * 24);
2746
+ if (daysSinceDismissal > this.config.dismissalDays) {
2747
+ localStorage.removeItem(STORAGE_KEY);
2748
+ return false;
2749
+ }
2750
+ return true;
2751
+ } catch {
2752
+ return false;
2753
+ }
2754
+ }
2755
+ /**
2756
+ * Show the install banner if eligible
2757
+ */
2758
+ async show() {
2759
+ if (!this.config.enabled) {
2760
+ this.log("Banner is disabled");
2761
+ return false;
2762
+ }
2763
+ if (!this.isEligible()) {
2764
+ this.log("Device not eligible for iOS install banner");
2765
+ return false;
2766
+ }
2767
+ if (this.wasDismissed()) {
2768
+ this.log("Banner was dismissed recently");
2769
+ return false;
2770
+ }
2771
+ if (this.bannerElement) {
2772
+ this.log("Banner already visible");
2773
+ return true;
2774
+ }
2775
+ if (this.config.showDelay > 0) {
2776
+ await new Promise((resolve) => setTimeout(resolve, this.config.showDelay));
2777
+ }
2778
+ if (!this.isEligible() || this.wasDismissed()) {
2779
+ return false;
2780
+ }
2781
+ this.createBanner();
2782
+ this.log("Banner shown");
2783
+ this.onShow?.();
2784
+ return true;
1563
2785
  }
1564
2786
  /**
1565
- * Stop sending heartbeats
2787
+ * Hide the install banner
1566
2788
  */
1567
- stop() {
1568
- if (!this.isRunning) {
1569
- return;
1570
- }
1571
- this.isRunning = false;
1572
- if (this.intervalId) {
1573
- clearInterval(this.intervalId);
1574
- this.intervalId = null;
2789
+ hide() {
2790
+ if (this.bannerElement) {
2791
+ this.bannerElement.remove();
2792
+ this.bannerElement = null;
1575
2793
  }
1576
- if (this.visibilityHandler) {
1577
- document.removeEventListener("visibilitychange", this.visibilityHandler);
1578
- this.visibilityHandler = null;
2794
+ if (this.overlayElement) {
2795
+ this.overlayElement.remove();
2796
+ this.overlayElement = null;
1579
2797
  }
1580
- this.log("Heartbeat stopped");
1581
2798
  }
1582
2799
  /**
1583
- * Check if heartbeat is running
2800
+ * Dismiss the banner and remember the dismissal
1584
2801
  */
1585
- isHeartbeatRunning() {
1586
- return this.isRunning;
2802
+ dismiss() {
2803
+ this.hide();
2804
+ try {
2805
+ localStorage.setItem(STORAGE_KEY, Date.now().toString());
2806
+ } catch {
2807
+ }
2808
+ this.log("Banner dismissed");
2809
+ this.onDismiss?.();
1587
2810
  }
1588
2811
  /**
1589
- * Send a single heartbeat
2812
+ * Reset dismissal state (for testing or after significant time)
1590
2813
  */
1591
- async sendHeartbeat() {
1592
- if (!this.registrationId || !isValidUUID(this.registrationId)) {
1593
- this.log("Cannot send heartbeat: no valid registration ID", "warn");
1594
- return false;
1595
- }
2814
+ resetDismissal() {
1596
2815
  try {
1597
- const url = `${this.serviceUrl}/api/v1/registration/heartbeat/${this.registrationId}`;
1598
- const response = await makeAuthenticatedRequest(
1599
- this.organizationId,
1600
- this.organizationSecret,
1601
- "POST",
1602
- url
1603
- );
1604
- if (!response.ok) {
1605
- throw new Error(`Heartbeat failed: ${response.status}`);
1606
- }
1607
- this.log("Heartbeat sent successfully");
1608
- this.onHeartbeatSent?.();
1609
- return true;
1610
- } catch (error) {
1611
- const err = error instanceof Error ? error : new Error(String(error));
1612
- this.log(`Heartbeat failed: ${err.message}`, "error");
1613
- this.onHeartbeatError?.(err);
1614
- return false;
2816
+ localStorage.removeItem(STORAGE_KEY);
2817
+ } catch {
1615
2818
  }
1616
2819
  }
1617
2820
  /**
1618
- * Update heartbeat configuration
2821
+ * Get iOS install steps (localized)
1619
2822
  */
1620
- updateConfig(config) {
1621
- const wasRunning = this.isRunning;
1622
- if (wasRunning) {
1623
- this.stop();
1624
- }
1625
- this.config = {
1626
- ...this.config,
1627
- ...config
1628
- };
1629
- if (wasRunning && this.config.enabled) {
1630
- this.start();
1631
- }
2823
+ getInstallSteps() {
2824
+ return [
2825
+ this.strings.step1,
2826
+ this.strings.step2,
2827
+ this.strings.step3
2828
+ ];
1632
2829
  }
2830
+ // ============================================
2831
+ // Private methods
2832
+ // ============================================
1633
2833
  /**
1634
- * Set up visibility change handler
1635
- */
1636
- setupVisibilityHandler() {
1637
- this.visibilityHandler = () => {
1638
- if (document.hidden) {
1639
- if (this.intervalId) {
1640
- clearInterval(this.intervalId);
1641
- this.intervalId = null;
1642
- }
1643
- this.log("Heartbeat paused (page hidden)");
1644
- } else {
1645
- if (this.isRunning && !this.intervalId) {
1646
- this.sendHeartbeat();
1647
- this.intervalId = setInterval(() => {
1648
- this.sendHeartbeat();
1649
- }, this.config.interval);
1650
- this.log("Heartbeat resumed (page visible)");
1651
- }
2834
+ * Create and inject the banner element
2835
+ */
2836
+ createBanner() {
2837
+ this.overlayElement = document.createElement("div");
2838
+ this.overlayElement.id = `${BANNER_ID}-overlay`;
2839
+ this.overlayElement.style.cssText = `
2840
+ position: fixed;
2841
+ top: 0;
2842
+ left: 0;
2843
+ right: 0;
2844
+ bottom: 0;
2845
+ background: rgba(0, 0, 0, 0.5);
2846
+ z-index: 999998;
2847
+ opacity: 0;
2848
+ transition: opacity 0.3s ease;
2849
+ `;
2850
+ this.overlayElement.addEventListener("click", () => this.dismiss());
2851
+ document.body.appendChild(this.overlayElement);
2852
+ this.bannerElement = document.createElement("div");
2853
+ this.bannerElement.id = BANNER_ID;
2854
+ const positionStyles = this.config.position === "top" ? "top: 20px;" : "bottom: 20px;";
2855
+ this.bannerElement.style.cssText = `
2856
+ position: fixed;
2857
+ ${positionStyles}
2858
+ left: 20px;
2859
+ right: 20px;
2860
+ max-width: 400px;
2861
+ margin: 0 auto;
2862
+ background: ${this.styles.backgroundColor};
2863
+ color: ${this.styles.textColor};
2864
+ border-radius: ${this.styles.borderRadius};
2865
+ box-shadow: ${this.styles.boxShadow};
2866
+ z-index: 999999;
2867
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
2868
+ opacity: 0;
2869
+ transform: translateY(${this.config.position === "top" ? "-20px" : "20px"});
2870
+ transition: opacity 0.3s ease, transform 0.3s ease;
2871
+ `;
2872
+ const steps = this.getInstallSteps();
2873
+ const iconHtml = this.config.iconUrl ? `<img src="${this.config.iconUrl}" alt="App icon" style="width: 60px; height: 60px; border-radius: 12px; margin-right: 16px;">` : "";
2874
+ this.bannerElement.innerHTML = `
2875
+ <div style="padding: 20px;">
2876
+ <div style="display: flex; align-items: flex-start; margin-bottom: 16px;">
2877
+ ${iconHtml}
2878
+ <div style="flex: 1;">
2879
+ <h3 style="margin: 0 0 4px 0; font-size: 18px; font-weight: 600;">${this.config.title}</h3>
2880
+ <p style="margin: 0; font-size: 14px; opacity: 0.7;">${this.config.description}</p>
2881
+ </div>
2882
+ </div>
2883
+
2884
+ <div id="${BANNER_ID}-steps" style="display: none; margin-bottom: 16px;">
2885
+ <ol style="margin: 0; padding-left: 24px; font-size: 14px; line-height: 1.6;">
2886
+ ${steps.map((step) => `<li style="margin-bottom: 8px;">${step}</li>`).join("")}
2887
+ </ol>
2888
+ <div style="margin-top: 16px; padding: 12px; background: rgba(0,122,255,0.1); border-radius: 8px; font-size: 13px;">
2889
+ <strong style="display: flex; align-items: center; gap: 8px;">
2890
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2891
+ <path d="M12 5v14M5 12l7-7 7 7"/>
2892
+ </svg>
2893
+ ${this.strings.shareIconHint}
2894
+ </strong>
2895
+ <span style="display: block; margin-top: 4px; opacity: 0.7;">${this.strings.shareIconDescription}</span>
2896
+ </div>
2897
+ </div>
2898
+
2899
+ <div style="display: flex; gap: 12px;">
2900
+ <button id="${BANNER_ID}-show" style="
2901
+ flex: 1;
2902
+ padding: 12px 16px;
2903
+ background: ${this.styles.buttonBackgroundColor};
2904
+ color: ${this.styles.buttonTextColor};
2905
+ border: none;
2906
+ border-radius: 10px;
2907
+ font-size: 16px;
2908
+ font-weight: 600;
2909
+ cursor: pointer;
2910
+ -webkit-tap-highlight-color: transparent;
2911
+ ">${this.config.buttonText}</button>
2912
+ <button id="${BANNER_ID}-close" style="
2913
+ padding: 12px 16px;
2914
+ background: transparent;
2915
+ color: ${this.styles.textColor};
2916
+ border: 1px solid rgba(0,0,0,0.1);
2917
+ border-radius: 10px;
2918
+ font-size: 16px;
2919
+ cursor: pointer;
2920
+ -webkit-tap-highlight-color: transparent;
2921
+ ">${this.config.closeText}</button>
2922
+ </div>
2923
+ </div>
2924
+ `;
2925
+ document.body.appendChild(this.bannerElement);
2926
+ const showButton = document.getElementById(`${BANNER_ID}-show`);
2927
+ const closeButton = document.getElementById(`${BANNER_ID}-close`);
2928
+ const stepsElement = document.getElementById(`${BANNER_ID}-steps`);
2929
+ showButton?.addEventListener("click", () => {
2930
+ if (stepsElement) {
2931
+ stepsElement.style.display = stepsElement.style.display === "none" ? "block" : "none";
2932
+ showButton.textContent = stepsElement.style.display === "none" ? this.config.buttonText : "Got it!";
1652
2933
  }
1653
- };
1654
- document.addEventListener("visibilitychange", this.visibilityHandler);
2934
+ this.onInstallClick?.();
2935
+ });
2936
+ closeButton?.addEventListener("click", () => this.dismiss());
2937
+ requestAnimationFrame(() => {
2938
+ if (this.overlayElement) {
2939
+ this.overlayElement.style.opacity = "1";
2940
+ }
2941
+ if (this.bannerElement) {
2942
+ this.bannerElement.style.opacity = "1";
2943
+ this.bannerElement.style.transform = "translateY(0)";
2944
+ }
2945
+ });
1655
2946
  }
1656
2947
  /**
1657
2948
  * Log message if debug is enabled
1658
2949
  */
1659
2950
  log(message, level = "log") {
1660
- if (!this.debug) return;
1661
- const prefix = "[CloudSignal PWA Heartbeat]";
2951
+ if (!this.debug && level === "log") return;
2952
+ const prefix = "[CloudSignal PWA IOSBanner]";
1662
2953
  console[level](`${prefix} ${message}`);
1663
2954
  }
1664
2955
  };
1665
2956
 
1666
2957
  // src/CloudSignalPWA.ts
1667
2958
  var DEFAULT_SERVICE_URL = "https://pwa.cloudsignal.app";
1668
- var SDK_VERSION = "1.0.0";
2959
+ var SDK_VERSION = "1.1.0";
1669
2960
  var CloudSignalPWA = class {
1670
2961
  constructor(config) {
1671
2962
  this.initialized = false;
1672
2963
  this.serviceConfig = null;
2964
+ // v1.1.0 Managers
2965
+ this.wakeLockManager = null;
2966
+ this.offlineQueueManager = null;
2967
+ this.iosInstallBanner = null;
1673
2968
  // Event emitter
1674
2969
  this.eventHandlers = /* @__PURE__ */ new Map();
1675
2970
  this.config = config;
@@ -1714,8 +3009,56 @@ var CloudSignalPWA = class {
1714
3009
  config: config.heartbeat,
1715
3010
  debug: this.debug,
1716
3011
  onHeartbeatSent: () => this.emit("heartbeat:sent", { timestamp: Date.now() }),
1717
- onHeartbeatError: (err) => this.emit("heartbeat:error", { timestamp: Date.now(), error: err.message })
3012
+ onHeartbeatError: (err) => this.emit("heartbeat:error", { timestamp: Date.now(), error: err.message }),
3013
+ onIntervalChanged: (interval, reason) => this.emit("heartbeat:intervalChanged", { interval, reason }),
3014
+ onPausedForBattery: () => this.emit("heartbeat:pausedForBattery", { timestamp: Date.now() }),
3015
+ onResumedFromBattery: () => this.emit("heartbeat:resumedFromBattery", { timestamp: Date.now() })
1718
3016
  });
3017
+ if (config.wakeLock?.enabled !== false) {
3018
+ this.wakeLockManager = new WakeLockManager({
3019
+ config: {
3020
+ enabled: config.wakeLock?.enabled ?? true,
3021
+ autoReacquire: config.wakeLock?.reacquireOnVisibility ?? true
3022
+ },
3023
+ debug: this.debug,
3024
+ onAcquired: () => this.emit("wakeLock:acquired", { timestamp: Date.now() }),
3025
+ onReleased: (event) => this.emit("wakeLock:released", { reason: event.reason, timestamp: Date.now() }),
3026
+ onError: (event) => this.emit("wakeLock:error", { error: event.error })
3027
+ });
3028
+ }
3029
+ if (config.offlineQueue?.enabled !== false) {
3030
+ this.offlineQueueManager = new OfflineQueueManager({
3031
+ config: {
3032
+ enabled: config.offlineQueue?.enabled ?? true,
3033
+ maxQueueSize: config.offlineQueue?.maxQueueSize ?? 100,
3034
+ maxAge: (config.offlineQueue?.maxAgeTTLHours ?? 24) * 60 * 60 * 1e3,
3035
+ processOnOnline: config.offlineQueue?.autoProcessOnOnline ?? true
3036
+ },
3037
+ debug: this.debug,
3038
+ onRequestQueued: (req) => this.emit("offlineQueue:queued", { url: req.url, method: req.method }),
3039
+ onRequestProcessed: (result) => this.emit("offlineQueue:processed", { success: result.success, requestId: result.id }),
3040
+ onQueueEmpty: () => this.emit("offlineQueue:flushed", {})
3041
+ });
3042
+ }
3043
+ if (config.iosInstallBanner?.enabled !== false) {
3044
+ const deviceInfo = this.deviceDetector.getDeviceInfo();
3045
+ if (deviceInfo.isIOS && deviceInfo.browser === "Safari") {
3046
+ this.iosInstallBanner = new IOSInstallBanner({
3047
+ config: {
3048
+ enabled: config.iosInstallBanner?.enabled ?? true,
3049
+ title: config.iosInstallBanner?.title,
3050
+ description: config.iosInstallBanner?.subtitle,
3051
+ iconUrl: config.iosInstallBanner?.iconUrl,
3052
+ showDelay: config.iosInstallBanner?.showDelay,
3053
+ dismissalDays: config.iosInstallBanner?.dismissRememberDays
3054
+ },
3055
+ debug: this.debug,
3056
+ onShow: () => this.emit("iosBanner:shown", {}),
3057
+ onDismiss: () => this.emit("iosBanner:dismissed", {}),
3058
+ onInstallClick: () => this.emit("iosBanner:installClicked", {})
3059
+ });
3060
+ }
3061
+ }
1719
3062
  this.log(`CloudSignal PWA SDK v${SDK_VERSION} initialized`);
1720
3063
  }
1721
3064
  // ============================================
@@ -1735,6 +3078,18 @@ var CloudSignalPWA = class {
1735
3078
  }
1736
3079
  const serviceConfig = await this.downloadConfig();
1737
3080
  this.setupNetworkListeners();
3081
+ if (this.offlineQueueManager) {
3082
+ await this.offlineQueueManager.initialize();
3083
+ }
3084
+ if (this.config.notificationAnalytics?.enabled !== false && swReg) {
3085
+ this.configureNotificationAnalytics(swReg);
3086
+ }
3087
+ if (this.iosInstallBanner && this.config.iosInstallBanner?.showOnFirstVisit !== false) {
3088
+ const installState = this.installationManager.getState();
3089
+ if (!installState.isInstalled) {
3090
+ this.iosInstallBanner.show();
3091
+ }
3092
+ }
1738
3093
  this.initialized = true;
1739
3094
  const result = {
1740
3095
  success: true,
@@ -1903,6 +3258,123 @@ var CloudSignalPWA = class {
1903
3258
  this.heartbeatManager.stop();
1904
3259
  this.emit("heartbeat:stopped", { timestamp: Date.now() });
1905
3260
  }
3261
+ /**
3262
+ * Get current heartbeat interval (may vary with network conditions)
3263
+ */
3264
+ getHeartbeatInterval() {
3265
+ return this.heartbeatManager.getCurrentInterval();
3266
+ }
3267
+ /**
3268
+ * Get current network connection info
3269
+ */
3270
+ getNetworkInfo() {
3271
+ return this.heartbeatManager.getNetworkInfo();
3272
+ }
3273
+ /**
3274
+ * Get battery info if available
3275
+ */
3276
+ async getBatteryInfo() {
3277
+ return this.heartbeatManager.getBatteryInfo();
3278
+ }
3279
+ // ============================================
3280
+ // Wake Lock (v1.1.0)
3281
+ // ============================================
3282
+ /**
3283
+ * Request screen wake lock to prevent sleep
3284
+ */
3285
+ async requestWakeLock() {
3286
+ if (!this.wakeLockManager) {
3287
+ this.log("Wake lock manager not initialized", "warn");
3288
+ return false;
3289
+ }
3290
+ return this.wakeLockManager.acquire();
3291
+ }
3292
+ /**
3293
+ * Release screen wake lock
3294
+ */
3295
+ releaseWakeLock() {
3296
+ this.wakeLockManager?.release();
3297
+ }
3298
+ /**
3299
+ * Get wake lock state
3300
+ */
3301
+ getWakeLockState() {
3302
+ if (!this.wakeLockManager) {
3303
+ return { isSupported: false, isActive: false };
3304
+ }
3305
+ return this.wakeLockManager.getState();
3306
+ }
3307
+ // ============================================
3308
+ // Offline Queue (v1.1.0)
3309
+ // ============================================
3310
+ /**
3311
+ * Queue a request for later execution when offline
3312
+ */
3313
+ async queueOfflineRequest(url, method, options) {
3314
+ if (!this.offlineQueueManager) {
3315
+ this.log("Offline queue manager not initialized", "warn");
3316
+ return null;
3317
+ }
3318
+ return this.offlineQueueManager.queueRequest(url, method, {
3319
+ body: options?.body,
3320
+ headers: options?.headers
3321
+ });
3322
+ }
3323
+ /**
3324
+ * Process all queued requests
3325
+ */
3326
+ async processOfflineQueue() {
3327
+ if (!this.offlineQueueManager) {
3328
+ return [];
3329
+ }
3330
+ return this.offlineQueueManager.processQueue();
3331
+ }
3332
+ /**
3333
+ * Get offline queue statistics
3334
+ */
3335
+ async getOfflineQueueStats() {
3336
+ if (!this.offlineQueueManager) {
3337
+ return { totalQueued: 0, byType: { registration: 0, heartbeat: 0, analytics: 0, preferences: 0, unregister: 0, custom: 0 } };
3338
+ }
3339
+ return this.offlineQueueManager.getStats();
3340
+ }
3341
+ /**
3342
+ * Clear all queued requests
3343
+ */
3344
+ async clearOfflineQueue() {
3345
+ await this.offlineQueueManager?.clearQueue();
3346
+ }
3347
+ // ============================================
3348
+ // iOS Install Banner (v1.1.0)
3349
+ // ============================================
3350
+ /**
3351
+ * Show iOS install banner manually
3352
+ */
3353
+ showIOSInstallBanner() {
3354
+ if (!this.iosInstallBanner) {
3355
+ this.log("iOS install banner not available (not on iOS Safari)", "warn");
3356
+ return;
3357
+ }
3358
+ this.iosInstallBanner.show();
3359
+ }
3360
+ /**
3361
+ * Hide iOS install banner
3362
+ */
3363
+ hideIOSInstallBanner() {
3364
+ this.iosInstallBanner?.hide();
3365
+ }
3366
+ /**
3367
+ * Check if iOS install banner was previously dismissed
3368
+ */
3369
+ wasIOSBannerDismissed() {
3370
+ return this.iosInstallBanner?.wasDismissed() ?? false;
3371
+ }
3372
+ /**
3373
+ * Reset iOS banner dismissal state
3374
+ */
3375
+ resetIOSBannerDismissal() {
3376
+ this.iosInstallBanner?.resetDismissal();
3377
+ }
1906
3378
  // ============================================
1907
3379
  // Service Worker
1908
3380
  // ============================================
@@ -2001,12 +3473,32 @@ var CloudSignalPWA = class {
2001
3473
  window.addEventListener("online", () => {
2002
3474
  this.deviceDetector.clearCache();
2003
3475
  this.emit("network:online", { isOnline: true });
3476
+ if (this.offlineQueueManager) {
3477
+ this.offlineQueueManager.processQueue().catch((err) => {
3478
+ this.log(`Failed to process offline queue: ${err.message}`, "error");
3479
+ });
3480
+ }
2004
3481
  });
2005
3482
  window.addEventListener("offline", () => {
2006
3483
  this.deviceDetector.clearCache();
2007
3484
  this.emit("network:offline", { isOnline: false });
2008
3485
  });
2009
3486
  }
3487
+ /**
3488
+ * Configure notification analytics in service worker
3489
+ */
3490
+ configureNotificationAnalytics(swReg) {
3491
+ const endpoint = this.config.notificationAnalytics?.endpoint || `${this.serviceUrl}/api/v1/analytics/notifications`;
3492
+ swReg.active?.postMessage({
3493
+ type: "CONFIGURE_ANALYTICS",
3494
+ config: {
3495
+ enabled: this.config.notificationAnalytics?.enabled !== false,
3496
+ endpoint,
3497
+ organizationId: this.config.organizationId
3498
+ }
3499
+ });
3500
+ this.log("Notification analytics configured");
3501
+ }
2010
3502
  /**
2011
3503
  * Log message if debug is enabled
2012
3504
  */
@@ -2018,9 +3510,438 @@ var CloudSignalPWA = class {
2018
3510
  };
2019
3511
  var CloudSignalPWA_default = CloudSignalPWA;
2020
3512
 
3513
+ // src/NotificationPermissionPrompt.ts
3514
+ var STORAGE_KEY2 = "cloudsignal_pwa_notification_prompt_dismissed";
3515
+ var PROMPT_ID = "cloudsignal-notification-prompt";
3516
+ var NOTIFICATION_PROMPT_TRANSLATIONS = {
3517
+ en: {
3518
+ title: "Stay Updated",
3519
+ description: "Get notified about important updates, messages, and alerts.",
3520
+ allowButton: "Enable Notifications",
3521
+ laterButton: "Maybe Later",
3522
+ iosNote: "On iOS, you must first add this app to your home screen."
3523
+ },
3524
+ es: {
3525
+ title: "Mantente Actualizado",
3526
+ description: "Recibe notificaciones sobre actualizaciones importantes, mensajes y alertas.",
3527
+ allowButton: "Activar Notificaciones",
3528
+ laterButton: "Quiz\xE1s Despu\xE9s",
3529
+ iosNote: "En iOS, primero debes a\xF1adir esta aplicaci\xF3n a tu pantalla de inicio."
3530
+ },
3531
+ fr: {
3532
+ title: "Restez Inform\xE9",
3533
+ description: "Recevez des notifications sur les mises \xE0 jour importantes, messages et alertes.",
3534
+ allowButton: "Activer les Notifications",
3535
+ laterButton: "Plus Tard",
3536
+ iosNote: "Sur iOS, vous devez d'abord ajouter cette application \xE0 votre \xE9cran d'accueil."
3537
+ },
3538
+ de: {
3539
+ title: "Bleiben Sie Informiert",
3540
+ description: "Erhalten Sie Benachrichtigungen \xFCber wichtige Updates, Nachrichten und Warnungen.",
3541
+ allowButton: "Benachrichtigungen Aktivieren",
3542
+ laterButton: "Vielleicht Sp\xE4ter",
3543
+ iosNote: "Auf iOS m\xFCssen Sie diese App zuerst zum Startbildschirm hinzuf\xFCgen."
3544
+ },
3545
+ it: {
3546
+ title: "Rimani Aggiornato",
3547
+ description: "Ricevi notifiche su aggiornamenti importanti, messaggi e avvisi.",
3548
+ allowButton: "Attiva Notifiche",
3549
+ laterButton: "Forse Dopo",
3550
+ iosNote: "Su iOS, devi prima aggiungere questa app alla schermata Home."
3551
+ },
3552
+ pt: {
3553
+ title: "Fique Atualizado",
3554
+ description: "Receba notifica\xE7\xF5es sobre atualiza\xE7\xF5es importantes, mensagens e alertas.",
3555
+ allowButton: "Ativar Notifica\xE7\xF5es",
3556
+ laterButton: "Talvez Depois",
3557
+ iosNote: "No iOS, voc\xEA deve primeiro adicionar este aplicativo \xE0 tela inicial."
3558
+ },
3559
+ nl: {
3560
+ title: "Blijf Op De Hoogte",
3561
+ description: "Ontvang meldingen over belangrijke updates, berichten en waarschuwingen.",
3562
+ allowButton: "Meldingen Inschakelen",
3563
+ laterButton: "Misschien Later",
3564
+ iosNote: "Op iOS moet je deze app eerst toevoegen aan je beginscherm."
3565
+ },
3566
+ ru: {
3567
+ title: "\u0411\u0443\u0434\u044C\u0442\u0435 \u0432 \u041A\u0443\u0440\u0441\u0435",
3568
+ description: "\u041F\u043E\u043B\u0443\u0447\u0430\u0439\u0442\u0435 \u0443\u0432\u0435\u0434\u043E\u043C\u043B\u0435\u043D\u0438\u044F \u043E \u0432\u0430\u0436\u043D\u044B\u0445 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u044F\u0445, \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F\u0445 \u0438 \u043F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u044F\u0445.",
3569
+ allowButton: "\u0412\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0423\u0432\u0435\u0434\u043E\u043C\u043B\u0435\u043D\u0438\u044F",
3570
+ laterButton: "\u041F\u043E\u0437\u0436\u0435",
3571
+ iosNote: "\u041D\u0430 iOS \u0441\u043D\u0430\u0447\u0430\u043B\u0430 \u0434\u043E\u0431\u0430\u0432\u044C\u0442\u0435 \u044D\u0442\u043E \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u043D\u0430 \u0433\u043B\u0430\u0432\u043D\u044B\u0439 \u044D\u043A\u0440\u0430\u043D."
3572
+ },
3573
+ zh: {
3574
+ title: "\u4FDD\u6301\u66F4\u65B0",
3575
+ description: "\u83B7\u53D6\u91CD\u8981\u66F4\u65B0\u3001\u6D88\u606F\u548C\u63D0\u9192\u7684\u901A\u77E5\u3002",
3576
+ allowButton: "\u542F\u7528\u901A\u77E5",
3577
+ laterButton: "\u7A0D\u540E\u518D\u8BF4",
3578
+ iosNote: "\u5728iOS\u4E0A\uFF0C\u60A8\u5FC5\u987B\u5148\u5C06\u6B64\u5E94\u7528\u6DFB\u52A0\u5230\u4E3B\u5C4F\u5E55\u3002"
3579
+ },
3580
+ ja: {
3581
+ title: "\u6700\u65B0\u60C5\u5831\u3092\u5165\u624B",
3582
+ description: "\u91CD\u8981\u306A\u30A2\u30C3\u30D7\u30C7\u30FC\u30C8\u3001\u30E1\u30C3\u30BB\u30FC\u30B8\u3001\u30A2\u30E9\u30FC\u30C8\u306E\u901A\u77E5\u3092\u53D7\u3051\u53D6\u308A\u307E\u3059\u3002",
3583
+ allowButton: "\u901A\u77E5\u3092\u6709\u52B9\u306B\u3059\u308B",
3584
+ laterButton: "\u5F8C\u3067",
3585
+ iosNote: "iOS\u3067\u306F\u3001\u307E\u305A\u3053\u306E\u30A2\u30D7\u30EA\u3092\u30DB\u30FC\u30E0\u753B\u9762\u306B\u8FFD\u52A0\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"
3586
+ },
3587
+ ko: {
3588
+ title: "\uCD5C\uC2E0 \uC815\uBCF4 \uBC1B\uAE30",
3589
+ description: "\uC911\uC694\uD55C \uC5C5\uB370\uC774\uD2B8, \uBA54\uC2DC\uC9C0 \uBC0F \uC54C\uB9BC\uC5D0 \uB300\uD55C \uC54C\uB9BC\uC744 \uBC1B\uC73C\uC138\uC694.",
3590
+ allowButton: "\uC54C\uB9BC \uD65C\uC131\uD654",
3591
+ laterButton: "\uB098\uC911\uC5D0",
3592
+ iosNote: "iOS\uC5D0\uC11C\uB294 \uBA3C\uC800 \uC774 \uC571\uC744 \uD648 \uD654\uBA74\uC5D0 \uCD94\uAC00\uD574\uC57C \uD569\uB2C8\uB2E4."
3593
+ },
3594
+ ar: {
3595
+ title: "\u0627\u0628\u0642 \u0639\u0644\u0649 \u0627\u0637\u0644\u0627\u0639",
3596
+ description: "\u0627\u062D\u0635\u0644 \u0639\u0644\u0649 \u0625\u0634\u0639\u0627\u0631\u0627\u062A \u062D\u0648\u0644 \u0627\u0644\u062A\u062D\u062F\u064A\u062B\u0627\u062A \u0627\u0644\u0645\u0647\u0645\u0629 \u0648\u0627\u0644\u0631\u0633\u0627\u0626\u0644 \u0648\u0627\u0644\u062A\u0646\u0628\u064A\u0647\u0627\u062A.",
3597
+ allowButton: "\u062A\u0641\u0639\u064A\u0644 \u0627\u0644\u0625\u0634\u0639\u0627\u0631\u0627\u062A",
3598
+ laterButton: "\u0631\u0628\u0645\u0627 \u0644\u0627\u062D\u0642\u0627\u064B",
3599
+ iosNote: "\u0639\u0644\u0649 iOS\u060C \u064A\u062C\u0628 \u0623\u0648\u0644\u0627\u064B \u0625\u0636\u0627\u0641\u0629 \u0647\u0630\u0627 \u0627\u0644\u062A\u0637\u0628\u064A\u0642 \u0625\u0644\u0649 \u0627\u0644\u0634\u0627\u0634\u0629 \u0627\u0644\u0631\u0626\u064A\u0633\u064A\u0629."
3600
+ },
3601
+ he: {
3602
+ title: "\u05D4\u05D9\u05E9\u05D0\u05E8 \u05DE\u05E2\u05D5\u05D3\u05DB\u05DF",
3603
+ description: "\u05E7\u05D1\u05DC \u05D4\u05EA\u05E8\u05D0\u05D5\u05EA \u05E2\u05DC \u05E2\u05D3\u05DB\u05D5\u05E0\u05D9\u05DD \u05D7\u05E9\u05D5\u05D1\u05D9\u05DD, \u05D4\u05D5\u05D3\u05E2\u05D5\u05EA \u05D5\u05D4\u05EA\u05E8\u05D0\u05D5\u05EA.",
3604
+ allowButton: "\u05D4\u05E4\u05E2\u05DC \u05D4\u05EA\u05E8\u05D0\u05D5\u05EA",
3605
+ laterButton: "\u05D0\u05D5\u05DC\u05D9 \u05DE\u05D0\u05D5\u05D7\u05E8 \u05D9\u05D5\u05EA\u05E8",
3606
+ iosNote: "\u05D1-iOS, \u05E2\u05DC\u05D9\u05DA \u05DC\u05D4\u05D5\u05E1\u05D9\u05E3 \u05EA\u05D7\u05D9\u05DC\u05D4 \u05D0\u05EA \u05D4\u05D0\u05E4\u05DC\u05D9\u05E7\u05E6\u05D9\u05D4 \u05DC\u05DE\u05E1\u05DA \u05D4\u05D1\u05D9\u05EA."
3607
+ },
3608
+ hi: {
3609
+ title: "\u0905\u092A\u0921\u0947\u091F \u0930\u0939\u0947\u0902",
3610
+ description: "\u092E\u0939\u0924\u094D\u0935\u092A\u0942\u0930\u094D\u0923 \u0905\u092A\u0921\u0947\u091F, \u0938\u0902\u0926\u0947\u0936\u094B\u0902 \u0914\u0930 \u0905\u0932\u0930\u094D\u091F \u0915\u0947 \u092C\u093E\u0930\u0947 \u092E\u0947\u0902 \u0938\u0942\u091A\u0928\u093E\u090F\u0902 \u092A\u094D\u0930\u093E\u092A\u094D\u0924 \u0915\u0930\u0947\u0902\u0964",
3611
+ allowButton: "\u0938\u0942\u091A\u0928\u093E\u090F\u0902 \u0938\u0915\u094D\u0937\u092E \u0915\u0930\u0947\u0902",
3612
+ laterButton: "\u092C\u093E\u0926 \u092E\u0947\u0902",
3613
+ iosNote: "iOS \u092A\u0930, \u0906\u092A\u0915\u094B \u092A\u0939\u0932\u0947 \u0907\u0938 \u0910\u092A \u0915\u094B \u0939\u094B\u092E \u0938\u094D\u0915\u094D\u0930\u0940\u0928 \u092A\u0930 \u091C\u094B\u0921\u093C\u0928\u093E \u0939\u094B\u0917\u093E\u0964"
3614
+ },
3615
+ tr: {
3616
+ title: "G\xFCncel Kal\u0131n",
3617
+ description: "\xD6nemli g\xFCncellemeler, mesajlar ve uyar\u0131lar hakk\u0131nda bildirim al\u0131n.",
3618
+ allowButton: "Bildirimleri Etkinle\u015Ftir",
3619
+ laterButton: "Belki Sonra",
3620
+ iosNote: "iOS'ta \xF6nce bu uygulamay\u0131 ana ekran\u0131n\u0131za eklemelisiniz."
3621
+ },
3622
+ pl: {
3623
+ title: "B\u0105d\u017A Na Bie\u017C\u0105co",
3624
+ description: "Otrzymuj powiadomienia o wa\u017Cnych aktualizacjach, wiadomo\u015Bciach i alertach.",
3625
+ allowButton: "W\u0142\u0105cz Powiadomienia",
3626
+ laterButton: "Mo\u017Ce P\xF3\u017Aniej",
3627
+ iosNote: "Na iOS musisz najpierw doda\u0107 t\u0119 aplikacj\u0119 do ekranu g\u0142\xF3wnego."
3628
+ },
3629
+ sv: {
3630
+ title: "H\xE5ll Dig Uppdaterad",
3631
+ description: "F\xE5 aviseringar om viktiga uppdateringar, meddelanden och varningar.",
3632
+ allowButton: "Aktivera Aviseringar",
3633
+ laterButton: "Kanske Senare",
3634
+ iosNote: "P\xE5 iOS m\xE5ste du f\xF6rst l\xE4gga till denna app p\xE5 hemsk\xE4rmen."
3635
+ },
3636
+ da: {
3637
+ title: "Hold Dig Opdateret",
3638
+ description: "F\xE5 notifikationer om vigtige opdateringer, beskeder og advarsler.",
3639
+ allowButton: "Aktiv\xE9r Notifikationer",
3640
+ laterButton: "M\xE5ske Senere",
3641
+ iosNote: "P\xE5 iOS skal du f\xF8rst tilf\xF8je denne app til startsk\xE6rmen."
3642
+ },
3643
+ no: {
3644
+ title: "Hold Deg Oppdatert",
3645
+ description: "F\xE5 varsler om viktige oppdateringer, meldinger og varsler.",
3646
+ allowButton: "Aktiver Varsler",
3647
+ laterButton: "Kanskje Senere",
3648
+ iosNote: "P\xE5 iOS m\xE5 du f\xF8rst legge til denne appen p\xE5 startskjermen."
3649
+ },
3650
+ fi: {
3651
+ title: "Pysy Ajan Tasalla",
3652
+ description: "Saat ilmoituksia t\xE4rkeist\xE4 p\xE4ivityksist\xE4, viesteist\xE4 ja h\xE4lytyksist\xE4.",
3653
+ allowButton: "Ota Ilmoitukset K\xE4ytt\xF6\xF6n",
3654
+ laterButton: "Ehk\xE4 My\xF6hemmin",
3655
+ iosNote: "iOS:ssa sinun on ensin lis\xE4tt\xE4v\xE4 t\xE4m\xE4 sovellus aloitusn\xE4yt\xF6lle."
3656
+ }
3657
+ };
3658
+ var DEFAULT_STYLES2 = {
3659
+ backgroundColor: "#ffffff",
3660
+ textColor: "#1a1a1a",
3661
+ primaryButtonBackground: "#007AFF",
3662
+ primaryButtonText: "#ffffff",
3663
+ secondaryButtonBackground: "transparent",
3664
+ secondaryButtonText: "#666666",
3665
+ borderRadius: "16px",
3666
+ boxShadow: "0 4px 24px rgba(0, 0, 0, 0.15)"
3667
+ };
3668
+ var NotificationPermissionPrompt = class {
3669
+ constructor(options = {}) {
3670
+ this.promptElement = null;
3671
+ this.overlayElement = null;
3672
+ this.debug = options.debug ?? false;
3673
+ this.onAllow = options.onAllow;
3674
+ this.onLater = options.onLater;
3675
+ this.onShow = options.onShow;
3676
+ this.language = options.language ?? detectBrowserLanguage();
3677
+ const baseStrings = NOTIFICATION_PROMPT_TRANSLATIONS[this.language];
3678
+ this.strings = {
3679
+ ...baseStrings,
3680
+ ...options.customStrings
3681
+ };
3682
+ this.config = {
3683
+ enabled: options.config?.enabled ?? true,
3684
+ iconUrl: options.config?.iconUrl ?? "",
3685
+ showDelay: options.config?.showDelay ?? 2e3,
3686
+ dismissalDays: options.config?.dismissalDays ?? 3,
3687
+ position: options.config?.position ?? "center",
3688
+ customStyles: options.config?.customStyles ?? {},
3689
+ showIOSNote: options.config?.showIOSNote ?? true
3690
+ };
3691
+ this.styles = { ...DEFAULT_STYLES2, ...this.config.customStyles };
3692
+ this.log(`Initialized with language: ${this.language}`);
3693
+ }
3694
+ /**
3695
+ * Get current language
3696
+ */
3697
+ getLanguage() {
3698
+ return this.language;
3699
+ }
3700
+ /**
3701
+ * Set language
3702
+ */
3703
+ setLanguage(language) {
3704
+ this.language = language;
3705
+ this.strings = { ...NOTIFICATION_PROMPT_TRANSLATIONS[language] };
3706
+ this.log(`Language changed to: ${language}`);
3707
+ }
3708
+ /**
3709
+ * Check if should show prompt
3710
+ */
3711
+ shouldShow() {
3712
+ if ("Notification" in window && Notification.permission !== "default") {
3713
+ return false;
3714
+ }
3715
+ if (this.wasDismissed()) {
3716
+ return false;
3717
+ }
3718
+ return true;
3719
+ }
3720
+ /**
3721
+ * Check if previously dismissed
3722
+ */
3723
+ wasDismissed() {
3724
+ try {
3725
+ const dismissed = localStorage.getItem(STORAGE_KEY2);
3726
+ if (!dismissed) return false;
3727
+ const dismissedTime = parseInt(dismissed, 10);
3728
+ const daysSinceDismissal = (Date.now() - dismissedTime) / (1e3 * 60 * 60 * 24);
3729
+ if (daysSinceDismissal > this.config.dismissalDays) {
3730
+ localStorage.removeItem(STORAGE_KEY2);
3731
+ return false;
3732
+ }
3733
+ return true;
3734
+ } catch {
3735
+ return false;
3736
+ }
3737
+ }
3738
+ /**
3739
+ * Check if device is iOS
3740
+ */
3741
+ isIOS() {
3742
+ return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
3743
+ }
3744
+ /**
3745
+ * Check if PWA is installed (standalone mode)
3746
+ */
3747
+ isInstalled() {
3748
+ if (navigator.standalone === true) return true;
3749
+ if (window.matchMedia("(display-mode: standalone)").matches) return true;
3750
+ return false;
3751
+ }
3752
+ /**
3753
+ * Show the soft prompt
3754
+ */
3755
+ async show() {
3756
+ if (!this.config.enabled) {
3757
+ this.log("Prompt is disabled");
3758
+ return false;
3759
+ }
3760
+ if (!this.shouldShow()) {
3761
+ this.log("Prompt should not be shown");
3762
+ return false;
3763
+ }
3764
+ if (this.promptElement) {
3765
+ this.log("Prompt already visible");
3766
+ return true;
3767
+ }
3768
+ if (this.config.showDelay > 0) {
3769
+ await new Promise((resolve) => setTimeout(resolve, this.config.showDelay));
3770
+ }
3771
+ if (!this.shouldShow()) {
3772
+ return false;
3773
+ }
3774
+ this.createPrompt();
3775
+ this.log("Prompt shown");
3776
+ this.onShow?.();
3777
+ return true;
3778
+ }
3779
+ /**
3780
+ * Hide the prompt
3781
+ */
3782
+ hide() {
3783
+ if (this.promptElement) {
3784
+ this.promptElement.remove();
3785
+ this.promptElement = null;
3786
+ }
3787
+ if (this.overlayElement) {
3788
+ this.overlayElement.remove();
3789
+ this.overlayElement = null;
3790
+ }
3791
+ }
3792
+ /**
3793
+ * Dismiss and remember
3794
+ */
3795
+ dismiss() {
3796
+ this.hide();
3797
+ try {
3798
+ localStorage.setItem(STORAGE_KEY2, Date.now().toString());
3799
+ } catch {
3800
+ }
3801
+ this.log("Prompt dismissed");
3802
+ this.onLater?.();
3803
+ }
3804
+ /**
3805
+ * Reset dismissal state
3806
+ */
3807
+ resetDismissal() {
3808
+ try {
3809
+ localStorage.removeItem(STORAGE_KEY2);
3810
+ } catch {
3811
+ }
3812
+ }
3813
+ /**
3814
+ * Handle allow click - triggers native permission
3815
+ */
3816
+ async handleAllow() {
3817
+ this.hide();
3818
+ this.onAllow?.();
3819
+ if ("Notification" in window) {
3820
+ return await Notification.requestPermission();
3821
+ }
3822
+ return "denied";
3823
+ }
3824
+ /**
3825
+ * Create and inject the prompt element
3826
+ */
3827
+ createPrompt() {
3828
+ this.overlayElement = document.createElement("div");
3829
+ this.overlayElement.id = `${PROMPT_ID}-overlay`;
3830
+ this.overlayElement.style.cssText = `
3831
+ position: fixed;
3832
+ top: 0;
3833
+ left: 0;
3834
+ right: 0;
3835
+ bottom: 0;
3836
+ background: rgba(0, 0, 0, 0.5);
3837
+ z-index: 999998;
3838
+ opacity: 0;
3839
+ transition: opacity 0.3s ease;
3840
+ `;
3841
+ this.overlayElement.addEventListener("click", () => this.dismiss());
3842
+ document.body.appendChild(this.overlayElement);
3843
+ this.promptElement = document.createElement("div");
3844
+ this.promptElement.id = PROMPT_ID;
3845
+ let positionStyles;
3846
+ switch (this.config.position) {
3847
+ case "top":
3848
+ positionStyles = "top: 20px; left: 50%; transform: translateX(-50%) translateY(-20px);";
3849
+ break;
3850
+ case "bottom":
3851
+ positionStyles = "bottom: 20px; left: 50%; transform: translateX(-50%) translateY(20px);";
3852
+ break;
3853
+ default:
3854
+ positionStyles = "top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.95);";
3855
+ }
3856
+ this.promptElement.style.cssText = `
3857
+ position: fixed;
3858
+ ${positionStyles}
3859
+ width: 90%;
3860
+ max-width: 380px;
3861
+ background: ${this.styles.backgroundColor};
3862
+ color: ${this.styles.textColor};
3863
+ border-radius: ${this.styles.borderRadius};
3864
+ box-shadow: ${this.styles.boxShadow};
3865
+ z-index: 999999;
3866
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
3867
+ opacity: 0;
3868
+ transition: opacity 0.3s ease, transform 0.3s ease;
3869
+ `;
3870
+ const iconHtml = this.config.iconUrl ? `<img src="${this.config.iconUrl}" alt="App icon" style="width: 48px; height: 48px; border-radius: 10px; margin-bottom: 12px;">` : `<div style="width: 48px; height: 48px; border-radius: 10px; margin-bottom: 12px; background: linear-gradient(135deg, #007AFF, #5856D6); display: flex; align-items: center; justify-content: center;">
3871
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
3872
+ <path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/>
3873
+ <path d="M13.73 21a2 2 0 0 1-3.46 0"/>
3874
+ </svg>
3875
+ </div>`;
3876
+ const showIOSNote = this.config.showIOSNote && this.isIOS() && !this.isInstalled();
3877
+ const iosNoteHtml = showIOSNote && this.strings.iosNote ? `<p style="margin: 12px 0 0 0; padding: 10px; background: rgba(255, 149, 0, 0.1); border-radius: 8px; font-size: 12px; color: #996600;">
3878
+ \u26A0\uFE0F ${this.strings.iosNote}
3879
+ </p>` : "";
3880
+ this.promptElement.innerHTML = `
3881
+ <div style="padding: 24px; text-align: center;">
3882
+ ${iconHtml}
3883
+ <h3 style="margin: 0 0 8px 0; font-size: 20px; font-weight: 600;">${this.strings.title}</h3>
3884
+ <p style="margin: 0; font-size: 14px; opacity: 0.7; line-height: 1.5;">${this.strings.description}</p>
3885
+ ${iosNoteHtml}
3886
+ <div style="margin-top: 20px; display: flex; flex-direction: column; gap: 10px;">
3887
+ <button id="${PROMPT_ID}-allow" style="
3888
+ width: 100%;
3889
+ padding: 14px 20px;
3890
+ background: ${this.styles.primaryButtonBackground};
3891
+ color: ${this.styles.primaryButtonText};
3892
+ border: none;
3893
+ border-radius: 10px;
3894
+ font-size: 16px;
3895
+ font-weight: 600;
3896
+ cursor: pointer;
3897
+ -webkit-tap-highlight-color: transparent;
3898
+ ">${this.strings.allowButton}</button>
3899
+ <button id="${PROMPT_ID}-later" style="
3900
+ width: 100%;
3901
+ padding: 12px 20px;
3902
+ background: ${this.styles.secondaryButtonBackground};
3903
+ color: ${this.styles.secondaryButtonText};
3904
+ border: none;
3905
+ border-radius: 10px;
3906
+ font-size: 14px;
3907
+ cursor: pointer;
3908
+ -webkit-tap-highlight-color: transparent;
3909
+ ">${this.strings.laterButton}</button>
3910
+ </div>
3911
+ </div>
3912
+ `;
3913
+ document.body.appendChild(this.promptElement);
3914
+ document.getElementById(`${PROMPT_ID}-allow`)?.addEventListener("click", () => this.handleAllow());
3915
+ document.getElementById(`${PROMPT_ID}-later`)?.addEventListener("click", () => this.dismiss());
3916
+ requestAnimationFrame(() => {
3917
+ if (this.overlayElement) {
3918
+ this.overlayElement.style.opacity = "1";
3919
+ }
3920
+ if (this.promptElement) {
3921
+ this.promptElement.style.opacity = "1";
3922
+ if (this.config.position === "center") {
3923
+ this.promptElement.style.transform = "translate(-50%, -50%) scale(1)";
3924
+ } else if (this.config.position === "top") {
3925
+ this.promptElement.style.transform = "translateX(-50%) translateY(0)";
3926
+ } else {
3927
+ this.promptElement.style.transform = "translateX(-50%) translateY(0)";
3928
+ }
3929
+ }
3930
+ });
3931
+ }
3932
+ /**
3933
+ * Log message if debug is enabled
3934
+ */
3935
+ log(message, level = "log") {
3936
+ if (!this.debug && level === "log") return;
3937
+ const prefix = "[CloudSignal PWA NotificationPrompt]";
3938
+ console[level](`${prefix} ${message}`);
3939
+ }
3940
+ };
3941
+
2021
3942
  // src/index.ts
2022
- var VERSION = "1.0.0";
3943
+ var VERSION = "1.1.0";
2023
3944
 
2024
- export { CloudSignalPWA, DeviceDetector, HeartbeatManager, IndexedDBStorage, InstallationManager, PushNotificationManager, ServiceWorkerManager, VERSION, CloudSignalPWA_default as default, deviceDetector, generateAuthHeaders, generateBrowserFingerprint, generateHMACSignature, generateTrackingId, getRegistrationId, getStorageItem, isValidUUID, makeAuthenticatedRequest, removeRegistrationId, removeStorageItem, setRegistrationId, setStorageItem };
3945
+ export { CloudSignalPWA, DeviceDetector, HeartbeatManager, IOSInstallBanner, IOS_BANNER_TRANSLATIONS, IndexedDBStorage, InstallationManager, NOTIFICATION_PROMPT_TRANSLATIONS, NotificationPermissionPrompt, OfflineQueueManager, PushNotificationManager, ServiceWorkerManager, VERSION, WakeLockManager, CloudSignalPWA_default as default, detectBrowserLanguage, deviceDetector, generateAuthHeaders, generateBrowserFingerprint, generateHMACSignature, generateTrackingId, getRegistrationId, getStorageItem, isValidUUID, makeAuthenticatedRequest, removeRegistrationId, removeStorageItem, setRegistrationId, setStorageItem };
2025
3946
  //# sourceMappingURL=index.js.map
2026
3947
  //# sourceMappingURL=index.js.map