All files / src/components/shared/PushPermission index.tsx

0% Statements 0/189
100% Branches 0/0
0% Functions 0/1
0% Lines 0/189

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190                                                                                                                                                                                                                                                                                                                                                                                           
'use client';

import { useState, useEffect, useCallback } from 'react';
import { cn } from '@/lib/core/utils';
import { clientLogger } from '@/lib/logging/clientLogger';
import {
  isPushSupported,
  getNotificationPermission,
  requestNotificationPermission,
  subscribeToPush,
} from '@/lib/pwa';
import { Button } from '@/components/ui/Button';
import { Icon } from '@/components/ui/icons';

interface PushPermissionProps {
  /** Delay before showing the prompt (ms) */
  showDelay?: number;
  /** Additional CSS classes */
  className?: string;
  /** Callback when permission is granted */
  onPermissionGranted?: () => void;
  /** Callback when prompt is dismissed */
  onDismiss?: () => void;
}

// Storage key for tracking dismissal
const DISMISS_KEY = 'push-prompt-dismissed';

/**
 * PushPermission Component
 *
 * Prompts users to enable push notifications.
 * Shows contextual benefits to encourage opt-in.
 */
export function PushPermission({
  showDelay = 30000, // 30 seconds by default
  className,
  onPermissionGranted,
  onDismiss,
}: PushPermissionProps) {
  const [showPrompt, setShowPrompt] = useState(false);
  const [permission, setPermission] = useState<NotificationPermission | null>(
    null
  );
  const [isRequesting, setIsRequesting] = useState(false);

  // Check if prompt was dismissed this session
  const wasDismissedThisSession = useCallback((): boolean => {
    if (typeof sessionStorage === 'undefined') return false;
    return sessionStorage.getItem(DISMISS_KEY) === 'true';
  }, []);

  useEffect(() => {
    // Check if notifications are supported
    if (!isPushSupported()) return;

    // Get current permission
    const currentPermission = getNotificationPermission();
    setPermission(currentPermission);

    // Only show prompt if permission is default (not yet asked)
    // and not dismissed this session
    if (currentPermission === 'default' && !wasDismissedThisSession()) {
      const timer = setTimeout(() => setShowPrompt(true), showDelay);
      return () => clearTimeout(timer);
    }
  }, [showDelay, wasDismissedThisSession]);

  // Handle enabling notifications
  const handleEnable = async () => {
    setIsRequesting(true);

    try {
      const result = await requestNotificationPermission();
      setPermission(result);

      if (result === 'granted') {
        await subscribeToPush();
        setShowPrompt(false);
        onPermissionGranted?.();
      } else {
        // Permission was denied, hide the prompt
        setShowPrompt(false);
      }
    } catch (error) {
      clientLogger.error('Failed to enable notifications', error instanceof Error ? error : new Error(String(error)));
    } finally {
      setIsRequesting(false);
    }
  };

  // Handle dismiss
  const handleDismiss = () => {
    setShowPrompt(false);
    sessionStorage.setItem(DISMISS_KEY, 'true');
    onDismiss?.();
  };

  // Don't render if not supported, already decided, or nothing to show
  if (!showPrompt || permission !== 'default') return null;

  return (
    <div
      className={cn(
        'fixed bottom-4 right-4 max-w-sm',
        'bg-white dark:bg-gray-800 rounded-xl shadow-xl',
        'border border-gray-200 dark:border-gray-700',
        'p-5 z-50',
        className
      )}
      role="dialog"
      aria-labelledby="push-permission-title"
      aria-describedby="push-permission-description"
    >
      <button
        onClick={handleDismiss}
        className="absolute top-3 right-3 p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
        aria-label="Close"
      >
        <Icon name="x" className="h-5 w-5" />
      </button>

      <div className="flex items-start gap-4">
        <div className="flex-shrink-0 p-3 bg-gradient-to-br from-amber-400 to-orange-500 rounded-xl">
          <Icon name="bell" className="h-6 w-6 text-white" />
        </div>

        <div className="flex-1 min-w-0">
          <h3
            id="push-permission-title"
            className="font-semibold text-gray-900 dark:text-white"
          >
            Stay Updated
          </h3>
          <p
            id="push-permission-description"
            className="text-sm text-gray-600 dark:text-gray-400 mt-1"
          >
            Get notified about order updates, deals, and back-in-stock items.
          </p>
        </div>
      </div>

      {/* Benefits list */}
      <ul className="mt-4 space-y-2">
        <li className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
          <Icon
            name="check-circle"
            className="h-4 w-4 text-green-500 flex-shrink-0"
          />
          <span>Real-time order tracking</span>
        </li>
        <li className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
          <Icon
            name="check-circle"
            className="h-4 w-4 text-green-500 flex-shrink-0"
          />
          <span>Exclusive sale alerts</span>
        </li>
        <li className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
          <Icon
            name="check-circle"
            className="h-4 w-4 text-green-500 flex-shrink-0"
          />
          <span>Price drop notifications</span>
        </li>
      </ul>

      <div className="flex gap-3 mt-5">
        <Button
          onClick={handleEnable}
          disabled={isRequesting}
          className="flex-1"
        >
          {isRequesting ? 'Enabling...' : 'Enable'}
        </Button>
        <Button variant="ghost" onClick={handleDismiss}>
          Not now
        </Button>
      </div>

      <p className="text-xs text-gray-500 dark:text-gray-500 mt-3 text-center">
        You can change this anytime in settings
      </p>
    </div>
  );
}

export default PushPermission;