/** * Stale Data UX Snippets * * Platform-specific implementations for showing stale data banners * when polling hasn't succeeded for extended periods */ // Common configuration const STALE_DATA_CONFIG = { staleThresholdHours: 4, criticalThresholdHours: 24, bannerAutoDismissMs: 10000 }; // Common i18n keys const I18N_KEYS = { 'staleness.banner.title': 'Data may be outdated', 'staleness.banner.message': 'Last updated {hours} hours ago. Tap to refresh.', 'staleness.banner.critical': 'Data is very outdated. Please refresh.', 'staleness.banner.action_refresh': 'Refresh Now', 'staleness.banner.action_settings': 'Settings', 'staleness.banner.dismiss': 'Dismiss' }; /** * Android Implementation */ class AndroidStaleDataUX { private context: Record; // Android Context private notificationManager: Record; // NotificationManager constructor(context: Record) { this.context = context; this.notificationManager = context.getSystemService('notification'); } showStalenessBanner(hoursSinceUpdate: number): void { const isCritical = hoursSinceUpdate >= STALE_DATA_CONFIG.criticalThresholdHours; const title = this.context.getString(I18N_KEYS['staleness.banner.title']); const message = isCritical ? this.context.getString(I18N_KEYS['staleness.banner.critical']) : this.context.getString(I18N_KEYS['staleness.banner.message'], hoursSinceUpdate); // Create notification const notification = { smallIcon: 'ic_warning', contentTitle: title, contentText: message, priority: isCritical ? 'high' : 'normal', autoCancel: true, actions: [ { title: this.context.getString(I18N_KEYS['staleness.banner.action_refresh']), intent: this.createRefreshIntent() }, { title: this.context.getString(I18N_KEYS['staleness.banner.action_settings']), intent: this.createSettingsIntent() } ] }; this.notificationManager.notify('stale_data_warning', notification); } private createRefreshIntent(): Record { // Create PendingIntent for refresh action return { action: 'com.timesafari.dailynotification.REFRESH_DATA', flags: ['FLAG_UPDATE_CURRENT'] }; } private createSettingsIntent(): Record { // Create PendingIntent for settings action return { action: 'com.timesafari.dailynotification.OPEN_SETTINGS', flags: ['FLAG_UPDATE_CURRENT'] }; } showInAppBanner(hoursSinceUpdate: number): void { // Show banner in app UI (Snackbar or similar) const message = this.context.getString( I18N_KEYS['staleness.banner.message'], hoursSinceUpdate ); // Create Snackbar const _snackbar = { _message: message, _duration: 'LENGTH_INDEFINITE', action: { text: this.context.getString(I18N_KEYS['staleness.banner.action_refresh']), callback: () => this.refreshData() } }; // Show snackbar // Showing Android in-app banner (example implementation) } private refreshData(): void { // Trigger manual refresh // Refreshing data on Android (example implementation) } } /** * iOS Implementation */ class iOSStaleDataUX { private viewController: Record; // UIViewController constructor(viewController: Record) { this.viewController = viewController; } showStalenessBanner(hoursSinceUpdate: number): void { const isCritical = hoursSinceUpdate >= STALE_DATA_CONFIG.criticalThresholdHours; const title = NSLocalizedString(I18N_KEYS['staleness.banner.title'], ''); const message = isCritical ? NSLocalizedString(I18N_KEYS['staleness.banner.critical'], '') : NSLocalizedString(I18N_KEYS['staleness.banner.message'], '').replace('{hours}', hoursSinceUpdate.toString()); // Create alert controller const alert = { title, message, preferredStyle: 'alert', actions: [ { title: NSLocalizedString(I18N_KEYS['staleness.banner.action_refresh'], ''), style: 'default', handler: () => this.refreshData() }, { title: NSLocalizedString(I18N_KEYS['staleness.banner.action_settings'], ''), style: 'default', handler: () => this.openSettings() }, { title: NSLocalizedString(I18N_KEYS['staleness.banner.dismiss'], ''), style: 'cancel' } ] }; this.viewController.present(alert, { animated: true }); } showBannerView(hoursSinceUpdate: number): void { // Create banner view const _banner = { title: NSLocalizedString(I18N_KEYS['staleness.banner.title'], ''), message: NSLocalizedString(I18N_KEYS['staleness.banner.message'], '').replace('{hours}', hoursSinceUpdate.toString()), backgroundColor: 'systemYellow', textColor: 'label', actions: [ { title: NSLocalizedString(I18N_KEYS['staleness.banner.action_refresh'], ''), action: () => this.refreshData() } ] }; // Show banner // Showing iOS banner view (example implementation) } private refreshData(): void { // Refreshing data on iOS (example implementation) } private openSettings(): void { // Opening settings on iOS (example implementation) } } /** * Web Implementation */ class WebStaleDataUX { private container: HTMLElement; constructor(container: HTMLElement = document.body) { this.container = container; } showStalenessBanner(hoursSinceUpdate: number): void { const isCritical = hoursSinceUpdate >= STALE_DATA_CONFIG.criticalThresholdHours; const banner = document.createElement('div'); banner.className = `staleness-banner ${isCritical ? 'critical' : 'warning'}`; banner.innerHTML = ` `; // Add styles banner.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; background: ${isCritical ? '#ff6b6b' : '#ffd93d'}; color: ${isCritical ? 'white' : 'black'}; padding: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); z-index: 1000; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; `; this.container.appendChild(banner); // Auto-dismiss after timeout setTimeout(() => { if (banner.parentElement) { banner.remove(); } }, STALE_DATA_CONFIG.bannerAutoDismissMs); } showToast(hoursSinceUpdate: number): void { const toast = document.createElement('div'); toast.className = 'staleness-toast'; toast.innerHTML = `
⚠️ ${I18N_KEYS['staleness.banner.message'].replace('{hours}', hoursSinceUpdate.toString())}
`; // Add styles toast.style.cssText = ` position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: #333; color: white; padding: 12px 16px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); z-index: 1000; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; display: flex; align-items: center; gap: 12px; `; this.container.appendChild(toast); // Auto-dismiss setTimeout(() => { if (toast.parentElement) { toast.remove(); } }, STALE_DATA_CONFIG.bannerAutoDismissMs); } } /** * Stale Data Manager */ class StaleDataManager { private platform: 'android' | 'ios' | 'web'; private ux: AndroidStaleDataUX | iOSStaleDataUX | WebStaleDataUX; private lastSuccessfulPoll = 0; constructor(platform: 'android' | 'ios' | 'web', context?: Record) { this.platform = platform; switch (platform) { case 'android': this.ux = new AndroidStaleDataUX(context); break; case 'ios': this.ux = new iOSStaleDataUX(context); break; case 'web': this.ux = new WebStaleDataUX(context); break; } } updateLastSuccessfulPoll(): void { this.lastSuccessfulPoll = Date.now(); } checkAndShowStaleDataBanner(): void { const hoursSinceUpdate = (Date.now() - this.lastSuccessfulPoll) / (1000 * 60 * 60); if (hoursSinceUpdate >= STALE_DATA_CONFIG.staleThresholdHours) { this.ux.showStalenessBanner(Math.floor(hoursSinceUpdate)); } } isDataStale(): boolean { const hoursSinceUpdate = (Date.now() - this.lastSuccessfulPoll) / (1000 * 60 * 60); return hoursSinceUpdate >= STALE_DATA_CONFIG.staleThresholdHours; } isDataCritical(): boolean { const hoursSinceUpdate = (Date.now() - this.lastSuccessfulPoll) / (1000 * 60 * 60); return hoursSinceUpdate >= STALE_DATA_CONFIG.criticalThresholdHours; } getHoursSinceUpdate(): number { return (Date.now() - this.lastSuccessfulPoll) / (1000 * 60 * 60); } } // Global functions for web if (typeof window !== 'undefined') { (window as Record).refreshData = (): void => { // Refreshing data from web banner (example implementation) }; (window as Record).openSettings = (): void => { // Opening settings from web banner (example implementation) }; } export { StaleDataManager, AndroidStaleDataUX, iOSStaleDataUX, WebStaleDataUX, STALE_DATA_CONFIG, I18N_KEYS };