@swoff/cli 0.3.6 → 0.3.7

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.
Files changed (88) hide show
  1. package/README.md +375 -91
  2. package/bin/swoff +8 -14
  3. package/dist/__tests__/assemble-sw.test.js +2 -2
  4. package/dist/__tests__/assemble-sw.test.js.map +1 -1
  5. package/dist/__tests__/cli-integration.test.js +0 -1
  6. package/dist/__tests__/cli-integration.test.js.map +1 -1
  7. package/dist/__tests__/config-types.test.js +4 -3
  8. package/dist/__tests__/config-types.test.js.map +1 -1
  9. package/dist/__tests__/file-generators.test.js +12 -23
  10. package/dist/__tests__/file-generators.test.js.map +1 -1
  11. package/dist/__tests__/loader.test.js +0 -1
  12. package/dist/__tests__/loader.test.js.map +1 -1
  13. package/dist/__tests__/validator.test.js +0 -1
  14. package/dist/__tests__/validator.test.js.map +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/lib/commands/add.js +4 -4
  18. package/dist/lib/commands/add.js.map +1 -1
  19. package/dist/lib/commands/generate-guide.js +33 -6
  20. package/dist/lib/commands/generate-guide.js.map +1 -1
  21. package/dist/lib/commands/info.js +18 -4
  22. package/dist/lib/commands/info.js.map +1 -1
  23. package/dist/lib/commands/init.js +6 -2
  24. package/dist/lib/commands/init.js.map +1 -1
  25. package/dist/lib/generators/file-generators/auth-fetch.js +3 -3
  26. package/dist/lib/generators/file-generators/auth-fetch.js.map +1 -1
  27. package/dist/lib/generators/file-generators/auth-store.js +114 -5
  28. package/dist/lib/generators/file-generators/auth-store.js.map +1 -1
  29. package/dist/lib/generators/file-generators/auth-user.js +2 -2
  30. package/dist/lib/generators/file-generators/auth-user.js.map +1 -1
  31. package/dist/lib/generators/file-generators/cache.js +1 -3
  32. package/dist/lib/generators/file-generators/cache.js.map +1 -1
  33. package/dist/lib/generators/file-generators/client-injector.js +4 -16
  34. package/dist/lib/generators/file-generators/client-injector.js.map +1 -1
  35. package/dist/lib/generators/file-generators/fetch-wrapper.js +40 -15
  36. package/dist/lib/generators/file-generators/fetch-wrapper.js.map +1 -1
  37. package/dist/lib/generators/file-generators/generate-hooks.js +23 -209
  38. package/dist/lib/generators/file-generators/generate-hooks.js.map +1 -1
  39. package/dist/lib/generators/file-generators/guide-generator.js +153 -36
  40. package/dist/lib/generators/file-generators/guide-generator.js.map +1 -1
  41. package/dist/lib/generators/file-generators/mutation-queue.js +31 -3
  42. package/dist/lib/generators/file-generators/mutation-queue.js.map +1 -1
  43. package/dist/lib/generators/file-generators/push.js +143 -0
  44. package/dist/lib/generators/file-generators/push.js.map +1 -0
  45. package/dist/lib/generators/file-generators/quick-readme.js +13 -3
  46. package/dist/lib/generators/file-generators/quick-readme.js.map +1 -1
  47. package/dist/lib/generators/file-generators/sw-generator-build.js +1 -1
  48. package/dist/lib/generators/file-generators/sw-injector.js +1 -1
  49. package/dist/lib/generators/file-generators/sw-push.js +67 -0
  50. package/dist/lib/generators/file-generators/sw-push.js.map +1 -0
  51. package/dist/lib/generators/file-generators/sw-template.js +2 -0
  52. package/dist/lib/generators/file-generators/sw-template.js.map +1 -1
  53. package/dist/lib/generators/file-generators/type-definitions.js +13 -13
  54. package/dist/lib/generators/sw-generator.js +8 -9
  55. package/dist/lib/generators/sw-generator.js.map +1 -1
  56. package/dist/lib/generators/sw-sections/activate-handler.js +6 -2
  57. package/dist/lib/generators/sw-sections/activate-handler.js.map +1 -1
  58. package/dist/lib/generators/sw-sections/assemble-sw.js +5 -1
  59. package/dist/lib/generators/sw-sections/assemble-sw.js.map +1 -1
  60. package/dist/lib/generators/sw-sections/default-template.js +1 -30
  61. package/dist/lib/generators/sw-sections/default-template.js.map +1 -1
  62. package/dist/lib/generators/sw-sections/fetch-handler.js +157 -86
  63. package/dist/lib/generators/sw-sections/fetch-handler.js.map +1 -1
  64. package/dist/lib/generators/sw-sections/install-handler.js +12 -10
  65. package/dist/lib/generators/sw-sections/install-handler.js.map +1 -1
  66. package/dist/lib/generators/sw-sections/tag-management.js +11 -0
  67. package/dist/lib/generators/sw-sections/tag-management.js.map +1 -1
  68. package/dist/lib/generators/swoff-files-generator.js +9 -9
  69. package/dist/lib/generators/swoff-files-generator.js.map +1 -1
  70. package/dist/lib/shared/config-types.js +5 -3
  71. package/dist/lib/shared/config-types.js.map +1 -1
  72. package/package.json +3 -2
  73. package/templates/hooks/useAuth.jsx +30 -0
  74. package/templates/hooks/useAuth.tsx +30 -0
  75. package/templates/hooks/useBackgroundSync.jsx +31 -0
  76. package/templates/hooks/useBackgroundSync.tsx +31 -0
  77. package/templates/hooks/useCacheInvalidation.jsx +19 -0
  78. package/templates/hooks/useCacheInvalidation.tsx +19 -0
  79. package/templates/hooks/useCachedFetch.jsx +55 -0
  80. package/templates/hooks/useCachedFetch.tsx +69 -0
  81. package/templates/hooks/useMutationQueue.jsx +34 -0
  82. package/templates/hooks/useMutationQueue.tsx +34 -0
  83. package/templates/hooks/useNetworkStatus.jsx +19 -0
  84. package/templates/hooks/useNetworkStatus.tsx +19 -0
  85. package/templates/hooks/usePushSubscription.jsx +67 -0
  86. package/templates/hooks/usePushSubscription.tsx +67 -0
  87. package/templates/hooks/useSWUpdate.jsx +76 -0
  88. package/templates/hooks/useSWUpdate.tsx +76 -0
@@ -0,0 +1,31 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import { retrySync } from "../background-sync.js";
3
+
4
+ export function useBackgroundSync() {
5
+ const [state, setState] = useState({
6
+ supported: "serviceWorker" in navigator && "SyncManager" in window,
7
+ registered: false,
8
+ lastSync: null,
9
+ });
10
+
11
+ useEffect(() => {
12
+ const onSyncComplete = (e) => {
13
+ setState((s) => ({
14
+ ...s,
15
+ registered: true,
16
+ lastSync: { succeeded: e.detail.succeeded, failed: e.detail.failed },
17
+ }));
18
+ };
19
+
20
+ window.addEventListener("background-sync-complete", onSyncComplete);
21
+ return () => {
22
+ window.removeEventListener("background-sync-complete", onSyncComplete);
23
+ };
24
+ }, []);
25
+
26
+ const triggerSync = useCallback(async () => {
27
+ await retrySync();
28
+ }, []);
29
+
30
+ return { ...state, triggerSync };
31
+ }
@@ -0,0 +1,31 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import { retrySync } from "../background-sync.ts";
3
+
4
+ export function useBackgroundSync() {
5
+ const [state, setState] = useState({
6
+ supported: "serviceWorker" in navigator && "SyncManager" in window,
7
+ registered: false,
8
+ lastSync: null as { succeeded: number; failed: number } | null,
9
+ });
10
+
11
+ useEffect(() => {
12
+ const onSyncComplete = (e: CustomEvent) => {
13
+ setState((s) => ({
14
+ ...s,
15
+ registered: true,
16
+ lastSync: { succeeded: e.detail.succeeded, failed: e.detail.failed },
17
+ }));
18
+ };
19
+
20
+ window.addEventListener("background-sync-complete", onSyncComplete);
21
+ return () => {
22
+ window.removeEventListener("background-sync-complete", onSyncComplete);
23
+ };
24
+ }, []);
25
+
26
+ const triggerSync = useCallback(async () => {
27
+ await retrySync();
28
+ }, []);
29
+
30
+ return { ...state, triggerSync };
31
+ }
@@ -0,0 +1,19 @@
1
+ import { useCallback } from "react";
2
+ import { invalidateByTag, invalidateByTags } from "../cache.js";
3
+ import { invalidateUrl } from "../invalidation-tags.js";
4
+
5
+ export function useCacheInvalidation() {
6
+ const invalidateTag = useCallback(async (tag) => {
7
+ await invalidateByTag(tag);
8
+ }, []);
9
+
10
+ const invalidateTags = useCallback(async (tags) => {
11
+ await invalidateByTags(tags);
12
+ }, []);
13
+
14
+ const invalidateCacheUrl = useCallback(async (url) => {
15
+ await invalidateUrl(url);
16
+ }, []);
17
+
18
+ return { invalidateByTag: invalidateTag, invalidateByTags: invalidateTags, invalidateUrl: invalidateCacheUrl };
19
+ }
@@ -0,0 +1,19 @@
1
+ import { useCallback } from "react";
2
+ import { invalidateByTag, invalidateByTags } from "../cache.ts";
3
+ import { invalidateUrl } from "../invalidation-tags.ts";
4
+
5
+ export function useCacheInvalidation() {
6
+ const invalidateTag = useCallback(async (tag: string) => {
7
+ await invalidateByTag(tag);
8
+ }, []);
9
+
10
+ const invalidateTags = useCallback(async (tags: string[]) => {
11
+ await invalidateByTags(tags);
12
+ }, []);
13
+
14
+ const invalidateCacheUrl = useCallback(async (url: string) => {
15
+ await invalidateUrl(url);
16
+ }, []);
17
+
18
+ return { invalidateByTag: invalidateTag, invalidateByTags: invalidateTags, invalidateUrl: invalidateCacheUrl };
19
+ }
@@ -0,0 +1,55 @@
1
+ import { useState, useEffect, useCallback, useRef, startTransition } from "react";
2
+ import { fetchWithCache } from "../fetch-wrapper.js";
3
+ import { generateTags } from "../invalidation-tags.js";
4
+
5
+ export function useCachedFetch(url, options = {}) {
6
+ const [data, setData] = useState(null);
7
+ const [error, setError] = useState(null);
8
+ const [loading, setLoading] = useState(true);
9
+ const [refetchCount, setRefetchCount] = useState(0);
10
+ const optionsRef = useRef(options);
11
+ useEffect(() => { optionsRef.current = options; }, [options]);
12
+
13
+ const refetch = useCallback(() => setRefetchCount((c) => c + 1), []);
14
+
15
+ useEffect(() => {
16
+ let cancelled = false;
17
+ startTransition(() => setLoading(true));
18
+
19
+ const doFetch = async () => {
20
+ try {
21
+ const { response } = await fetchWithCache(url, optionsRef.current);
22
+ if (cancelled) return;
23
+ if (response) {
24
+ setData(await response.json());
25
+ } else {
26
+ setData(null);
27
+ }
28
+ if (!cancelled) setError(null);
29
+ } catch (err) {
30
+ if (!cancelled) setError(err instanceof Error ? err : new Error(String(err)));
31
+ } finally {
32
+ if (!cancelled) setLoading(false);
33
+ }
34
+ };
35
+
36
+ doFetch();
37
+ return () => { cancelled = true; };
38
+ }, [url, refetchCount]);
39
+
40
+ useEffect(() => {
41
+ const onInvalidated = (e) => {
42
+ const detail = e.detail;
43
+ const tags = detail?.tags;
44
+ if (!tags || !Array.isArray(tags)) return;
45
+ const urlTags = generateTags(url);
46
+ if (tags.some((t) => urlTags.includes(t))) {
47
+ setRefetchCount((c) => c + 1);
48
+ }
49
+ };
50
+ window.addEventListener("cache-invalidated", onInvalidated);
51
+ return () => window.removeEventListener("cache-invalidated", onInvalidated);
52
+ }, [url]);
53
+
54
+ return { data, error, loading, refetch };
55
+ }
@@ -0,0 +1,69 @@
1
+ import {
2
+ useState,
3
+ useEffect,
4
+ useCallback,
5
+ useRef,
6
+ startTransition,
7
+ } from "react";
8
+ import { fetchWithCache } from "../fetch-wrapper.ts";
9
+ import { generateTags } from "../invalidation-tags.ts";
10
+
11
+ export function useCachedFetch<T>(
12
+ url: string,
13
+ options: Parameters<typeof fetchWithCache>[1] = {},
14
+ ) {
15
+ const [data, setData] = useState<T | null>(null);
16
+ const [error, setError] = useState<Error | null>(null);
17
+ const [loading, setLoading] = useState(true);
18
+ const [refetchCount, setRefetchCount] = useState(0);
19
+ const optionsRef = useRef(options);
20
+ useEffect(() => {
21
+ optionsRef.current = options;
22
+ }, [options]);
23
+
24
+ const refetch = useCallback(() => setRefetchCount((c) => c + 1), []);
25
+
26
+ useEffect(() => {
27
+ let cancelled = false;
28
+ startTransition(() => setLoading(true));
29
+
30
+ const doFetch = async () => {
31
+ try {
32
+ const { response } = await fetchWithCache(url, optionsRef.current);
33
+ if (cancelled) return;
34
+ if (response) {
35
+ setData(await response.json());
36
+ } else {
37
+ setData(null);
38
+ }
39
+ if (!cancelled) setError(null);
40
+ } catch (err) {
41
+ if (!cancelled)
42
+ setError(err instanceof Error ? err : new Error(String(err)));
43
+ } finally {
44
+ if (!cancelled) setLoading(false);
45
+ }
46
+ };
47
+
48
+ doFetch();
49
+ return () => {
50
+ cancelled = true;
51
+ };
52
+ }, [url, refetchCount]);
53
+
54
+ useEffect(() => {
55
+ const onInvalidated = (e: Event) => {
56
+ const detail = (e as CustomEvent).detail;
57
+ const tags = detail?.tags;
58
+ if (!tags || !Array.isArray(tags)) return;
59
+ const urlTags = generateTags(url);
60
+ if (tags.some((t: string) => urlTags.includes(t))) {
61
+ setRefetchCount((c) => c + 1);
62
+ }
63
+ };
64
+ window.addEventListener("cache-invalidated", onInvalidated);
65
+ return () => window.removeEventListener("cache-invalidated", onInvalidated);
66
+ }, [url]);
67
+
68
+ return { data, error, loading, refetch };
69
+ }
@@ -0,0 +1,34 @@
1
+ import { useState, useEffect } from "react";
2
+ import { getPendingCount } from "../mutation-queue.js";
3
+
4
+ export function useMutationQueue() {
5
+ const [state, setState] = useState({
6
+ pending: 0,
7
+ lastSync: null,
8
+ });
9
+
10
+ useEffect(() => {
11
+ getPendingCount().then((count) => setState((s) => ({ ...s, pending: count })));
12
+
13
+ const onSync = (e) => {
14
+ getPendingCount().then((count) => {
15
+ setState({
16
+ pending: count,
17
+ lastSync: { succeeded: e.detail.succeeded, failed: e.detail.failed },
18
+ });
19
+ });
20
+ };
21
+ const onChange = () => {
22
+ getPendingCount().then((count) => setState((s) => ({ ...s, pending: count })));
23
+ };
24
+
25
+ window.addEventListener("mutation-sync-complete", onSync);
26
+ window.addEventListener("mutation-queue-changed", onChange);
27
+ return () => {
28
+ window.removeEventListener("mutation-sync-complete", onSync);
29
+ window.removeEventListener("mutation-queue-changed", onChange);
30
+ };
31
+ }, []);
32
+
33
+ return state;
34
+ }
@@ -0,0 +1,34 @@
1
+ import { useState, useEffect } from "react";
2
+ import { getPendingCount } from "../mutation-queue.ts";
3
+
4
+ export function useMutationQueue() {
5
+ const [state, setState] = useState<{
6
+ pending: number;
7
+ lastSync: { succeeded: number; failed: number } | null;
8
+ }>({ pending: 0, lastSync: null });
9
+
10
+ useEffect(() => {
11
+ getPendingCount().then((count: number) => setState((s) => ({ ...s, pending: count })));
12
+
13
+ const onSync = (e: CustomEvent) => {
14
+ getPendingCount().then((count: number) => {
15
+ setState({
16
+ pending: count,
17
+ lastSync: { succeeded: e.detail.succeeded, failed: e.detail.failed },
18
+ });
19
+ });
20
+ };
21
+ const onChange = () => {
22
+ getPendingCount().then((count: number) => setState((s) => ({ ...s, pending: count })));
23
+ };
24
+
25
+ window.addEventListener("mutation-sync-complete", onSync);
26
+ window.addEventListener("mutation-queue-changed", onChange);
27
+ return () => {
28
+ window.removeEventListener("mutation-sync-complete", onSync);
29
+ window.removeEventListener("mutation-queue-changed", onChange);
30
+ };
31
+ }, []);
32
+
33
+ return state;
34
+ }
@@ -0,0 +1,19 @@
1
+ import { useState, useEffect } from "react";
2
+
3
+ export function useNetworkStatus() {
4
+ const [online, setOnline] = useState(navigator.onLine);
5
+
6
+ useEffect(() => {
7
+ const onOnline = () => setOnline(true);
8
+ const onOffline = () => setOnline(false);
9
+
10
+ window.addEventListener("online", onOnline);
11
+ window.addEventListener("offline", onOffline);
12
+ return () => {
13
+ window.removeEventListener("online", onOnline);
14
+ window.removeEventListener("offline", onOffline);
15
+ };
16
+ }, []);
17
+
18
+ return online;
19
+ }
@@ -0,0 +1,19 @@
1
+ import { useState, useEffect } from "react";
2
+
3
+ export function useNetworkStatus() {
4
+ const [online, setOnline] = useState(navigator.onLine);
5
+
6
+ useEffect(() => {
7
+ const onOnline = () => setOnline(true);
8
+ const onOffline = () => setOnline(false);
9
+
10
+ window.addEventListener("online", onOnline);
11
+ window.addEventListener("offline", onOffline);
12
+ return () => {
13
+ window.removeEventListener("online", onOnline);
14
+ window.removeEventListener("offline", onOffline);
15
+ };
16
+ }, []);
17
+
18
+ return online;
19
+ }
@@ -0,0 +1,67 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import {
3
+ subscribeToPush,
4
+ unsubscribeFromPush,
5
+ isSubscribed,
6
+ getPushSubscription,
7
+ } from "../push.js";
8
+
9
+ export function usePushSubscription(vapidPublicKey) {
10
+ const [state, setState] = useState({
11
+ subscribed: false,
12
+ subscription: null,
13
+ permission: Notification.permission,
14
+ loading: true,
15
+ });
16
+
17
+ useEffect(() => {
18
+ let cancelled = false;
19
+
20
+ isSubscribed().then((subscribed) => {
21
+ if (cancelled) return;
22
+ if (subscribed) {
23
+ getPushSubscription().then((sub) => {
24
+ if (cancelled) return;
25
+ setState({ subscribed: true, subscription: sub, permission: Notification.permission, loading: false });
26
+ });
27
+ } else {
28
+ setState({ subscribed: false, subscription: null, permission: Notification.permission, loading: false });
29
+ }
30
+ });
31
+
32
+ const onSubChanged = (e) => {
33
+ if (cancelled) return;
34
+ if (!e.detail.subscribed) {
35
+ setState({ subscribed: false, subscription: null, permission: Notification.permission, loading: false });
36
+ } else {
37
+ getPushSubscription().then((sub) => {
38
+ if (cancelled) return;
39
+ setState({ subscribed: true, subscription: sub, permission: Notification.permission, loading: false });
40
+ });
41
+ }
42
+ };
43
+ const onPermissionChanged = (e) => {
44
+ if (cancelled) return;
45
+ setState((s) => ({ ...s, permission: e.detail.permission }));
46
+ };
47
+
48
+ window.addEventListener("push-subscription-changed", onSubChanged);
49
+ window.addEventListener("push-permission-changed", onPermissionChanged);
50
+ return () => {
51
+ cancelled = true;
52
+ window.removeEventListener("push-subscription-changed", onSubChanged);
53
+ window.removeEventListener("push-permission-changed", onPermissionChanged);
54
+ };
55
+ }, []);
56
+
57
+ const subscribe = useCallback(async () => {
58
+ const sub = await subscribeToPush(vapidPublicKey);
59
+ return sub !== null;
60
+ }, [vapidPublicKey]);
61
+
62
+ const unsubscribe = useCallback(async () => {
63
+ await unsubscribeFromPush();
64
+ }, []);
65
+
66
+ return { ...state, subscribe, unsubscribe };
67
+ }
@@ -0,0 +1,67 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import {
3
+ subscribeToPush,
4
+ unsubscribeFromPush,
5
+ isSubscribed,
6
+ getPushSubscription,
7
+ } from "../push.ts";
8
+
9
+ export function usePushSubscription(vapidPublicKey: string) {
10
+ const [state, setState] = useState({
11
+ subscribed: false,
12
+ subscription: null as PushSubscription | null,
13
+ permission: Notification.permission,
14
+ loading: true,
15
+ });
16
+
17
+ useEffect(() => {
18
+ let cancelled = false;
19
+
20
+ isSubscribed().then((subscribed) => {
21
+ if (cancelled) return;
22
+ if (subscribed) {
23
+ getPushSubscription().then((sub) => {
24
+ if (cancelled) return;
25
+ setState({ subscribed: true, subscription: sub, permission: Notification.permission, loading: false });
26
+ });
27
+ } else {
28
+ setState({ subscribed: false, subscription: null, permission: Notification.permission, loading: false });
29
+ }
30
+ });
31
+
32
+ const onSubChanged = (e: CustomEvent) => {
33
+ if (cancelled) return;
34
+ if (!e.detail.subscribed) {
35
+ setState({ subscribed: false, subscription: null, permission: Notification.permission, loading: false });
36
+ } else {
37
+ getPushSubscription().then((sub) => {
38
+ if (cancelled) return;
39
+ setState({ subscribed: true, subscription: sub, permission: Notification.permission, loading: false });
40
+ });
41
+ }
42
+ };
43
+ const onPermissionChanged = (e: CustomEvent) => {
44
+ if (cancelled) return;
45
+ setState((s) => ({ ...s, permission: e.detail.permission }));
46
+ };
47
+
48
+ window.addEventListener("push-subscription-changed", onSubChanged);
49
+ window.addEventListener("push-permission-changed", onPermissionChanged);
50
+ return () => {
51
+ cancelled = true;
52
+ window.removeEventListener("push-subscription-changed", onSubChanged);
53
+ window.removeEventListener("push-permission-changed", onPermissionChanged);
54
+ };
55
+ }, []);
56
+
57
+ const subscribe = useCallback(async () => {
58
+ const sub = await subscribeToPush(vapidPublicKey);
59
+ return sub !== null;
60
+ }, [vapidPublicKey]);
61
+
62
+ const unsubscribe = useCallback(async () => {
63
+ await unsubscribeFromPush();
64
+ }, []);
65
+
66
+ return { ...state, subscribe, unsubscribe };
67
+ }
@@ -0,0 +1,76 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import { handleUpdateApproved } from "../sw/injector.js";
3
+
4
+ export function useSWUpdate() {
5
+ const [state, setState] = useState({
6
+ updateStatus: "idle",
7
+ currentVersion: window.currentSWVersion || null,
8
+ availableVersion: null,
9
+ progress: 0,
10
+ forceUpdate: false,
11
+ error: null,
12
+ });
13
+
14
+ useEffect(() => {
15
+ if (sessionStorage.getItem("sw-dismissed-update") === "true") return;
16
+
17
+ const onAvailable = (e) =>
18
+ setState((s) => ({
19
+ ...s,
20
+ updateStatus: "available",
21
+ availableVersion: e.detail.version,
22
+ forceUpdate: window.swUpdateRequired || false,
23
+ }));
24
+ const onProgress = (e) =>
25
+ setState((s) => ({ ...s, updateStatus: "downloading", progress: e.detail.percent }));
26
+ const onReady = () =>
27
+ setState((s) => ({ ...s, updateStatus: "idle", progress: 0 }));
28
+ const onError = () =>
29
+ setState((s) => ({ ...s, error: "SW registration failed" }));
30
+
31
+ window.addEventListener("sw-update-available", onAvailable);
32
+ window.addEventListener("sw-progress", onProgress);
33
+ window.addEventListener("sw-ready", onReady);
34
+ window.addEventListener("sw-error", onError);
35
+ return () => {
36
+ window.removeEventListener("sw-update-available", onAvailable);
37
+ window.removeEventListener("sw-progress", onProgress);
38
+ window.removeEventListener("sw-ready", onReady);
39
+ window.removeEventListener("sw-error", onError);
40
+ };
41
+ }, []);
42
+
43
+ const acceptUpdate = useCallback(async () => {
44
+ if (!state.availableVersion) return;
45
+ await handleUpdateApproved(state.availableVersion);
46
+ }, [state.availableVersion]);
47
+
48
+ const dismissUpdate = useCallback(() => {
49
+ sessionStorage.setItem("sw-dismissed-update", "true");
50
+ setState((s) => ({ ...s, updateStatus: "idle" }));
51
+ }, []);
52
+
53
+ return { ...state, acceptUpdate, dismissUpdate };
54
+ }
55
+
56
+ export function useSWProgress() {
57
+ const [state, setState] = useState({
58
+ status: "idle",
59
+ progress: 0,
60
+ });
61
+
62
+ useEffect(() => {
63
+ const onProgress = (e) =>
64
+ setState({ status: "installing", progress: e.detail.percent });
65
+ const onReady = () => setState({ status: "idle", progress: 0 });
66
+
67
+ window.addEventListener("sw-progress", onProgress);
68
+ window.addEventListener("sw-ready", onReady);
69
+ return () => {
70
+ window.removeEventListener("sw-progress", onProgress);
71
+ window.removeEventListener("sw-ready", onReady);
72
+ };
73
+ }, []);
74
+
75
+ return state;
76
+ }
@@ -0,0 +1,76 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import { handleUpdateApproved } from "../sw/injector.ts";
3
+
4
+ export function useSWUpdate() {
5
+ const [state, setState] = useState({
6
+ updateStatus: "idle" as "idle" | "available" | "downloading" | "ready",
7
+ currentVersion: (window.currentSWVersion as string | undefined) || null,
8
+ availableVersion: null as string | null,
9
+ progress: 0 as number,
10
+ forceUpdate: false,
11
+ error: null as string | null,
12
+ });
13
+
14
+ useEffect(() => {
15
+ if (sessionStorage.getItem("sw-dismissed-update") === "true") return;
16
+
17
+ const onAvailable = (e: WindowEventMap["sw-update-available"]) =>
18
+ setState((s) => ({
19
+ ...s,
20
+ updateStatus: "available",
21
+ availableVersion: e.detail.version,
22
+ forceUpdate: window.swUpdateRequired || false,
23
+ }));
24
+ const onProgress = (e: WindowEventMap["sw-progress"]) =>
25
+ setState((s) => ({ ...s, updateStatus: "downloading", progress: e.detail.percent }));
26
+ const onReady = () =>
27
+ setState((s) => ({ ...s, updateStatus: "idle", progress: 0 }));
28
+ const onError = () =>
29
+ setState((s) => ({ ...s, error: "SW registration failed" }));
30
+
31
+ window.addEventListener("sw-update-available", onAvailable);
32
+ window.addEventListener("sw-progress", onProgress);
33
+ window.addEventListener("sw-ready", onReady);
34
+ window.addEventListener("sw-error", onError);
35
+ return () => {
36
+ window.removeEventListener("sw-update-available", onAvailable);
37
+ window.removeEventListener("sw-progress", onProgress);
38
+ window.removeEventListener("sw-ready", onReady);
39
+ window.removeEventListener("sw-error", onError);
40
+ };
41
+ }, []);
42
+
43
+ const acceptUpdate = useCallback(async () => {
44
+ if (!state.availableVersion) return;
45
+ await handleUpdateApproved(state.availableVersion);
46
+ }, [state.availableVersion]);
47
+
48
+ const dismissUpdate = useCallback(() => {
49
+ sessionStorage.setItem("sw-dismissed-update", "true");
50
+ setState((s) => ({ ...s, updateStatus: "idle" }));
51
+ }, []);
52
+
53
+ return { ...state, acceptUpdate, dismissUpdate };
54
+ }
55
+
56
+ export function useSWProgress() {
57
+ const [state, setState] = useState({
58
+ status: "idle" as "idle" | "installing",
59
+ progress: 0 as number,
60
+ });
61
+
62
+ useEffect(() => {
63
+ const onProgress = (e: WindowEventMap["sw-progress"]) =>
64
+ setState({ status: "installing", progress: e.detail.percent });
65
+ const onReady = () => setState({ status: "idle", progress: 0 });
66
+
67
+ window.addEventListener("sw-progress", onProgress);
68
+ window.addEventListener("sw-ready", onReady);
69
+ return () => {
70
+ window.removeEventListener("sw-progress", onProgress);
71
+ window.removeEventListener("sw-ready", onReady);
72
+ };
73
+ }, []);
74
+
75
+ return state;
76
+ }