@djangocfg/ui-nextjs 2.1.227 → 2.1.229
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 +55 -0
- package/package.json +12 -6
- package/src/components/sidebar.tsx +3 -1
- package/src/pwa/@docs/README.md +92 -0
- package/src/pwa/@docs/research/ios-android-install-flows.md +576 -0
- package/src/pwa/README.md +235 -0
- package/src/pwa/components/A2HSHint.tsx +236 -0
- package/src/pwa/components/DesktopGuide.tsx +234 -0
- package/src/pwa/components/IOSGuide.tsx +29 -0
- package/src/pwa/components/IOSGuideDrawer.tsx +103 -0
- package/src/pwa/components/IOSGuideModal.tsx +103 -0
- package/src/pwa/components/PWAPageResumeManager.tsx +33 -0
- package/src/pwa/context/InstallContext.tsx +102 -0
- package/src/pwa/hooks/useInstallPrompt.ts +168 -0
- package/src/pwa/hooks/useIsPWA.ts +116 -0
- package/src/pwa/hooks/usePWAPageResume.ts +163 -0
- package/src/pwa/index.ts +80 -0
- package/src/pwa/types/components.ts +95 -0
- package/src/pwa/types/config.ts +29 -0
- package/src/pwa/types/index.ts +26 -0
- package/src/pwa/types/install.ts +38 -0
- package/src/pwa/types/platform.ts +29 -0
- package/src/pwa/utils/localStorage.ts +181 -0
- package/src/pwa/utils/logger.ts +149 -0
- package/src/pwa/utils/platform.ts +151 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook to resume last page when opening PWA
|
|
5
|
+
*
|
|
6
|
+
* Saves current pathname on navigation and restores it when PWA is launched.
|
|
7
|
+
* Uses TTL-enabled localStorage to auto-expire saved page after 24 hours.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* // In a component that wraps your app
|
|
12
|
+
* usePWAPageResume({ enabled: true });
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { usePathname, useRouter } from 'next/navigation';
|
|
17
|
+
import { useEffect, useRef } from 'react';
|
|
18
|
+
|
|
19
|
+
import { useIsPWA } from './useIsPWA';
|
|
20
|
+
|
|
21
|
+
const STORAGE_KEY = 'pwa_last_page';
|
|
22
|
+
const TTL_24_HOURS = 24 * 60 * 60 * 1000;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Default paths to exclude from saving
|
|
26
|
+
* These paths are typically transient or sensitive
|
|
27
|
+
*/
|
|
28
|
+
const DEFAULT_EXCLUDE_PATTERNS = [
|
|
29
|
+
'/auth',
|
|
30
|
+
'/login',
|
|
31
|
+
'/register',
|
|
32
|
+
'/error',
|
|
33
|
+
'/404',
|
|
34
|
+
'/500',
|
|
35
|
+
'/oauth',
|
|
36
|
+
'/callback',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Storage wrapper format with metadata (for TTL support)
|
|
41
|
+
*/
|
|
42
|
+
interface StorageWrapper {
|
|
43
|
+
_meta: {
|
|
44
|
+
createdAt: number;
|
|
45
|
+
ttl: number;
|
|
46
|
+
};
|
|
47
|
+
_value: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if path should be excluded from saving
|
|
52
|
+
*/
|
|
53
|
+
function isExcludedPath(pathname: string): boolean {
|
|
54
|
+
return DEFAULT_EXCLUDE_PATTERNS.some(pattern =>
|
|
55
|
+
pathname === pattern || pathname.startsWith(`${pattern}/`)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Read last page from localStorage with TTL check
|
|
61
|
+
*/
|
|
62
|
+
function readLastPage(): string | null {
|
|
63
|
+
if (typeof window === 'undefined') return null;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const item = localStorage.getItem(STORAGE_KEY);
|
|
67
|
+
if (!item) return null;
|
|
68
|
+
|
|
69
|
+
const parsed = JSON.parse(item) as StorageWrapper;
|
|
70
|
+
|
|
71
|
+
// Check if it's the wrapped format with _meta
|
|
72
|
+
if (parsed && typeof parsed === 'object' && '_meta' in parsed && '_value' in parsed) {
|
|
73
|
+
// Check TTL expiration
|
|
74
|
+
const age = Date.now() - parsed._meta.createdAt;
|
|
75
|
+
if (age > parsed._meta.ttl) {
|
|
76
|
+
// Expired! Clean up
|
|
77
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return parsed._value;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Old format (backwards compatible) - treat as string
|
|
84
|
+
return item;
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Save page to localStorage with TTL
|
|
92
|
+
*/
|
|
93
|
+
function saveLastPage(pathname: string): void {
|
|
94
|
+
if (typeof window === 'undefined') return;
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const wrapped: StorageWrapper = {
|
|
98
|
+
_meta: {
|
|
99
|
+
createdAt: Date.now(),
|
|
100
|
+
ttl: TTL_24_HOURS,
|
|
101
|
+
},
|
|
102
|
+
_value: pathname,
|
|
103
|
+
};
|
|
104
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(wrapped));
|
|
105
|
+
} catch {
|
|
106
|
+
// Ignore localStorage errors
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface UsePWAPageResumeOptions {
|
|
111
|
+
/**
|
|
112
|
+
* Enable page resume feature
|
|
113
|
+
* @default true
|
|
114
|
+
*/
|
|
115
|
+
enabled?: boolean;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface UsePWAPageResumeReturn {
|
|
119
|
+
/** Whether app is running as PWA */
|
|
120
|
+
isPWA: boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Hook to resume last page when opening PWA
|
|
125
|
+
*
|
|
126
|
+
* - Saves current pathname on every navigation (always, not just in PWA mode)
|
|
127
|
+
* - Restores last page on PWA launch (only once per session)
|
|
128
|
+
* - Auto-excludes auth, error, and callback pages
|
|
129
|
+
* - Uses TTL (24 hours) to auto-expire saved page
|
|
130
|
+
*
|
|
131
|
+
* @param options - Configuration options
|
|
132
|
+
* @returns Object with isPWA state
|
|
133
|
+
*/
|
|
134
|
+
export function usePWAPageResume(options: UsePWAPageResumeOptions = {}): UsePWAPageResumeReturn {
|
|
135
|
+
const { enabled = true } = options;
|
|
136
|
+
const pathname = usePathname();
|
|
137
|
+
const router = useRouter();
|
|
138
|
+
const isPWA = useIsPWA();
|
|
139
|
+
const hasResumed = useRef(false);
|
|
140
|
+
|
|
141
|
+
// Resume on PWA launch (only once)
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
if (!enabled || !isPWA || hasResumed.current) return;
|
|
144
|
+
|
|
145
|
+
hasResumed.current = true;
|
|
146
|
+
|
|
147
|
+
const lastPage = readLastPage();
|
|
148
|
+
if (lastPage && lastPage !== pathname && !isExcludedPath(lastPage)) {
|
|
149
|
+
router.replace(lastPage);
|
|
150
|
+
}
|
|
151
|
+
}, [isPWA, enabled]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
152
|
+
|
|
153
|
+
// Save current page on navigation
|
|
154
|
+
// Save ALWAYS (not just in PWA mode) because user might install PWA after browsing
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
if (!enabled) return;
|
|
157
|
+
if (isExcludedPath(pathname)) return;
|
|
158
|
+
|
|
159
|
+
saveLastPage(pathname);
|
|
160
|
+
}, [pathname, enabled]);
|
|
161
|
+
|
|
162
|
+
return { isPWA };
|
|
163
|
+
}
|
package/src/pwa/index.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PWA Install Snippet
|
|
3
|
+
*
|
|
4
|
+
* Simplified PWA installation for web apps
|
|
5
|
+
* Handles Add to Home Screen (A2HS) for iOS Safari and Android Chrome
|
|
6
|
+
*
|
|
7
|
+
* @example Basic usage
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { PwaProvider, A2HSHint } from '@/snippets/PWAInstall';
|
|
10
|
+
*
|
|
11
|
+
* export default function Layout({ children }) {
|
|
12
|
+
* return (
|
|
13
|
+
* <PwaProvider>
|
|
14
|
+
* {children}
|
|
15
|
+
* <A2HSHint resetAfterDays={3} />
|
|
16
|
+
* </PwaProvider>
|
|
17
|
+
* );
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// Main API
|
|
23
|
+
export { PwaProvider, useInstall } from './context/InstallContext';
|
|
24
|
+
export { A2HSHint } from './components/A2HSHint';
|
|
25
|
+
export { IOSGuide } from './components/IOSGuide';
|
|
26
|
+
export { DesktopGuide } from './components/DesktopGuide';
|
|
27
|
+
|
|
28
|
+
// Hooks
|
|
29
|
+
export { useIsPWA, clearIsPWACache, type UseIsPWAOptions } from './hooks/useIsPWA';
|
|
30
|
+
export { usePWAPageResume, type UsePWAPageResumeOptions, type UsePWAPageResumeReturn } from './hooks/usePWAPageResume';
|
|
31
|
+
|
|
32
|
+
// Page Resume Manager
|
|
33
|
+
export { PWAPageResumeManager } from './components/PWAPageResumeManager';
|
|
34
|
+
|
|
35
|
+
// Utilities
|
|
36
|
+
export {
|
|
37
|
+
isStandalone,
|
|
38
|
+
isStandaloneReliable,
|
|
39
|
+
isMobileDevice,
|
|
40
|
+
hasValidManifest,
|
|
41
|
+
getDisplayMode,
|
|
42
|
+
onDisplayModeChange,
|
|
43
|
+
} from './utils/platform';
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
pwaLogger,
|
|
47
|
+
enablePWADebug,
|
|
48
|
+
disablePWADebug,
|
|
49
|
+
isPWADebugEnabled,
|
|
50
|
+
} from './utils/logger';
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
STORAGE_KEYS,
|
|
54
|
+
markA2HSDismissed,
|
|
55
|
+
isA2HSDismissedRecently,
|
|
56
|
+
clearAllPWAInstallData,
|
|
57
|
+
} from './utils/localStorage';
|
|
58
|
+
|
|
59
|
+
// Types - Configuration
|
|
60
|
+
export type { PwaInstallConfig } from './types';
|
|
61
|
+
|
|
62
|
+
// Types - Platform
|
|
63
|
+
export type { PlatformInfo } from './types';
|
|
64
|
+
|
|
65
|
+
// Types - Install
|
|
66
|
+
export type {
|
|
67
|
+
InstallPromptState,
|
|
68
|
+
BeforeInstallPromptEvent,
|
|
69
|
+
InstallOutcome,
|
|
70
|
+
IOSGuideState,
|
|
71
|
+
} from './types';
|
|
72
|
+
|
|
73
|
+
// Types - Components
|
|
74
|
+
export type {
|
|
75
|
+
InstallContextType,
|
|
76
|
+
InstallManagerProps,
|
|
77
|
+
AndroidInstallButtonProps,
|
|
78
|
+
IOSGuideModalProps,
|
|
79
|
+
InstallStep,
|
|
80
|
+
} from './types';
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component Props Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { PlatformInfo } from './platform';
|
|
6
|
+
import type { InstallOutcome } from './install';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Install context type
|
|
10
|
+
*/
|
|
11
|
+
export interface InstallContextType {
|
|
12
|
+
// State
|
|
13
|
+
platform: PlatformInfo;
|
|
14
|
+
isInstalled: boolean;
|
|
15
|
+
canPrompt: boolean;
|
|
16
|
+
|
|
17
|
+
// iOS Guide
|
|
18
|
+
showIOSGuide: boolean;
|
|
19
|
+
setShowIOSGuide: (show: boolean) => void;
|
|
20
|
+
|
|
21
|
+
// Actions
|
|
22
|
+
promptInstall: () => Promise<InstallOutcome>;
|
|
23
|
+
dismissIOSGuide: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Install manager props
|
|
28
|
+
*/
|
|
29
|
+
export interface InstallManagerProps {
|
|
30
|
+
/**
|
|
31
|
+
* Delay before showing iOS guide (ms)
|
|
32
|
+
* @default 2000
|
|
33
|
+
*/
|
|
34
|
+
delayMs?: number;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Number of days before re-showing dismissed guide
|
|
38
|
+
* @default 7
|
|
39
|
+
*/
|
|
40
|
+
resetDays?: number;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Custom class name for install button
|
|
44
|
+
*/
|
|
45
|
+
buttonClassName?: string;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Custom button text
|
|
49
|
+
*/
|
|
50
|
+
buttonText?: string;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Force show install UI (ignores platform detection)
|
|
54
|
+
* Useful for testing on desktop in development
|
|
55
|
+
* @default false
|
|
56
|
+
*/
|
|
57
|
+
forceShow?: boolean;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Callback when install is successful
|
|
61
|
+
*/
|
|
62
|
+
onInstallSuccess?: () => void;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Callback when install is dismissed
|
|
66
|
+
*/
|
|
67
|
+
onInstallDismiss?: () => void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Android install button props
|
|
72
|
+
*/
|
|
73
|
+
export interface AndroidInstallButtonProps {
|
|
74
|
+
onInstall: () => Promise<InstallOutcome>;
|
|
75
|
+
className?: string;
|
|
76
|
+
text?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* iOS guide modal props
|
|
81
|
+
*/
|
|
82
|
+
export interface IOSGuideModalProps {
|
|
83
|
+
onDismiss: () => void;
|
|
84
|
+
open?: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Install step for iOS guide
|
|
89
|
+
*/
|
|
90
|
+
export interface InstallStep {
|
|
91
|
+
number: number;
|
|
92
|
+
title: string;
|
|
93
|
+
icon: React.ComponentType<{ className?: string }>;
|
|
94
|
+
description: string;
|
|
95
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PWA Install Configuration Types
|
|
3
|
+
*
|
|
4
|
+
* Configuration for Progressive Web App installation features
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface PwaInstallConfig {
|
|
8
|
+
/** Enable PWA installation features */
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
|
|
11
|
+
/** Show A2HS (Add to Home Screen) hint (enabled by default when enabled is true) */
|
|
12
|
+
showInstallHint?: boolean;
|
|
13
|
+
|
|
14
|
+
/** Number of days before re-showing dismissed hint (null = never show again) */
|
|
15
|
+
resetAfterDays?: number | null;
|
|
16
|
+
|
|
17
|
+
/** Delay before showing hint (ms) */
|
|
18
|
+
delayMs?: number;
|
|
19
|
+
|
|
20
|
+
/** App logo URL to display in hint */
|
|
21
|
+
logo?: string;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resume last page when opening PWA
|
|
25
|
+
* When enabled, saves current pathname on navigation and restores it on PWA launch
|
|
26
|
+
* @default false
|
|
27
|
+
*/
|
|
28
|
+
resumeLastPage?: boolean;
|
|
29
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PWA Install Types - Central Export
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Configuration
|
|
6
|
+
export type { PwaInstallConfig } from './config';
|
|
7
|
+
|
|
8
|
+
// Platform
|
|
9
|
+
export type { PlatformInfo } from './platform';
|
|
10
|
+
|
|
11
|
+
// Install
|
|
12
|
+
export type {
|
|
13
|
+
InstallPromptState,
|
|
14
|
+
BeforeInstallPromptEvent,
|
|
15
|
+
InstallOutcome,
|
|
16
|
+
IOSGuideState,
|
|
17
|
+
} from './install';
|
|
18
|
+
|
|
19
|
+
// Components
|
|
20
|
+
export type {
|
|
21
|
+
InstallContextType,
|
|
22
|
+
InstallManagerProps,
|
|
23
|
+
AndroidInstallButtonProps,
|
|
24
|
+
IOSGuideModalProps,
|
|
25
|
+
InstallStep,
|
|
26
|
+
} from './components';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PWA Installation Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Install prompt state
|
|
7
|
+
*/
|
|
8
|
+
export interface InstallPromptState {
|
|
9
|
+
isIOS: boolean;
|
|
10
|
+
isAndroid: boolean;
|
|
11
|
+
isSafari: boolean;
|
|
12
|
+
isChrome: boolean;
|
|
13
|
+
isInstalled: boolean;
|
|
14
|
+
canPrompt: boolean;
|
|
15
|
+
deferredPrompt: BeforeInstallPromptEvent | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* BeforeInstallPrompt event (Android Chrome)
|
|
20
|
+
*/
|
|
21
|
+
export interface BeforeInstallPromptEvent extends Event {
|
|
22
|
+
prompt: () => Promise<void>;
|
|
23
|
+
userChoice: Promise<{ outcome: 'accepted' | 'dismissed'; platform: string }>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Install outcome
|
|
28
|
+
*/
|
|
29
|
+
export type InstallOutcome = 'accepted' | 'dismissed' | null;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* iOS guide dismissal state
|
|
33
|
+
*/
|
|
34
|
+
export interface IOSGuideState {
|
|
35
|
+
dismissed: boolean;
|
|
36
|
+
dismissedAt: number | null;
|
|
37
|
+
shouldShow: boolean;
|
|
38
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform Detection Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Platform detection result
|
|
7
|
+
*/
|
|
8
|
+
export interface PlatformInfo {
|
|
9
|
+
// Operating System
|
|
10
|
+
isIOS: boolean;
|
|
11
|
+
isAndroid: boolean;
|
|
12
|
+
isDesktop: boolean;
|
|
13
|
+
|
|
14
|
+
// Browser
|
|
15
|
+
isSafari: boolean;
|
|
16
|
+
isChrome: boolean;
|
|
17
|
+
isEdge: boolean;
|
|
18
|
+
isFirefox: boolean;
|
|
19
|
+
|
|
20
|
+
// Installation State
|
|
21
|
+
isStandalone: boolean;
|
|
22
|
+
|
|
23
|
+
// Capability
|
|
24
|
+
canPrompt: boolean;
|
|
25
|
+
|
|
26
|
+
// Composite checks
|
|
27
|
+
shouldShowAndroidPrompt: boolean;
|
|
28
|
+
shouldShowIOSGuide: boolean;
|
|
29
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocalStorage utilities for PWA install state persistence
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { IOSGuideState } from '../types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Storage keys
|
|
9
|
+
*/
|
|
10
|
+
export const STORAGE_KEYS = {
|
|
11
|
+
IOS_GUIDE_DISMISSED: 'pwa_ios_guide_dismissed_at',
|
|
12
|
+
APP_INSTALLED: 'pwa_app_installed',
|
|
13
|
+
A2HS_DISMISSED: 'pwa_a2hs_dismissed_at',
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Check if iOS guide was dismissed recently
|
|
18
|
+
* @param resetDays Number of days before re-showing (default: 7)
|
|
19
|
+
*/
|
|
20
|
+
export function isDismissedRecently(resetDays: number = 7): boolean {
|
|
21
|
+
if (typeof window === 'undefined') return false;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const dismissed = localStorage.getItem(STORAGE_KEYS.IOS_GUIDE_DISMISSED);
|
|
25
|
+
if (!dismissed) return false;
|
|
26
|
+
|
|
27
|
+
const dismissedAt = parseInt(dismissed, 10);
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
const daysSinceDismissed = (now - dismissedAt) / (1000 * 60 * 60 * 24);
|
|
30
|
+
|
|
31
|
+
return daysSinceDismissed < resetDays;
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Mark iOS guide as dismissed
|
|
39
|
+
*/
|
|
40
|
+
export function markIOSGuideDismissed(): void {
|
|
41
|
+
if (typeof window === 'undefined') return;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
localStorage.setItem(STORAGE_KEYS.IOS_GUIDE_DISMISSED, Date.now().toString());
|
|
45
|
+
} catch {
|
|
46
|
+
// Fail silently if localStorage is not available
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Clear iOS guide dismissal
|
|
52
|
+
*/
|
|
53
|
+
export function clearIOSGuideDismissal(): void {
|
|
54
|
+
if (typeof window === 'undefined') return;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
localStorage.removeItem(STORAGE_KEYS.IOS_GUIDE_DISMISSED);
|
|
58
|
+
} catch {
|
|
59
|
+
// Fail silently
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get iOS guide state
|
|
65
|
+
*/
|
|
66
|
+
export function getIOSGuideState(resetDays: number = 7): IOSGuideState {
|
|
67
|
+
if (typeof window === 'undefined') {
|
|
68
|
+
return {
|
|
69
|
+
dismissed: false,
|
|
70
|
+
dismissedAt: null,
|
|
71
|
+
shouldShow: false,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const dismissed = localStorage.getItem(STORAGE_KEYS.IOS_GUIDE_DISMISSED);
|
|
77
|
+
if (!dismissed) {
|
|
78
|
+
return {
|
|
79
|
+
dismissed: false,
|
|
80
|
+
dismissedAt: null,
|
|
81
|
+
shouldShow: true,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const dismissedAt = parseInt(dismissed, 10);
|
|
86
|
+
const now = Date.now();
|
|
87
|
+
const daysSinceDismissed = (now - dismissedAt) / (1000 * 60 * 60 * 24);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
dismissed: true,
|
|
91
|
+
dismissedAt,
|
|
92
|
+
shouldShow: daysSinceDismissed >= resetDays,
|
|
93
|
+
};
|
|
94
|
+
} catch {
|
|
95
|
+
return {
|
|
96
|
+
dismissed: false,
|
|
97
|
+
dismissedAt: null,
|
|
98
|
+
shouldShow: false,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Mark app as installed
|
|
105
|
+
*/
|
|
106
|
+
export function markAppInstalled(): void {
|
|
107
|
+
if (typeof window === 'undefined') return;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
localStorage.setItem(STORAGE_KEYS.APP_INSTALLED, 'true');
|
|
111
|
+
// Clear dismissal state when app is installed
|
|
112
|
+
clearIOSGuideDismissal();
|
|
113
|
+
} catch {
|
|
114
|
+
// Fail silently
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check if app is marked as installed
|
|
120
|
+
*/
|
|
121
|
+
export function isAppInstalled(): boolean {
|
|
122
|
+
if (typeof window === 'undefined') return false;
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
return localStorage.getItem(STORAGE_KEYS.APP_INSTALLED) === 'true';
|
|
126
|
+
} catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Check if A2HS hint was dismissed recently
|
|
133
|
+
* @param resetDays Number of days before re-showing (default: 3)
|
|
134
|
+
*/
|
|
135
|
+
export function isA2HSDismissedRecently(resetDays: number = 3): boolean {
|
|
136
|
+
return isDismissedRecentlyHelper(resetDays, STORAGE_KEYS.A2HS_DISMISSED);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Mark A2HS hint as dismissed
|
|
141
|
+
*/
|
|
142
|
+
export function markA2HSDismissed(): void {
|
|
143
|
+
if (typeof window === 'undefined') return;
|
|
144
|
+
try {
|
|
145
|
+
localStorage.setItem(STORAGE_KEYS.A2HS_DISMISSED, Date.now().toString());
|
|
146
|
+
} catch {
|
|
147
|
+
// Fail silently
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Helper: Check if a key was dismissed recently
|
|
153
|
+
* @internal
|
|
154
|
+
*/
|
|
155
|
+
function isDismissedRecentlyHelper(resetDays: number, key: string): boolean {
|
|
156
|
+
if (typeof window === 'undefined') return false;
|
|
157
|
+
try {
|
|
158
|
+
const dismissed = localStorage.getItem(key);
|
|
159
|
+
if (!dismissed) return false;
|
|
160
|
+
const dismissedAt = parseInt(dismissed, 10);
|
|
161
|
+
const daysSince = (Date.now() - dismissedAt) / (1000 * 60 * 60 * 24);
|
|
162
|
+
return daysSince < resetDays;
|
|
163
|
+
} catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Clear all PWA install data
|
|
170
|
+
*/
|
|
171
|
+
export function clearAllPWAInstallData(): void {
|
|
172
|
+
if (typeof window === 'undefined') return;
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
localStorage.removeItem(STORAGE_KEYS.IOS_GUIDE_DISMISSED);
|
|
176
|
+
localStorage.removeItem(STORAGE_KEYS.APP_INSTALLED);
|
|
177
|
+
localStorage.removeItem(STORAGE_KEYS.A2HS_DISMISSED);
|
|
178
|
+
} catch {
|
|
179
|
+
// Fail silently
|
|
180
|
+
}
|
|
181
|
+
}
|