|
| 1 | +import { useState } from 'react'; |
| 2 | + |
| 3 | +import { c } from 'ttag'; |
| 4 | + |
| 5 | +import { Button } from '@proton/atoms/Button/Button'; |
| 6 | +import { useTheme } from '@proton/components'; |
| 7 | +import { IcCross } from '@proton/icons/icons/IcCross'; |
| 8 | +import { getAppHref } from '@proton/shared/lib/apps/helper'; |
| 9 | +import { APPS, LUMO_SHORT_APP_NAME } from '@proton/shared/lib/constants'; |
| 10 | +import lumoCatIcon from '@proton/styles/assets/img/lumo/lumo-cat-icon.svg'; |
| 11 | +import { useFlag } from '@proton/unleash/useFlag'; |
| 12 | +import clsx from '@proton/utils/clsx'; |
| 13 | + |
| 14 | +import './TroubleshootWithLumo.scss'; |
| 15 | + |
| 16 | +// Opens the minimal Lumo "agent" surface pre-loaded with the account-protection agent. Using |
| 17 | +// getAppHref keeps the host correct per environment (e.g. lumo.proton.me vs lumo.proton.dev). |
| 18 | +// `theme` makes the embedded Lumo surface match this page's light/dark appearance. |
| 19 | +const buildLumoSrc = (theme: 'light' | 'dark') => |
| 20 | + getAppHref(`/agent?skill=proton-account-recovery&theme=${theme}`, APPS.PROTONLUMO); |
| 21 | + |
| 22 | +// Bottom-right entry point on the sign-in page. Opens the account-protection Lumo agent in a |
| 23 | +// floating, non-blocking panel. Unlike a modal, the panel lets users keep following recovery |
| 24 | +// steps on the page while the chat stays open, and the iframe is kept mounted across close/reopen |
| 25 | +// so the conversation is preserved (we only toggle visibility instead of unmounting). |
| 26 | +const TroubleshootWithLumo = () => { |
| 27 | + const [isOpen, setIsOpen] = useState(false); |
| 28 | + // Resolved once on first open: this lazy-loads the iframe AND freezes its `src`, so a later |
| 29 | + // theme toggle on the host page can't reload it and wipe the conversation. |
| 30 | + const [src, setSrc] = useState<string | undefined>(undefined); |
| 31 | + const theme = useTheme(); |
| 32 | + const lumoSignInHelperEnabled = useFlag('LumoSignInHelp'); |
| 33 | + |
| 34 | + const title = c('Title').t`Troubleshoot with ${LUMO_SHORT_APP_NAME}`; |
| 35 | + |
| 36 | + const open = () => { |
| 37 | + if (!src) { |
| 38 | + setSrc(buildLumoSrc(theme.information.dark ? 'dark' : 'light')); |
| 39 | + } |
| 40 | + setIsOpen(true); |
| 41 | + }; |
| 42 | + |
| 43 | + // Gated behind the rollout flag: when off, no Lumo entry point appears on the sign-in page. |
| 44 | + if (!lumoSignInHelperEnabled) { |
| 45 | + return null; |
| 46 | + } |
| 47 | + |
| 48 | + return ( |
| 49 | + <div className="relative inline-block text-left"> |
| 50 | + {src && ( |
| 51 | + <div |
| 52 | + role="dialog" |
| 53 | + aria-label={title} |
| 54 | + aria-hidden={!isOpen} |
| 55 | + className={clsx( |
| 56 | + 'lumo-troubleshoot-panel absolute z-50 bg-norm border border-weak rounded-xl sm:shadow-lifted shadow-color-primary overflow-hidden flex flex-column flex-nowrap', |
| 57 | + isOpen && 'is-open' |
| 58 | + )} |
| 59 | + > |
| 60 | + <div className="flex flex-row flex-nowrap items-center gap-2 px-3 py-2 border-bottom border-weak shrink-0"> |
| 61 | + <span className="lumo-troubleshoot-avatar flex items-center justify-center rounded-full bg-weak shrink-0 ratio-square"> |
| 62 | + <img src={lumoCatIcon} alt="" aria-hidden="true" className="lumo-troubleshoot-avatar-icon" /> |
| 63 | + </span> |
| 64 | + <span className="text-semibold text-ellipsis flex-1">{title}</span> |
| 65 | + <Button |
| 66 | + icon |
| 67 | + shape="ghost" |
| 68 | + size="small" |
| 69 | + className="shrink-0" |
| 70 | + onClick={() => setIsOpen(false)} |
| 71 | + title={c('Action').t`Close`} |
| 72 | + > |
| 73 | + <IcCross /> |
| 74 | + </Button> |
| 75 | + </div> |
| 76 | + <iframe |
| 77 | + title={title} |
| 78 | + src={src} |
| 79 | + className="w-full flex-1 border-none" |
| 80 | + allow="clipboard-write" |
| 81 | + /> |
| 82 | + </div> |
| 83 | + )} |
| 84 | + <button |
| 85 | + type="button" |
| 86 | + className="lumo-troubleshoot-launcher link-focus inline-flex items-center gap-2 rounded-full bg-norm border border-weak shadow-norm color-norm py-1 pl-1 pr-3" |
| 87 | + aria-expanded={isOpen} |
| 88 | + onClick={() => (isOpen ? setIsOpen(false) : open())} |
| 89 | + > |
| 90 | + <span className="lumo-troubleshoot-avatar flex items-center justify-center rounded-full bg-weak shrink-0 ratio-square"> |
| 91 | + <img src={lumoCatIcon} alt="" aria-hidden="true" className="lumo-troubleshoot-avatar-icon" /> |
| 92 | + </span> |
| 93 | + <span className="text-sm text-semibold">{c('Action').t`Get help from ${LUMO_SHORT_APP_NAME}`}</span> |
| 94 | + </button> |
| 95 | + </div> |
| 96 | + ); |
| 97 | +}; |
| 98 | + |
| 99 | +export default TroubleshootWithLumo; |
0 commit comments