diff --git a/apps/pwa/src/components/InstructionSheet.tsx b/apps/pwa/src/components/InstructionSheet.tsx
new file mode 100644
index 0000000..ba5d8d5
--- /dev/null
+++ b/apps/pwa/src/components/InstructionSheet.tsx
@@ -0,0 +1,189 @@
+/**
+ * InstructionSheet — shared OS-specific notification re-enable guidance bottom sheet.
+ *
+ * Extracted from PermissionDeniedBanner (Phase 5 UAT-05-T4 fix).
+ * Used by both PermissionDeniedBanner and SettingsSheet.
+ *
+ * Security: T-05-24 — all copy is plain-text JSX children, no dangerouslySetInnerHTML.
+ */
+
+import { X } from 'lucide-react'
+
+// ── OS detection ──────────────────────────────────────────────────────────
+
+function isIOS(): boolean {
+ return /iPad|iPhone|iPod/.test(navigator.userAgent) &&
+ !(window as unknown as { MSStream?: unknown }).MSStream
+}
+
+// ── Instruction steps ────────────────────────────────────────────────────
+
+const IOS_STEPS = [
+ 'Open Settings on your iPhone',
+ 'Scroll down and tap Safari',
+ 'Tap Notifications',
+ 'Allow notifications for FamilySync',
+]
+
+const ANDROID_STEPS = [
+ 'Open Chrome on your phone',
+ 'Tap the three-dot menu → Settings',
+ 'Tap Site Settings → Notifications',
+ 'Find FamilySync and tap Allow',
+]
+
+// ── Instruction sheet ────────────────────────────────────────────────────
+
+interface InstructionSheetProps {
+ onClose: () => void
+}
+
+export function InstructionSheet({ onClose }: InstructionSheetProps) {
+ const steps = isIOS() ? IOS_STEPS : ANDROID_STEPS
+ const platform = isIOS() ? 'iOS' : 'Android'
+
+ return (
+
{
+ if (e.target === e.currentTarget) onClose()
+ }}
+ >
+
+ {/* Header */}
+
+
+ How to enable notifications
+
+
+
+
+ {/* Steps */}
+
+ {steps.map((step, i) => (
+ -
+
+ {i + 1}
+
+
+ {step}
+
+
+ ))}
+
+
+ {/* Done button */}
+
+
+
+ )
+}
diff --git a/apps/pwa/src/components/PermissionDeniedBanner.tsx b/apps/pwa/src/components/PermissionDeniedBanner.tsx
index c51e500..a0ee1dd 100644
--- a/apps/pwa/src/components/PermissionDeniedBanner.tsx
+++ b/apps/pwa/src/components/PermissionDeniedBanner.tsx
@@ -19,187 +19,9 @@
*/
import { useState } from 'react'
-import { AlertCircle, X } from 'lucide-react'
+import { AlertCircle } from 'lucide-react'
import { readNotificationsEnabled } from '../hooks/usePushSubscription.js'
-
-// ── OS detection ──────────────────────────────────────────────────────────
-
-function isIOS(): boolean {
- return /iPad|iPhone|iPod/.test(navigator.userAgent) &&
- !(window as unknown as { MSStream?: unknown }).MSStream
-}
-
-// ── Instruction steps ────────────────────────────────────────────────────
-
-const IOS_STEPS = [
- 'Open Settings on your iPhone',
- 'Scroll down and tap Safari',
- 'Tap Notifications',
- 'Allow notifications for FamilySync',
-]
-
-const ANDROID_STEPS = [
- 'Open Chrome on your phone',
- 'Tap the three-dot menu → Settings',
- 'Tap Site Settings → Notifications',
- 'Find FamilySync and tap Allow',
-]
-
-// ── Instruction sheet ────────────────────────────────────────────────────
-
-interface InstructionSheetProps {
- onClose: () => void
-}
-
-function InstructionSheet({ onClose }: InstructionSheetProps) {
- const steps = isIOS() ? IOS_STEPS : ANDROID_STEPS
- const platform = isIOS() ? 'iOS' : 'Android'
-
- return (
- {
- if (e.target === e.currentTarget) onClose()
- }}
- >
-
- {/* Header */}
-
-
- How to enable notifications
-
-
-
-
- {/* Steps */}
-
- {steps.map((step, i) => (
- -
-
- {i + 1}
-
-
- {step}
-
-
- ))}
-
-
- {/* Done button */}
-
-
-
- )
-}
+import { InstructionSheet } from './InstructionSheet.js'
// ── PermissionDeniedBanner ────────────────────────────────────────────────