recoveryCodes.tsx 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. import styled from '@emotion/styled';
  2. import {Button} from 'sentry/components/button';
  3. import Confirm from 'sentry/components/confirm';
  4. import {CopyToClipboardButton} from 'sentry/components/copyToClipboardButton';
  5. import EmptyMessage from 'sentry/components/emptyMessage';
  6. import Panel from 'sentry/components/panels/panel';
  7. import PanelAlert from 'sentry/components/panels/panelAlert';
  8. import PanelBody from 'sentry/components/panels/panelBody';
  9. import PanelHeader from 'sentry/components/panels/panelHeader';
  10. import PanelItem from 'sentry/components/panels/panelItem';
  11. import {IconDownload, IconPrint} from 'sentry/icons';
  12. import {t} from 'sentry/locale';
  13. import {space} from 'sentry/styles/space';
  14. type Props = {
  15. codes: string[];
  16. isEnrolled: boolean;
  17. onRegenerateBackupCodes: () => void;
  18. className?: string;
  19. };
  20. function RecoveryCodes({className, isEnrolled, codes, onRegenerateBackupCodes}: Props) {
  21. const printCodes = () => {
  22. // eslint-disable-next-line dot-notation
  23. const iframe = window.frames['printable'];
  24. iframe.document.write(codes.join('<br>'));
  25. iframe.print();
  26. iframe.document.close();
  27. };
  28. if (!isEnrolled || !codes) {
  29. return null;
  30. }
  31. const formattedCodes = codes.join(' \n');
  32. return (
  33. <CodeContainer className={className}>
  34. <PanelHeader hasButtons>
  35. {t('Unused Codes')}
  36. <Actions>
  37. <CopyToClipboardButton text={formattedCodes} size="sm" />
  38. <Button size="sm" onClick={printCodes} aria-label={t('print')}>
  39. <IconPrint />
  40. </Button>
  41. <Button
  42. size="sm"
  43. download="sentry-recovery-codes.txt"
  44. href={`data:text/plain;charset=utf-8,${formattedCodes}`}
  45. aria-label={t('download')}
  46. >
  47. <IconDownload />
  48. </Button>
  49. <Confirm
  50. onConfirm={onRegenerateBackupCodes}
  51. message={t(
  52. 'Are you sure you want to regenerate recovery codes? Your old codes will no longer work.'
  53. )}
  54. >
  55. <Button priority="danger" size="sm">
  56. {t('Regenerate Codes')}
  57. </Button>
  58. </Confirm>
  59. </Actions>
  60. </PanelHeader>
  61. <PanelBody>
  62. <PanelAlert type="warning">
  63. {t(
  64. 'Make sure to save a copy of your recovery codes and store them in a safe place.'
  65. )}
  66. </PanelAlert>
  67. <div>{!!codes.length && codes.map(code => <Code key={code}>{code}</Code>)}</div>
  68. {!codes.length && (
  69. <EmptyMessage>{t('You have no more recovery codes to use')}</EmptyMessage>
  70. )}
  71. </PanelBody>
  72. <iframe data-test-id="frame" name="printable" style={{display: 'none'}} />
  73. </CodeContainer>
  74. );
  75. }
  76. export default RecoveryCodes;
  77. const CodeContainer = styled(Panel)`
  78. margin-top: ${space(4)};
  79. `;
  80. const Actions = styled('div')`
  81. display: grid;
  82. grid-auto-flow: column;
  83. gap: ${space(1)};
  84. `;
  85. const Code = styled(PanelItem)`
  86. font-family: ${p => p.theme.text.familyMono};
  87. padding: ${space(2)};
  88. `;