recoveryCodes.tsx 2.9 KB

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