recoveryCodes.tsx 3.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. import styled from '@emotion/styled';
  2. import {Button, LinkButton} 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. <LinkButton
  42. size="sm"
  43. download="sentry-recovery-codes.txt"
  44. href={`data:text/plain;charset=utf-8,${formattedCodes}`}
  45. aria-label={t('download')}
  46. icon={<IconDownload />}
  47. />
  48. <Confirm
  49. onConfirm={onRegenerateBackupCodes}
  50. message={t(
  51. 'Are you sure you want to regenerate recovery codes? Your old codes will no longer work.'
  52. )}
  53. >
  54. <Button priority="danger" size="sm">
  55. {t('Regenerate Codes')}
  56. </Button>
  57. </Confirm>
  58. </Actions>
  59. </PanelHeader>
  60. <PanelBody>
  61. <PanelAlert type="warning">
  62. {t(
  63. 'Make sure to save a copy of your recovery codes and store them in a safe place.'
  64. )}
  65. </PanelAlert>
  66. <div>{!!codes.length && codes.map(code => <Code key={code}>{code}</Code>)}</div>
  67. {!codes.length && (
  68. <EmptyMessage>{t('You have no more recovery codes to use')}</EmptyMessage>
  69. )}
  70. </PanelBody>
  71. <iframe data-test-id="frame" name="printable" style={{display: 'none'}} />
  72. </CodeContainer>
  73. );
  74. }
  75. export default RecoveryCodes;
  76. const CodeContainer = styled(Panel)`
  77. margin-top: ${space(4)};
  78. `;
  79. const Actions = styled('div')`
  80. display: grid;
  81. grid-auto-flow: column;
  82. gap: ${space(1)};
  83. `;
  84. const Code = styled(PanelItem)`
  85. font-family: ${p => p.theme.text.familyMono};
  86. padding: ${space(2)};
  87. `;