Add support for ACI safety numbers behind a feature flag
This commit is contained in:
parent
42cd8ce792
commit
c1580a5eb3
38 changed files with 1392 additions and 204 deletions
|
@ -12,7 +12,7 @@ const ERROR_CORRECTION_LEVEL = 'L';
|
|||
type PropsType = Readonly<{
|
||||
alt: string;
|
||||
className?: string;
|
||||
data: string;
|
||||
data: string | Uint8Array;
|
||||
}>;
|
||||
|
||||
export function QrCode(props: PropsType): ReactElement {
|
||||
|
@ -37,6 +37,9 @@ export function QrCode(props: PropsType): ReactElement {
|
|||
if (getEnvironment() === Environment.Production) {
|
||||
return;
|
||||
}
|
||||
if (data instanceof Uint8Array) {
|
||||
return;
|
||||
}
|
||||
|
||||
void navigator.clipboard.writeText(data);
|
||||
|
||||
|
|
|
@ -1,34 +1,81 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState, useCallback } from 'react';
|
||||
|
||||
import { SafetyNumberMode } from '../types/safetyNumber';
|
||||
import { isSafetyNumberNotAvailable } from '../util/isSafetyNumberNotAvailable';
|
||||
import { Modal } from './Modal';
|
||||
import type { PropsType as SafetyNumberViewerPropsType } from './SafetyNumberViewer';
|
||||
import { SafetyNumberViewer } from './SafetyNumberViewer';
|
||||
import { SafetyNumberOnboarding } from './SafetyNumberOnboarding';
|
||||
import { SafetyNumberNotReady } from './SafetyNumberNotReady';
|
||||
|
||||
type PropsType = {
|
||||
toggleSafetyNumberModal: () => unknown;
|
||||
hasCompletedSafetyNumberOnboarding: boolean;
|
||||
markHasCompletedSafetyNumberOnboarding: () => unknown;
|
||||
} & Omit<SafetyNumberViewerPropsType, 'onClose'>;
|
||||
|
||||
export function SafetyNumberModal({
|
||||
i18n,
|
||||
toggleSafetyNumberModal,
|
||||
hasCompletedSafetyNumberOnboarding,
|
||||
markHasCompletedSafetyNumberOnboarding,
|
||||
...safetyNumberViewerProps
|
||||
}: PropsType): JSX.Element | null {
|
||||
return (
|
||||
<Modal
|
||||
modalName="SafetyNumberModal"
|
||||
hasXButton
|
||||
i18n={i18n}
|
||||
moduleClassName="module-SafetyNumberViewer__modal"
|
||||
onClose={toggleSafetyNumberModal}
|
||||
title={i18n('icu:SafetyNumberModal__title')}
|
||||
>
|
||||
const { contact, safetyNumberMode } = safetyNumberViewerProps;
|
||||
|
||||
const [isOnboarding, setIsOnboarding] = useState(
|
||||
safetyNumberMode === SafetyNumberMode.ACIAndE164 &&
|
||||
!hasCompletedSafetyNumberOnboarding
|
||||
);
|
||||
|
||||
const showOnboarding = useCallback(() => {
|
||||
setIsOnboarding(true);
|
||||
}, [setIsOnboarding]);
|
||||
|
||||
const hideOnboarding = useCallback(() => {
|
||||
setIsOnboarding(false);
|
||||
markHasCompletedSafetyNumberOnboarding();
|
||||
}, [setIsOnboarding, markHasCompletedSafetyNumberOnboarding]);
|
||||
|
||||
let title: string | undefined;
|
||||
let content: JSX.Element;
|
||||
let hasXButton = true;
|
||||
if (isSafetyNumberNotAvailable(contact)) {
|
||||
content = (
|
||||
<SafetyNumberNotReady
|
||||
i18n={i18n}
|
||||
onClose={() => toggleSafetyNumberModal()}
|
||||
/>
|
||||
);
|
||||
hasXButton = false;
|
||||
} else if (isOnboarding) {
|
||||
content = <SafetyNumberOnboarding i18n={i18n} onClose={hideOnboarding} />;
|
||||
} else {
|
||||
title = i18n('icu:SafetyNumberModal__title');
|
||||
|
||||
content = (
|
||||
<SafetyNumberViewer
|
||||
i18n={i18n}
|
||||
onClose={toggleSafetyNumberModal}
|
||||
showOnboarding={showOnboarding}
|
||||
{...safetyNumberViewerProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
modalName="SafetyNumberModal"
|
||||
hasXButton={hasXButton}
|
||||
i18n={i18n}
|
||||
moduleClassName="module-SafetyNumberViewer__modal"
|
||||
onClose={toggleSafetyNumberModal}
|
||||
title={title}
|
||||
>
|
||||
{content}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
23
ts/components/SafetyNumberNotReady.stories.tsx
Normal file
23
ts/components/SafetyNumberNotReady.stories.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { SafetyNumberNotReady } from './SafetyNumberNotReady';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
export default {
|
||||
title: 'Components/SafetyNumberNotReady',
|
||||
};
|
||||
|
||||
export function Default(): JSX.Element {
|
||||
return <SafetyNumberNotReady i18n={i18n} onClose={action('close')} />;
|
||||
}
|
||||
|
||||
Default.story = {
|
||||
name: 'Safety Number Not Ready',
|
||||
};
|
42
ts/components/SafetyNumberNotReady.tsx
Normal file
42
ts/components/SafetyNumberNotReady.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { Modal } from './Modal';
|
||||
import { Intl } from './Intl';
|
||||
import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { SAFETY_NUMBER_MIGRATION_URL } from '../types/support';
|
||||
|
||||
export type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
function onLearnMore() {
|
||||
openLinkInWebBrowser(SAFETY_NUMBER_MIGRATION_URL);
|
||||
}
|
||||
|
||||
export function SafetyNumberNotReady({
|
||||
i18n,
|
||||
onClose,
|
||||
}: PropsType): JSX.Element | null {
|
||||
return (
|
||||
<div className="module-SafetyNumberNotReady">
|
||||
<div>
|
||||
<Intl i18n={i18n} id="icu:SafetyNumberNotReady__body" />
|
||||
</div>
|
||||
|
||||
<Modal.ButtonFooter>
|
||||
<Button onClick={onLearnMore} variant={ButtonVariant.Secondary}>
|
||||
<Intl i18n={i18n} id="icu:SafetyNumberNotReady__learn-more" />
|
||||
</Button>
|
||||
<Button onClick={onClose} variant={ButtonVariant.Secondary}>
|
||||
<Intl i18n={i18n} id="icu:ok" />
|
||||
</Button>
|
||||
</Modal.ButtonFooter>
|
||||
</div>
|
||||
);
|
||||
}
|
23
ts/components/SafetyNumberOnboarding.stories.tsx
Normal file
23
ts/components/SafetyNumberOnboarding.stories.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { SafetyNumberOnboarding } from './SafetyNumberOnboarding';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
export default {
|
||||
title: 'Components/SafetyNumberOnboarding',
|
||||
};
|
||||
|
||||
export function Default(): JSX.Element {
|
||||
return <SafetyNumberOnboarding i18n={i18n} onClose={action('close')} />;
|
||||
}
|
||||
|
||||
Default.story = {
|
||||
name: 'Safety Number Onboarding',
|
||||
};
|
78
ts/components/SafetyNumberOnboarding.tsx
Normal file
78
ts/components/SafetyNumberOnboarding.tsx
Normal file
|
@ -0,0 +1,78 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import Lottie from 'lottie-react';
|
||||
import type { LottieRefCurrentProps } from 'lottie-react';
|
||||
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { Intl } from './Intl';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { SAFETY_NUMBER_MIGRATION_URL } from '../types/support';
|
||||
import { useReducedMotion } from '../hooks/useReducedMotion';
|
||||
import animationData from '../../images/safety-number-onboarding.json';
|
||||
import reducedAnimationData from '../../images/safety-number-onboarding-reduced-motion.json';
|
||||
|
||||
export type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export function SafetyNumberOnboarding({
|
||||
i18n,
|
||||
onClose,
|
||||
}: PropsType): JSX.Element | null {
|
||||
const isMotionReduced = useReducedMotion();
|
||||
const lottieRef = useRef<LottieRefCurrentProps | null>(null);
|
||||
|
||||
const onDOMLoaded = useCallback(() => {
|
||||
if (isMotionReduced) {
|
||||
lottieRef.current?.goToAndPlay(0);
|
||||
return;
|
||||
}
|
||||
|
||||
lottieRef.current?.playSegments(
|
||||
[
|
||||
[0, 360],
|
||||
[60, 360],
|
||||
],
|
||||
true
|
||||
);
|
||||
}, [isMotionReduced]);
|
||||
|
||||
return (
|
||||
<div className="module-SafetyNumberOnboarding">
|
||||
<h2>
|
||||
<Intl i18n={i18n} id="icu:SafetyNumberOnboarding__title" />
|
||||
</h2>
|
||||
<p>
|
||||
<Intl i18n={i18n} id="icu:SafetyNumberOnboarding__p1" />
|
||||
</p>
|
||||
<p>
|
||||
<Intl i18n={i18n} id="icu:SafetyNumberOnboarding__p2" />
|
||||
</p>
|
||||
<Lottie
|
||||
lottieRef={lottieRef}
|
||||
animationData={isMotionReduced ? reducedAnimationData : animationData}
|
||||
onDOMLoaded={onDOMLoaded}
|
||||
/>
|
||||
<div className="module-SafetyNumberOnboarding__help">
|
||||
<a
|
||||
key="signal-support"
|
||||
href={SAFETY_NUMBER_MIGRATION_URL}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<Intl i18n={i18n} id="icu:SafetyNumberOnboarding__help" />
|
||||
</a>
|
||||
</div>
|
||||
<Button
|
||||
className="module-SafetyNumberOnboarding__close"
|
||||
onClick={onClose}
|
||||
variant={ButtonVariant.Primary}
|
||||
>
|
||||
<Intl i18n={i18n} id="icu:SafetyNumberOnboarding__close" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -8,9 +8,33 @@ import { boolean, text } from '@storybook/addon-knobs';
|
|||
import type { PropsType } from './SafetyNumberViewer';
|
||||
import { SafetyNumberViewer } from './SafetyNumberViewer';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import {
|
||||
SafetyNumberIdentifierType,
|
||||
SafetyNumberMode,
|
||||
} from '../types/safetyNumber';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||
|
||||
function generateQRData() {
|
||||
const data = new Uint8Array(128);
|
||||
for (let i = 0; i < data.length; i += 1) {
|
||||
data[i] = Math.floor(Math.random() * 256);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function generateNumberBlocks() {
|
||||
const result = new Array<string>();
|
||||
for (let i = 0; i < 12; i += 1) {
|
||||
let digits = '';
|
||||
for (let j = 0; j < 5; j += 1) {
|
||||
digits += Math.floor(Math.random() * 10);
|
||||
}
|
||||
result.push(digits);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const contactWithAllData = getDefaultConversation({
|
||||
|
@ -49,7 +73,17 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
contact: overrideProps.contact || contactWithAllData,
|
||||
generateSafetyNumber: action('generate-safety-number'),
|
||||
i18n,
|
||||
safetyNumber: text('safetyNumber', overrideProps.safetyNumber || 'XXX'),
|
||||
safetyNumberMode: overrideProps.safetyNumberMode ?? SafetyNumberMode.ACI,
|
||||
safetyNumbers: overrideProps.safetyNumbers ?? [
|
||||
{
|
||||
identifierType: SafetyNumberIdentifierType.ACIIdentifier,
|
||||
numberBlocks: text(
|
||||
'safetyNumber',
|
||||
generateNumberBlocks().join(' ')
|
||||
).split(' '),
|
||||
qrData: generateQRData(),
|
||||
},
|
||||
],
|
||||
toggleVerified: action('toggle-verified'),
|
||||
verificationDisabled: boolean(
|
||||
'verificationDisabled',
|
||||
|
@ -68,6 +102,56 @@ export function SafetyNumber(): JSX.Element {
|
|||
return <SafetyNumberViewer {...createProps({})} />;
|
||||
}
|
||||
|
||||
export function SafetyNumberBeforeE164Transition(): JSX.Element {
|
||||
return (
|
||||
<SafetyNumberViewer
|
||||
{...createProps({
|
||||
safetyNumberMode: SafetyNumberMode.E164,
|
||||
safetyNumbers: [
|
||||
{
|
||||
identifierType: SafetyNumberIdentifierType.E164Identifier,
|
||||
numberBlocks: text(
|
||||
'safetyNumber',
|
||||
generateNumberBlocks().join(' ')
|
||||
).split(' '),
|
||||
qrData: generateQRData(),
|
||||
},
|
||||
],
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
SafetyNumberBeforeE164Transition.story = {
|
||||
name: 'Safety Number (before e164 transition)',
|
||||
};
|
||||
|
||||
export function SafetyNumberE164Transition(): JSX.Element {
|
||||
return (
|
||||
<SafetyNumberViewer
|
||||
{...createProps({
|
||||
safetyNumberMode: SafetyNumberMode.ACIAndE164,
|
||||
safetyNumbers: [
|
||||
{
|
||||
identifierType: SafetyNumberIdentifierType.E164Identifier,
|
||||
numberBlocks: generateNumberBlocks(),
|
||||
qrData: generateQRData(),
|
||||
},
|
||||
{
|
||||
identifierType: SafetyNumberIdentifierType.ACIIdentifier,
|
||||
numberBlocks: generateNumberBlocks(),
|
||||
qrData: generateQRData(),
|
||||
},
|
||||
],
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
SafetyNumberE164Transition.story = {
|
||||
name: 'Safety Number (e164 transition)',
|
||||
};
|
||||
|
||||
export function SafetyNumberNotVerified(): JSX.Element {
|
||||
return (
|
||||
<SafetyNumberViewer
|
||||
|
|
|
@ -1,19 +1,31 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { QrCode } from './QrCode';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import { Intl } from './Intl';
|
||||
import { Emojify } from './conversation/Emojify';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import type { SafetyNumberType } from '../types/safetyNumber';
|
||||
import { SAFETY_NUMBER_MIGRATION_URL } from '../types/support';
|
||||
import {
|
||||
SafetyNumberIdentifierType,
|
||||
SafetyNumberMode,
|
||||
} from '../types/safetyNumber';
|
||||
|
||||
export type PropsType = {
|
||||
contact: ConversationType;
|
||||
generateSafetyNumber: (contact: ConversationType) => void;
|
||||
i18n: LocalizerType;
|
||||
onClose: () => void;
|
||||
safetyNumber: string;
|
||||
safetyNumberMode: SafetyNumberMode;
|
||||
safetyNumbers?: ReadonlyArray<SafetyNumberType>;
|
||||
toggleVerified: (contact: ConversationType) => void;
|
||||
showOnboarding?: () => void;
|
||||
verificationDisabled: boolean;
|
||||
};
|
||||
|
||||
|
@ -22,23 +34,28 @@ export function SafetyNumberViewer({
|
|||
generateSafetyNumber,
|
||||
i18n,
|
||||
onClose,
|
||||
safetyNumber,
|
||||
safetyNumberMode,
|
||||
safetyNumbers,
|
||||
toggleVerified,
|
||||
showOnboarding,
|
||||
verificationDisabled,
|
||||
}: PropsType): JSX.Element | null {
|
||||
const hasSafetyNumbers = safetyNumbers != null;
|
||||
React.useEffect(() => {
|
||||
if (!contact) {
|
||||
return;
|
||||
}
|
||||
|
||||
generateSafetyNumber(contact);
|
||||
}, [contact, generateSafetyNumber, safetyNumber]);
|
||||
}, [contact, generateSafetyNumber]);
|
||||
|
||||
if (!contact) {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
if (!contact || !hasSafetyNumbers) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!contact.phoneNumber) {
|
||||
if (!safetyNumbers.length) {
|
||||
return (
|
||||
<div className="module-SafetyNumberViewer">
|
||||
<div>{i18n('icu:cannotGenerateSafetyNumber')}</div>
|
||||
|
@ -55,12 +72,10 @@ export function SafetyNumberViewer({
|
|||
);
|
||||
}
|
||||
|
||||
const showNumber = Boolean(contact.name || contact.profileName);
|
||||
const numberFragment =
|
||||
showNumber && contact.phoneNumber ? ` · ${contact.phoneNumber}` : '';
|
||||
const name = `${contact.title}${numberFragment}`;
|
||||
const boldName = (
|
||||
<span className="module-SafetyNumberViewer__bold-name">{name}</span>
|
||||
<span className="module-SafetyNumberViewer__bold-name">
|
||||
<Emojify text={contact.title} />
|
||||
</span>
|
||||
);
|
||||
|
||||
const { isVerified } = contact;
|
||||
|
@ -68,32 +83,136 @@ export function SafetyNumberViewer({
|
|||
? i18n('icu:SafetyNumberViewer__clearVerification')
|
||||
: i18n('icu:SafetyNumberViewer__markAsVerified');
|
||||
|
||||
const isMigrationVisible = safetyNumberMode === SafetyNumberMode.ACIAndE164;
|
||||
|
||||
const visibleSafetyNumber = safetyNumbers.at(selectedIndex);
|
||||
if (!visibleSafetyNumber) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cardClassName = classNames('module-SafetyNumberViewer__card', {
|
||||
'module-SafetyNumberViewer__card--aci':
|
||||
visibleSafetyNumber.identifierType ===
|
||||
SafetyNumberIdentifierType.ACIIdentifier,
|
||||
'module-SafetyNumberViewer__card--e164':
|
||||
visibleSafetyNumber.identifierType ===
|
||||
SafetyNumberIdentifierType.E164Identifier,
|
||||
});
|
||||
|
||||
const numberBlocks = visibleSafetyNumber.numberBlocks.join(' ');
|
||||
|
||||
const safetyNumberCard = (
|
||||
<div className="module-SafetyNumberViewer__card-container">
|
||||
<div className={cardClassName}>
|
||||
<QrCode
|
||||
className="module-SafetyNumberViewer__card__qr"
|
||||
data={visibleSafetyNumber.qrData}
|
||||
alt={i18n('icu:Install__scan-this-code')}
|
||||
/>
|
||||
<div className="module-SafetyNumberViewer__card__number">
|
||||
{numberBlocks}
|
||||
</div>
|
||||
|
||||
{selectedIndex > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={i18n('icu:SafetyNumberViewer__card__prev')}
|
||||
className="module-SafetyNumberViewer__card__prev"
|
||||
onClick={() => setSelectedIndex(x => x - 1)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedIndex < safetyNumbers.length - 1 && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={i18n('icu:SafetyNumberViewer__card__next')}
|
||||
className="module-SafetyNumberViewer__card__next"
|
||||
onClick={() => setSelectedIndex(x => x + 1)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const carousel = (
|
||||
<div className="module-SafetyNumberViewer__carousel">
|
||||
{safetyNumbers.map(({ identifierType }, index) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={i18n('icu:SafetyNumberViewer__carousel__dot', {
|
||||
index: index + 1,
|
||||
total: safetyNumbers.length,
|
||||
})}
|
||||
aria-pressed={index === selectedIndex}
|
||||
key={identifierType}
|
||||
className="module-SafetyNumberViewer__carousel__dot"
|
||||
onClick={() => setSelectedIndex(index)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="module-SafetyNumberViewer">
|
||||
<div className="module-SafetyNumberViewer__number">
|
||||
{safetyNumber || getPlaceholder()}
|
||||
</div>
|
||||
<Intl i18n={i18n} id="icu:verifyHelp" components={{ name: boldName }} />
|
||||
<div className="module-SafetyNumberViewer__verification-status">
|
||||
{isVerified ? (
|
||||
<span className="module-SafetyNumberViewer__icon--verified" />
|
||||
) : (
|
||||
<span className="module-SafetyNumberViewer__icon--shield" />
|
||||
)}
|
||||
{isVerified ? (
|
||||
{isMigrationVisible && (
|
||||
<div className="module-SafetyNumberViewer__migration">
|
||||
<div className="module-SafetyNumberViewer__migration__icon" />
|
||||
|
||||
<div className="module-SafetyNumberViewer__migration__text">
|
||||
<p>
|
||||
<Intl i18n={i18n} id="icu:SafetyNumberViewer__migration__text" />
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
href={SAFETY_NUMBER_MIGRATION_URL}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
onClick={e => {
|
||||
if (showOnboarding) {
|
||||
e.preventDefault();
|
||||
showOnboarding();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="icu:SafetyNumberViewer__migration__learn_more"
|
||||
/>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{safetyNumberCard}
|
||||
|
||||
{safetyNumbers.length > 1 && carousel}
|
||||
|
||||
<div className="module-SafetyNumberViewer__help">
|
||||
{isMigrationVisible ? (
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="icu:isVerified"
|
||||
id="icu:SafetyNumberViewer__hint--migration"
|
||||
components={{ name: boldName }}
|
||||
/>
|
||||
) : (
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="icu:isNotVerified"
|
||||
id="icu:SafetyNumberViewer__hint--normal"
|
||||
components={{ name: boldName }}
|
||||
/>
|
||||
)}
|
||||
<br />
|
||||
<a href={SAFETY_NUMBER_MIGRATION_URL} rel="noreferrer" target="_blank">
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="icu:SafetyNumberViewer__migration__learn_more"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="module-SafetyNumberViewer__button">
|
||||
<Button
|
||||
disabled={verificationDisabled}
|
||||
|
@ -108,9 +227,3 @@ export function SafetyNumberViewer({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getPlaceholder(): string {
|
||||
return Array.from(Array(12))
|
||||
.map(() => 'XXXXX')
|
||||
.join(' ');
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue