@cloudsignal/pwa-sdk 1.0.0 → 1.1.0
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/README.md +128 -3
- package/dist/index.cjs +2010 -82
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +855 -3
- package/dist/index.d.ts +855 -3
- package/dist/index.global.js +140 -3
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +2004 -83
- package/dist/index.js.map +1 -1
- package/dist/service-worker.js +171 -6
- package/dist/service-worker.js.map +1 -1
- package/package.json +6 -2
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.
|
|
1587
|
+
this.updateIntervalForNetwork();
|
|
1588
|
+
this.log(`Starting heartbeat with interval ${this.currentInterval}ms`);
|
|
1560
1589
|
this.sendHeartbeat();
|
|
1561
|
-
this.
|
|
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
|
-
*
|
|
2791
|
+
* Hide the install banner
|
|
1570
2792
|
*/
|
|
1571
|
-
|
|
1572
|
-
if (
|
|
1573
|
-
|
|
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.
|
|
1581
|
-
|
|
1582
|
-
this.
|
|
2798
|
+
if (this.overlayElement) {
|
|
2799
|
+
this.overlayElement.remove();
|
|
2800
|
+
this.overlayElement = null;
|
|
1583
2801
|
}
|
|
1584
|
-
this.log("Heartbeat stopped");
|
|
1585
2802
|
}
|
|
1586
2803
|
/**
|
|
1587
|
-
*
|
|
2804
|
+
* Dismiss the banner and remember the dismissal
|
|
1588
2805
|
*/
|
|
1589
|
-
|
|
1590
|
-
|
|
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
|
-
*
|
|
2816
|
+
* Reset dismissal state (for testing or after significant time)
|
|
1594
2817
|
*/
|
|
1595
|
-
|
|
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
|
-
|
|
1602
|
-
|
|
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
|
-
*
|
|
2825
|
+
* Get iOS install steps (localized)
|
|
1623
2826
|
*/
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
this.
|
|
1628
|
-
|
|
1629
|
-
|
|
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
|
-
*
|
|
1639
|
-
*/
|
|
1640
|
-
|
|
1641
|
-
this.
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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;
|