@swoff/cli 0.3.6 → 0.3.8

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 (109) hide show
  1. package/README.md +43 -326
  2. package/bin/swoff +8 -14
  3. package/dist/__tests__/assemble-sw.test.js +6 -6
  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 +9 -5
  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 +50 -2
  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 +7 -6
  18. package/dist/lib/commands/add.js.map +1 -1
  19. package/dist/lib/commands/generate-guide.js +36 -9
  20. package/dist/lib/commands/generate-guide.js.map +1 -1
  21. package/dist/lib/commands/info.js +45 -8
  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/config/validator.js +85 -4
  26. package/dist/lib/config/validator.js.map +1 -1
  27. package/dist/lib/generators/file-generators/auth-fetch.js +3 -3
  28. package/dist/lib/generators/file-generators/auth-fetch.js.map +1 -1
  29. package/dist/lib/generators/file-generators/auth-store.js +114 -5
  30. package/dist/lib/generators/file-generators/auth-store.js.map +1 -1
  31. package/dist/lib/generators/file-generators/auth-user.js +2 -2
  32. package/dist/lib/generators/file-generators/auth-user.js.map +1 -1
  33. package/dist/lib/generators/file-generators/cache.js +1 -3
  34. package/dist/lib/generators/file-generators/cache.js.map +1 -1
  35. package/dist/lib/generators/file-generators/client-injector.js +5 -17
  36. package/dist/lib/generators/file-generators/client-injector.js.map +1 -1
  37. package/dist/lib/generators/file-generators/fetch-wrapper.js +86 -20
  38. package/dist/lib/generators/file-generators/fetch-wrapper.js.map +1 -1
  39. package/dist/lib/generators/file-generators/generate-hooks.js +28 -210
  40. package/dist/lib/generators/file-generators/generate-hooks.js.map +1 -1
  41. package/dist/lib/generators/file-generators/gql-wrapper.js +152 -0
  42. package/dist/lib/generators/file-generators/gql-wrapper.js.map +1 -0
  43. package/dist/lib/generators/file-generators/guide-generator.js +357 -36
  44. package/dist/lib/generators/file-generators/guide-generator.js.map +1 -1
  45. package/dist/lib/generators/file-generators/mutation-queue.js +130 -34
  46. package/dist/lib/generators/file-generators/mutation-queue.js.map +1 -1
  47. package/dist/lib/generators/file-generators/mutation-state.js +126 -0
  48. package/dist/lib/generators/file-generators/mutation-state.js.map +1 -0
  49. package/dist/lib/generators/file-generators/push.js +143 -0
  50. package/dist/lib/generators/file-generators/push.js.map +1 -0
  51. package/dist/lib/generators/file-generators/quick-readme.js +71 -11
  52. package/dist/lib/generators/file-generators/quick-readme.js.map +1 -1
  53. package/dist/lib/generators/file-generators/server-push.js +136 -0
  54. package/dist/lib/generators/file-generators/server-push.js.map +1 -0
  55. package/dist/lib/generators/file-generators/sw-generator-build.js +1 -1
  56. package/dist/lib/generators/file-generators/sw-injector.js +1 -1
  57. package/dist/lib/generators/file-generators/sw-push.js +67 -0
  58. package/dist/lib/generators/file-generators/sw-push.js.map +1 -0
  59. package/dist/lib/generators/file-generators/sw-template.js +2 -0
  60. package/dist/lib/generators/file-generators/sw-template.js.map +1 -1
  61. package/dist/lib/generators/file-generators/type-definitions.js +39 -11
  62. package/dist/lib/generators/file-generators/type-definitions.js.map +1 -1
  63. package/dist/lib/generators/sw-generator.js +8 -9
  64. package/dist/lib/generators/sw-generator.js.map +1 -1
  65. package/dist/lib/generators/sw-sections/activate-handler.js +6 -2
  66. package/dist/lib/generators/sw-sections/activate-handler.js.map +1 -1
  67. package/dist/lib/generators/sw-sections/assemble-sw.js +11 -2
  68. package/dist/lib/generators/sw-sections/assemble-sw.js.map +1 -1
  69. package/dist/lib/generators/sw-sections/background-sync-handler.js +22 -8
  70. package/dist/lib/generators/sw-sections/background-sync-handler.js.map +1 -1
  71. package/dist/lib/generators/sw-sections/config-header.js +1 -1
  72. package/dist/lib/generators/sw-sections/config-header.js.map +1 -1
  73. package/dist/lib/generators/sw-sections/default-template.js +2 -30
  74. package/dist/lib/generators/sw-sections/default-template.js.map +1 -1
  75. package/dist/lib/generators/sw-sections/fetch-handler.js +230 -120
  76. package/dist/lib/generators/sw-sections/fetch-handler.js.map +1 -1
  77. package/dist/lib/generators/sw-sections/install-handler.js +12 -10
  78. package/dist/lib/generators/sw-sections/install-handler.js.map +1 -1
  79. package/dist/lib/generators/sw-sections/server-push-handler.js +114 -0
  80. package/dist/lib/generators/sw-sections/server-push-handler.js.map +1 -0
  81. package/dist/lib/generators/sw-sections/tag-management.js +17 -5
  82. package/dist/lib/generators/sw-sections/tag-management.js.map +1 -1
  83. package/dist/lib/generators/swoff-files-generator.js +16 -10
  84. package/dist/lib/generators/swoff-files-generator.js.map +1 -1
  85. package/dist/lib/shared/config-types.js +44 -4
  86. package/dist/lib/shared/config-types.js.map +1 -1
  87. package/package.json +3 -2
  88. package/templates/hooks/useAuth.jsx +30 -0
  89. package/templates/hooks/useAuth.tsx +30 -0
  90. package/templates/hooks/useBackgroundSync.jsx +31 -0
  91. package/templates/hooks/useBackgroundSync.tsx +31 -0
  92. package/templates/hooks/useCacheInvalidation.jsx +19 -0
  93. package/templates/hooks/useCacheInvalidation.tsx +19 -0
  94. package/templates/hooks/useCachedFetch.jsx +94 -0
  95. package/templates/hooks/useCachedFetch.tsx +112 -0
  96. package/templates/hooks/useMutation.jsx +76 -0
  97. package/templates/hooks/useMutation.tsx +97 -0
  98. package/templates/hooks/useMutationQueue.jsx +42 -0
  99. package/templates/hooks/useMutationQueue.tsx +49 -0
  100. package/templates/hooks/useMutationState.jsx +27 -0
  101. package/templates/hooks/useMutationState.tsx +41 -0
  102. package/templates/hooks/useNetworkStatus.jsx +19 -0
  103. package/templates/hooks/useNetworkStatus.tsx +19 -0
  104. package/templates/hooks/usePrefetch.jsx +10 -0
  105. package/templates/hooks/usePrefetch.tsx +25 -0
  106. package/templates/hooks/usePushSubscription.jsx +67 -0
  107. package/templates/hooks/usePushSubscription.tsx +67 -0
  108. package/templates/hooks/useSWUpdate.jsx +76 -0
  109. package/templates/hooks/useSWUpdate.tsx +76 -0
@@ -0,0 +1,76 @@
1
+ import { useEffect, useState, useCallback, useRef } from "react";
2
+ import { fetchWithCache } from "../fetch-wrapper.js";
3
+ import {
4
+ trackMutation,
5
+ resolveMutation,
6
+ rejectMutation,
7
+ } from "../mutation-state.js";
8
+
9
+ export function useMutation(options = {}) {
10
+ const [state, setState] = useState({
11
+ data: null,
12
+ error: null,
13
+ isLoading: false,
14
+ isError: false,
15
+ isSuccess: false,
16
+ });
17
+
18
+ const optionsRef = useRef(options);
19
+
20
+ useEffect(() => {
21
+ optionsRef.current = options;
22
+ }, [options]);
23
+
24
+ const mutate = useCallback(async (url, fetchOptions = {}) => {
25
+ const mutationId = "mut-" + crypto.randomUUID();
26
+ trackMutation(mutationId, "pending");
27
+ setState({
28
+ data: null,
29
+ error: null,
30
+ isLoading: true,
31
+ isError: false,
32
+ isSuccess: false,
33
+ });
34
+
35
+ try {
36
+ const { response } = await fetchWithCache(url, fetchOptions);
37
+ const data = await response.json();
38
+ resolveMutation(mutationId, data);
39
+ setState({
40
+ data,
41
+ error: null,
42
+ isLoading: false,
43
+ isError: false,
44
+ isSuccess: true,
45
+ });
46
+ optionsRef.current.onSuccess?.(data);
47
+ optionsRef.current.onSettled?.();
48
+ return data;
49
+ } catch (err) {
50
+ const error = err instanceof Error ? err : new Error(String(err));
51
+ rejectMutation(mutationId, error);
52
+ setState({
53
+ data: null,
54
+ error,
55
+ isLoading: false,
56
+ isError: true,
57
+ isSuccess: false,
58
+ });
59
+ optionsRef.current.onError?.(error);
60
+ optionsRef.current.onSettled?.();
61
+ return null;
62
+ }
63
+ }, []);
64
+
65
+ const reset = useCallback(() => {
66
+ setState({
67
+ data: null,
68
+ error: null,
69
+ isLoading: false,
70
+ isError: false,
71
+ isSuccess: false,
72
+ });
73
+ }, []);
74
+
75
+ return { ...state, mutate, reset };
76
+ }
@@ -0,0 +1,97 @@
1
+ import { useEffect, useState, useCallback, useRef } from "react";
2
+ import { fetchWithCache } from "../fetch-wrapper.ts";
3
+ import {
4
+ trackMutation,
5
+ resolveMutation,
6
+ rejectMutation,
7
+ } from "../mutation-state.ts";
8
+ import type { FetchWithCacheOptions } from "../fetch-wrapper.ts";
9
+
10
+ export interface UseMutationOptions<TData> {
11
+ onSuccess?: (data: TData) => void;
12
+ onError?: (error: Error) => void;
13
+ onSettled?: () => void;
14
+ }
15
+
16
+ export function useMutation<TData = unknown>(
17
+ options: UseMutationOptions<TData> = {},
18
+ ) {
19
+ const [state, setState] = useState<{
20
+ data: TData | null;
21
+ error: Error | null;
22
+ isLoading: boolean;
23
+ isError: boolean;
24
+ isSuccess: boolean;
25
+ }>({
26
+ data: null,
27
+ error: null,
28
+ isLoading: false,
29
+ isError: false,
30
+ isSuccess: false,
31
+ });
32
+
33
+ const optionsRef = useRef(options);
34
+
35
+ useEffect(() => {
36
+ optionsRef.current = options;
37
+ }, [options]);
38
+
39
+ const mutate = useCallback(
40
+ async (
41
+ url: string,
42
+ fetchOptions: FetchWithCacheOptions = {},
43
+ ): Promise<TData | null> => {
44
+ const mutationId = "mut-" + crypto.randomUUID();
45
+ trackMutation(mutationId, "pending");
46
+ setState({
47
+ data: null,
48
+ error: null,
49
+ isLoading: true,
50
+ isError: false,
51
+ isSuccess: false,
52
+ });
53
+
54
+ try {
55
+ const { response } = await fetchWithCache<TData>(url, fetchOptions);
56
+ const data: TData = await response.json();
57
+ resolveMutation(mutationId, data);
58
+ setState({
59
+ data,
60
+ error: null,
61
+ isLoading: false,
62
+ isError: false,
63
+ isSuccess: true,
64
+ });
65
+ optionsRef.current.onSuccess?.(data);
66
+ optionsRef.current.onSettled?.();
67
+ return data;
68
+ } catch (err) {
69
+ const error = err instanceof Error ? err : new Error(String(err));
70
+ rejectMutation(mutationId, error);
71
+ setState({
72
+ data: null,
73
+ error,
74
+ isLoading: false,
75
+ isError: true,
76
+ isSuccess: false,
77
+ });
78
+ optionsRef.current.onError?.(error);
79
+ optionsRef.current.onSettled?.();
80
+ return null;
81
+ }
82
+ },
83
+ [],
84
+ );
85
+
86
+ const reset = useCallback(() => {
87
+ setState({
88
+ data: null,
89
+ error: null,
90
+ isLoading: false,
91
+ isError: false,
92
+ isSuccess: false,
93
+ });
94
+ }, []);
95
+
96
+ return { ...state, mutate, reset };
97
+ }
@@ -0,0 +1,42 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import { getPendingCount, getQueueItems } from "../mutation-queue.js";
3
+
4
+ export function useMutationQueue() {
5
+ const [state, setState] = useState({
6
+ pending: 0,
7
+ items: [],
8
+ lastSync: null,
9
+ });
10
+
11
+ const refresh = useCallback(async () => {
12
+ const [count, items] = await Promise.all([
13
+ getPendingCount(),
14
+ getQueueItems(),
15
+ ]);
16
+ setState((s) => ({ ...s, pending: count, items }));
17
+ }, []);
18
+
19
+ useEffect(() => {
20
+ refresh();
21
+
22
+ const onSync = (e) => {
23
+ refresh();
24
+ setState((s) => ({
25
+ ...s,
26
+ lastSync: { succeeded: e.detail.succeeded, failed: e.detail.failed },
27
+ }));
28
+ };
29
+ const onChange = () => {
30
+ refresh();
31
+ };
32
+
33
+ window.addEventListener("mutation-sync-complete", onSync);
34
+ window.addEventListener("mutation-queue-changed", onChange);
35
+ return () => {
36
+ window.removeEventListener("mutation-sync-complete", onSync);
37
+ window.removeEventListener("mutation-queue-changed", onChange);
38
+ };
39
+ }, [refresh]);
40
+
41
+ return state;
42
+ }
@@ -0,0 +1,49 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import { getPendingCount, getQueueItems } from "../mutation-queue.ts";
3
+ import type { MutationQueueItem } from "../swoff.d.ts";
4
+
5
+ export interface MutationQueueState {
6
+ pending: number;
7
+ items: MutationQueueItem[];
8
+ lastSync: { succeeded: number; failed: number } | null;
9
+ }
10
+
11
+ export function useMutationQueue() {
12
+ const [state, setState] = useState<MutationQueueState>({
13
+ pending: 0,
14
+ items: [],
15
+ lastSync: null,
16
+ });
17
+
18
+ const refresh = useCallback(async () => {
19
+ const [count, items] = await Promise.all([
20
+ getPendingCount(),
21
+ getQueueItems(),
22
+ ]);
23
+ setState((s) => ({ ...s, pending: count, items }));
24
+ }, []);
25
+
26
+ useEffect(() => {
27
+ refresh();
28
+
29
+ const onSync = (e: CustomEvent) => {
30
+ refresh();
31
+ setState((s) => ({
32
+ ...s,
33
+ lastSync: { succeeded: e.detail.succeeded, failed: e.detail.failed },
34
+ }));
35
+ };
36
+ const onChange = () => {
37
+ refresh();
38
+ };
39
+
40
+ window.addEventListener("mutation-sync-complete", onSync);
41
+ window.addEventListener("mutation-queue-changed", onChange);
42
+ return () => {
43
+ window.removeEventListener("mutation-sync-complete", onSync);
44
+ window.removeEventListener("mutation-queue-changed", onChange);
45
+ };
46
+ }, [refresh]);
47
+
48
+ return state;
49
+ }
@@ -0,0 +1,27 @@
1
+ import { useState, useEffect } from "react";
2
+ import { getMutationState, onMutationStateChange } from "../mutation-state.js";
3
+
4
+ export function useMutationState(id) {
5
+ const [state, setState] = useState(() =>
6
+ id ? getMutationState(id) ?? null : null,
7
+ );
8
+
9
+ useEffect(() => {
10
+ if (!id) {
11
+ setState(null);
12
+ return;
13
+ }
14
+
15
+ setState(getMutationState(id) ?? null);
16
+
17
+ const unsub = onMutationStateChange((updated) => {
18
+ if (updated.id === id) {
19
+ setState(updated);
20
+ }
21
+ });
22
+
23
+ return unsub;
24
+ }, [id]);
25
+
26
+ return state;
27
+ }
@@ -0,0 +1,41 @@
1
+ import { useState, useEffect } from "react";
2
+ import {
3
+ getMutationState,
4
+ onMutationStateChange,
5
+ } from "../mutation-state.ts";
6
+ import type { MutationState } from "../mutation-state.ts";
7
+
8
+ /**
9
+ * Hook that subscribes to a specific mutation's state changes.
10
+ * Useful for showing per-mutation loading spinners, error states, etc.
11
+ *
12
+ * Usage:
13
+ * const mutation = useMutationState(mutationId);
14
+ * if (mutation?.status === "error") { ... }
15
+ */
16
+ export function useMutationState(id: string | null): MutationState | null {
17
+ const [state, setState] = useState<MutationState | null>(() =>
18
+ id ? getMutationState(id) ?? null : null,
19
+ );
20
+
21
+ useEffect(() => {
22
+ if (!id) {
23
+ setState(null);
24
+ return;
25
+ }
26
+
27
+ // Initial state
28
+ setState(getMutationState(id) ?? null);
29
+
30
+ // Subscribe to changes
31
+ const unsub = onMutationStateChange((updated) => {
32
+ if (updated.id === id) {
33
+ setState(updated);
34
+ }
35
+ });
36
+
37
+ return unsub;
38
+ }, [id]);
39
+
40
+ return state;
41
+ }
@@ -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,10 @@
1
+ import { useCallback } from "react";
2
+ import { prefetchCache } from "../fetch-wrapper.js";
3
+
4
+ export function usePrefetch() {
5
+ const prefetch = useCallback((url, options) => {
6
+ prefetchCache(url, options);
7
+ }, []);
8
+
9
+ return prefetch;
10
+ }
@@ -0,0 +1,25 @@
1
+ import { useCallback } from "react";
2
+ import { prefetchCache } from "../fetch-wrapper.ts";
3
+ import type { FetchWithCacheOptions } from "../fetch-wrapper.ts";
4
+
5
+ /**
6
+ * Hook that returns a stable prefetch callback for warming the SW cache.
7
+ * Useful for route-level prefetching, link hover prefetching, etc.
8
+ *
9
+ * Usage:
10
+ * const prefetch = usePrefetch();
11
+ * <Link onMouseEnter={() => prefetch("/api/todos")} to="/todos" />
12
+ */
13
+ export function usePrefetch(): (
14
+ url: string,
15
+ options?: FetchWithCacheOptions,
16
+ ) => void {
17
+ const prefetch = useCallback(
18
+ (url: string, options?: FetchWithCacheOptions) => {
19
+ prefetchCache(url, options);
20
+ },
21
+ [],
22
+ );
23
+
24
+ return prefetch;
25
+ }
@@ -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
+ }