refactor(quick-260610-jlp-01): extract InstructionSheet into shared component
- Move isIOS, IOS_STEPS, ANDROID_STEPS, InstructionSheetProps, InstructionSheet from PermissionDeniedBanner verbatim - Export InstructionSheet as named export from InstructionSheet.tsx - PermissionDeniedBanner now imports from ./InstructionSheet.js (behaviour identical)
This commit is contained in:
@@ -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 (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={`Re-enable notifications on ${platform}`}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
background: 'var(--color-overlay, rgba(0,0,0,0.32))',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-end',
|
||||
zIndex: 1000,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) onClose()
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: 'var(--color-surface, #ffffff)',
|
||||
borderRadius: '12px 12px 0 0',
|
||||
padding: 'var(--space-6, 24px)',
|
||||
maxHeight: '90dvh',
|
||||
overflowY: 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 'var(--space-4, 16px)',
|
||||
fontFamily: 'var(--font-family-base, system-ui, sans-serif)',
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<h2
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: 'var(--text-heading-size, 18px)',
|
||||
fontWeight: 600,
|
||||
lineHeight: 'var(--text-heading-line-height, 1.25)',
|
||||
color: 'var(--color-text-primary, #111318)',
|
||||
}}
|
||||
>
|
||||
How to enable notifications
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
aria-label="Close instructions"
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
minWidth: '44px',
|
||||
minHeight: '44px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'var(--color-text-secondary, #5c6472)',
|
||||
borderRadius: 'var(--space-1, 4px)',
|
||||
}}
|
||||
>
|
||||
<X size={20} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Steps */}
|
||||
<ol
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
listStyle: 'none',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 'var(--space-3, 12px)',
|
||||
}}
|
||||
>
|
||||
{steps.map((step, i) => (
|
||||
<li
|
||||
key={i}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: 'var(--space-3, 12px)',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
width: '28px',
|
||||
height: '28px',
|
||||
borderRadius: '50%',
|
||||
background: 'var(--color-destructive, #DC2626)',
|
||||
color: '#ffffff',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '13px',
|
||||
fontWeight: 600,
|
||||
fontFamily: 'var(--font-family-base, system-ui, sans-serif)',
|
||||
}}
|
||||
>
|
||||
{i + 1}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 'var(--text-body-size, 15px)',
|
||||
lineHeight: 'var(--text-body-line-height, 1.5)',
|
||||
color: 'var(--color-text-primary, #111318)',
|
||||
paddingTop: '4px',
|
||||
}}
|
||||
>
|
||||
{step}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
{/* Done button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: 'var(--color-text-primary, #111318)',
|
||||
color: '#ffffff',
|
||||
border: 'none',
|
||||
borderRadius: 'var(--space-1, 4px)',
|
||||
minHeight: '44px',
|
||||
padding: '0 var(--space-4, 16px)',
|
||||
fontSize: 'var(--text-label-size, 13px)',
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
fontFamily: 'var(--font-family-base, system-ui, sans-serif)',
|
||||
alignSelf: 'stretch',
|
||||
}}
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={`Re-enable notifications on ${platform}`}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
background: 'var(--color-overlay, rgba(0,0,0,0.32))',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-end',
|
||||
zIndex: 1000,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) onClose()
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: 'var(--color-surface, #ffffff)',
|
||||
borderRadius: '12px 12px 0 0',
|
||||
padding: 'var(--space-6, 24px)',
|
||||
maxHeight: '90dvh',
|
||||
overflowY: 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 'var(--space-4, 16px)',
|
||||
fontFamily: 'var(--font-family-base, system-ui, sans-serif)',
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<h2
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: 'var(--text-heading-size, 18px)',
|
||||
fontWeight: 600,
|
||||
lineHeight: 'var(--text-heading-line-height, 1.25)',
|
||||
color: 'var(--color-text-primary, #111318)',
|
||||
}}
|
||||
>
|
||||
How to enable notifications
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
aria-label="Close instructions"
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
minWidth: '44px',
|
||||
minHeight: '44px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'var(--color-text-secondary, #5c6472)',
|
||||
borderRadius: 'var(--space-1, 4px)',
|
||||
}}
|
||||
>
|
||||
<X size={20} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Steps */}
|
||||
<ol
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
listStyle: 'none',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 'var(--space-3, 12px)',
|
||||
}}
|
||||
>
|
||||
{steps.map((step, i) => (
|
||||
<li
|
||||
key={i}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: 'var(--space-3, 12px)',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
width: '28px',
|
||||
height: '28px',
|
||||
borderRadius: '50%',
|
||||
background: 'var(--color-destructive, #DC2626)',
|
||||
color: '#ffffff',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '13px',
|
||||
fontWeight: 600,
|
||||
fontFamily: 'var(--font-family-base, system-ui, sans-serif)',
|
||||
}}
|
||||
>
|
||||
{i + 1}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 'var(--text-body-size, 15px)',
|
||||
lineHeight: 'var(--text-body-line-height, 1.5)',
|
||||
color: 'var(--color-text-primary, #111318)',
|
||||
paddingTop: '4px',
|
||||
}}
|
||||
>
|
||||
{step}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
{/* Done button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: 'var(--color-text-primary, #111318)',
|
||||
color: '#ffffff',
|
||||
border: 'none',
|
||||
borderRadius: 'var(--space-1, 4px)',
|
||||
minHeight: '44px',
|
||||
padding: '0 var(--space-4, 16px)',
|
||||
fontSize: 'var(--text-label-size, 13px)',
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
fontFamily: 'var(--font-family-base, system-ui, sans-serif)',
|
||||
alignSelf: 'stretch',
|
||||
}}
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
import { InstructionSheet } from './InstructionSheet.js'
|
||||
|
||||
// ── PermissionDeniedBanner ────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user