recoveryCodes.tsx 2.8 KB

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