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