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