Fix emoji verification responsive layout (#31899)

* Extract SasEmoji to shared-components

and improve responsive layout

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add baseline screenshots

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix e2e test

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add SasEmoji snapshot test

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add figma link

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve doc

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add new dir to crypto-web-reviewers codeowners as per ask

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski
2026-01-29 17:40:04 +00:00
committed by GitHub
parent 24018f7e94
commit e07e26cae5
23 changed files with 444 additions and 143 deletions
+1
View File
@@ -16,6 +16,7 @@
/src/components/views/dialogs/devtools/Crypto.tsx @element-hq/element-crypto-web-reviewers /src/components/views/dialogs/devtools/Crypto.tsx @element-hq/element-crypto-web-reviewers
/playwright/e2e/crypto/ @element-hq/element-crypto-web-reviewers /playwright/e2e/crypto/ @element-hq/element-crypto-web-reviewers
/playwright/e2e/settings/encryption-user-tab/ @element-hq/element-crypto-web-reviewers /playwright/e2e/settings/encryption-user-tab/ @element-hq/element-crypto-web-reviewers
/packages/shared-components/src/crypto/ @element-hq/element-crypto-web-reviewers
/src/models/Call.ts @element-hq/element-call-reviewers /src/models/Call.ts @element-hq/element-call-reviewers
-1
View File
@@ -90,7 +90,6 @@
"@matrix-org/analytics-events": "^0.31.0", "@matrix-org/analytics-events": "^0.31.0",
"@matrix-org/emojibase-bindings": "^1.5.0", "@matrix-org/emojibase-bindings": "^1.5.0",
"@matrix-org/react-sdk-module-api": "^2.4.0", "@matrix-org/react-sdk-module-api": "^2.4.0",
"@matrix-org/spec": "^1.7.0",
"@sentry/browser": "^10.0.0", "@sentry/browser": "^10.0.0",
"@types/png-chunks-extract": "^1.0.2", "@types/png-chunks-extract": "^1.0.2",
"@vector-im/compound-design-tokens": "6.9.0", "@vector-im/compound-design-tokens": "6.9.0",
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+1
View File
@@ -50,6 +50,7 @@
}, },
"dependencies": { "dependencies": {
"@element-hq/element-web-module-api": "^1.8.0", "@element-hq/element-web-module-api": "^1.8.0",
"@matrix-org/spec": "^1.7.0",
"@vector-im/compound-design-tokens": "^6.4.3", "@vector-im/compound-design-tokens": "^6.4.3",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"counterpart": "^0.18.6", "counterpart": "^0.18.6",
@@ -0,0 +1,33 @@
/*
* Copyright 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
.container {
display: flex;
flex-wrap: wrap;
gap: var(--cpd-space-2x);
justify-content: space-evenly;
}
.segment {
display: inline-block;
margin-bottom: var(--cpd-space-4x);
text-align: center;
/* Allow maximum 4 per line, accounting for 8px gap */
min-width: calc(25% - 8px);
}
.emoji {
/* Use the Twemoji font for consistency with other clients */
font-family: Twemoji, var(--cpd-font-family-sans);
font-size: var(--cpd-font-size-heading-xl);
}
.label {
font-weight: var(--cpd-font-weight-regular);
font-size: var(--cpd-font-size-body-lg);
color: var(--cpd-color-text-secondary);
}
@@ -0,0 +1,48 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { type Meta, type StoryObj } from "@storybook/react-vite";
import { SasEmoji } from "./SasEmoji";
const meta = {
title: "Crypto/SasEmoji",
component: SasEmoji,
tags: ["autodocs"],
args: {
emoji: ["🍕", "🌽", "🚀", "🔒", "🔧", "🍓", "⌛"],
},
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/design/XLWIAB5n8yObYvU0INKPK1/Verification-by-Emoji?node-id=1-2935&t=NrV9JnuItrAyyh53-4",
},
},
} satisfies Meta<typeof SasEmoji>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const WorstCaseAlbanian: Story = {
globals: {
language: "sq",
},
args: {
emoji: ["🎅", "🎅", "🎅", "🎅", "🎅", "🎅", "🎅"],
},
};
export const WorstCaseGerman: Story = {
globals: {
language: "de",
},
args: {
emoji: ["🔧", "🔧", "🔧", "🔧", "🔧", "🔧", "🔧"],
},
};
@@ -0,0 +1,19 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { describe, it, expect } from "vitest";
import { render } from "@test-utils";
import { SasEmoji } from "./SasEmoji";
describe("<SasEmoji/>", () => {
it("should match snapshot", () => {
const { asFragment } = render(<SasEmoji emoji={["🦋", "🍄", "⚽", "🌏", "🦄", "🚀", "🔧"]} />);
expect(asFragment()).toMatchSnapshot();
});
});
@@ -0,0 +1,43 @@
/*
* Copyright 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import React, { type JSX } from "react";
import classNames from "classnames";
import { type SasEmoji, tEmoji } from "./SasEmojiTranslate.ts";
import styles from "./SasEmoji.module.css";
import { useI18n } from "../../utils/i18nContext.ts";
export type Props = {
/**
* The emoji to render
*/
emoji: [SasEmoji, SasEmoji, SasEmoji, SasEmoji, SasEmoji, SasEmoji, SasEmoji];
/**
* Optional className to apply to the container
*/
className?: string;
};
/**
* Renders the 7 emoji used for SAS verification.
* The component is responsive so can be rendered in any context, dialog, side panel.
*/
export function SasEmoji({ emoji, className }: Props): JSX.Element {
const { language } = useI18n();
const emojiBlocks = emoji.map((emoji, i) => (
<div className={styles.segment} key={i}>
<div className={styles.emoji} aria-hidden={true}>
{emoji}
</div>
<div className={styles.label}>{tEmoji(emoji, language)}</div>
</div>
));
return <div className={classNames(styles.container, className)}>{emojiBlocks}</div>;
}
@@ -0,0 +1,26 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { describe, it, expect } from "vitest";
import { tEmoji, type SasEmoji } from "./SasEmojiTranslate.ts";
describe("tEmoji", () => {
it.each([
["🐶", "en-GB", "Dog"],
["🐶", "en", "Dog"],
["🐶", "de-DE", "Hund"],
["🐶", "pt", "Cachorro"],
["🔧", "de-DE", "Schraubenschlüssel"],
["🎅", "sq", "Babagjyshi i Vitit të Ri"],
] as [emoji: SasEmoji, locale: string, expectation: string][])(
"should handle locale %s",
(emoji, locale, expectation) => {
expect(tEmoji(emoji, locale)).toEqual(expectation);
},
);
});
@@ -0,0 +1,122 @@
/*
* Copyright 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import SasEmojiJson from "@matrix-org/spec/sas-emoji.json";
import { getNormalizedLanguageKeys } from "matrix-web-i18n";
// Type as specified in https://spec.matrix.org/v1.17/client-server-api/#sas-method-emoji
export type SasEmoji =
| "🐶"
| "🐱"
| "🦁"
| "🐎"
| "🦄"
| "🐷"
| "🐘"
| "🐰"
| "🐼"
| "🐓"
| "🐧"
| "🐢"
| "🐟"
| "🐙"
| "🦋"
| "🌷"
| "🌳"
| "🌵"
| "🍄"
| "🌏"
| "🌙"
| "☁"
| "🔥"
| "🍌"
| "🍎"
| "🍓"
| "🌽"
| "🍕"
| "🎂"
| "❤"
| "😀"
| "🤖"
| "🎩"
| "👓"
| "🔧"
| "🎅"
| "👍"
| "☂"
| "⌛"
| "⏰"
| "🎁"
| "💡"
| "📕"
| "✏"
| "📎"
| "✂"
| "🔒"
| "🔑"
| "🔨"
| "☎"
| "🏁"
| "🚂"
| "🚲"
| "✈"
| "🚀"
| "🏆"
| "⚽"
| "🎸"
| "🎺"
| "🔔"
| "⚓"
| "🎧"
| "📁"
| "📌";
const SasEmojiMap = new Map<
SasEmoji,
[
description: string,
translations: {
[normalizedLanguageKey: string]: string;
},
]
>(
SasEmojiJson.map(({ emoji, description, translated_descriptions: translations }) => [
emoji as SasEmoji,
[
description,
// Normalize the translation keys
Object.keys(translations).reduce<Record<string, string>>((o, k) => {
for (const key of getNormalizedLanguageKeys(k)) {
o[key] = translations[k as keyof typeof translations]!;
}
return o;
}, {}),
],
]),
);
/**
* Translate given SAS emoji into the target locale
* @param emoji - the SAS emoji to translate
* @param locale - the BCP 47 locale to translate to, will fall back to English as the base locale for Matrix SAS Emoji.
*/
export function tEmoji(emoji: SasEmoji, locale: string): string {
const mapping = SasEmojiMap.get(emoji);
if (!mapping) {
throw new Error(`Emoji mapping not found for emoji ${emoji}`);
}
const [description, translations] = mapping;
for (const key of getNormalizedLanguageKeys(locale)) {
if (translations[key]) {
return translations[key];
}
}
return description;
}
@@ -0,0 +1,115 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<SasEmoji/> > should match snapshot 1`] = `
<DocumentFragment>
<div
class="container"
>
<div
class="segment"
>
<div
aria-hidden="true"
class="emoji"
>
🦋
</div>
<div
class="label"
>
Butterfly
</div>
</div>
<div
class="segment"
>
<div
aria-hidden="true"
class="emoji"
>
🍄
</div>
<div
class="label"
>
Mushroom
</div>
</div>
<div
class="segment"
>
<div
aria-hidden="true"
class="emoji"
>
</div>
<div
class="label"
>
Ball
</div>
</div>
<div
class="segment"
>
<div
aria-hidden="true"
class="emoji"
>
🌏
</div>
<div
class="label"
>
Globe
</div>
</div>
<div
class="segment"
>
<div
aria-hidden="true"
class="emoji"
>
🦄
</div>
<div
class="label"
>
Unicorn
</div>
</div>
<div
class="segment"
>
<div
aria-hidden="true"
class="emoji"
>
🚀
</div>
<div
class="label"
>
Rocket
</div>
</div>
<div
class="segment"
>
<div
aria-hidden="true"
class="emoji"
>
🔧
</div>
<div
class="label"
>
Spanner
</div>
</div>
</div>
</DocumentFragment>
`;
@@ -0,0 +1,8 @@
/*
* Copyright 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
export { SasEmoji } from "./SasEmoji.tsx";
+1
View File
@@ -12,6 +12,7 @@ export * from "./audio/PlayPauseButton";
export * from "./audio/SeekBar"; export * from "./audio/SeekBar";
export * from "./avatar/AvatarWithDetails"; export * from "./avatar/AvatarWithDetails";
export * from "./composer/Banner"; export * from "./composer/Banner";
export * from "./crypto/SasEmoji";
export * from "./event-tiles/TextualEventView"; export * from "./event-tiles/TextualEventView";
export * from "./message-body/MediaBody"; export * from "./message-body/MediaBody";
export * from "./pill-input/Pill"; export * from "./pill-input/Pill";
+5
View File
@@ -667,6 +667,11 @@
dependencies: dependencies:
"@babel/runtime" "^7.17.9" "@babel/runtime" "^7.17.9"
"@matrix-org/spec@^1.7.0":
version "1.16.0"
resolved "https://registry.yarnpkg.com/@matrix-org/spec/-/spec-1.16.0.tgz#c88f4ed521e4c0bd3a4c108bcaf13f25173a0fdc"
integrity sha512-xUKHkwGXXISMCfTrx6JW6uGEK5O8IeZVOjBm7FX1h/ihpK6l50nlSIMRYdtz4V6q3pvOVBOCft4hPYTJVeTZDA==
"@mdx-js/react@^3.0.0": "@mdx-js/react@^3.0.0":
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-3.1.1.tgz#24bda7fffceb2fe256f954482123cda1be5f5fef" resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-3.1.1.tgz#24bda7fffceb2fe256f954482123cda1be5f5fef"
+1 -1
View File
@@ -290,7 +290,7 @@ export async function doTwoWaySasVerification(page: Page, verifier: JSHandle<Ver
// on the bot side, wait for the emojis, confirm they match, and return them // on the bot side, wait for the emojis, confirm they match, and return them
const emojis = await handleSasVerification(verifier); const emojis = await handleSasVerification(verifier);
const emojiBlocks = page.locator(".mx_VerificationShowSas_emojiSas_block"); const emojiBlocks = page.locator(".mx_VerificationShowSas_emojiSas > div");
await expect(emojiBlocks).toHaveCount(emojis.length); await expect(emojiBlocks).toHaveCount(emojis.length);
// then, check that our application shows an emoji panel with the same emojis. // then, check that our application shows an emoji panel with the same emojis.
@@ -20,40 +20,9 @@ Please see LICENSE files in the repository root for full details.
} }
.mx_VerificationShowSas_emojiSas { .mx_VerificationShowSas_emojiSas {
text-align: center;
display: flex;
flex-wrap: wrap;
justify-content: center;
margin: 25px 0; margin: 25px 0;
} }
.mx_VerificationShowSas_emojiSas_block {
display: inline-block;
margin-bottom: 16px;
position: relative;
width: 52px;
}
.mx_Dialog .mx_VerificationShowSas_emojiSas_block,
.mx_AuthPage_modal .mx_VerificationShowSas_emojiSas_block {
width: 60px;
}
.mx_VerificationShowSas_emojiSas_emoji {
font-size: $font-32px;
/* Use the Twemoji font for consistency with other clients */
font-family: Twemoji, var(--cpd-font-family-sans);
}
.mx_VerificationShowSas_emojiSas_label {
font-size: $font-12px;
word-break: break-word;
}
.mx_VerificationShowSas_emojiSas_break {
flex-basis: 100%;
}
.mx_VerificationShowSas_buttonRow { .mx_VerificationShowSas_buttonRow {
text-align: center; text-align: center;
display: flex; display: flex;
@@ -6,12 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import React from "react"; import React, { type ComponentProps } from "react";
import { type Device } from "matrix-js-sdk/src/matrix"; import { type Device } from "matrix-js-sdk/src/matrix";
import { type GeneratedSas, type EmojiMapping } from "matrix-js-sdk/src/crypto-api"; import { type GeneratedSas } from "matrix-js-sdk/src/crypto-api";
import SasEmoji from "@matrix-org/spec/sas-emoji.json"; import { SasEmoji } from "@element-hq/web-shared-components";
import { _t, getNormalizedLanguageKeys, getUserLanguage } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { PendingActionSpinner } from "../right_panel/EncryptionInfo"; import { PendingActionSpinner } from "../right_panel/EncryptionInfo";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
@@ -34,52 +34,6 @@ interface IState {
cancelling?: boolean; cancelling?: boolean;
} }
const SasEmojiMap = new Map<
string, // lowercase
{
description: string;
translations: {
[normalizedLanguageKey: string]: string;
};
}
>(
SasEmoji.map(({ description, translated_descriptions: translations }) => [
description.toLowerCase(),
{
description,
// Normalize the translation keys
translations: Object.keys(translations).reduce<Record<string, string>>((o, k) => {
for (const key of getNormalizedLanguageKeys(k)) {
o[key] = translations[k as keyof typeof translations]!;
}
return o;
}, {}),
},
]),
);
/**
* Translate given EmojiMapping into the target locale
* @param mapping - the given EmojiMapping to translate
* @param locale - the BCP 47 locale to translate to, will fall back to English as the base locale for Matrix SAS Emoji.
*/
export function tEmoji(mapping: EmojiMapping, locale: string): string {
const name = mapping[1];
const emoji = SasEmojiMap.get(name.toLowerCase());
if (!emoji) {
console.warn("Emoji not found for translation", name);
return name;
}
for (const key of getNormalizedLanguageKeys(locale)) {
if (!!emoji.translations[key]) {
return emoji.translations[key];
}
}
return emoji.description;
}
export default class VerificationShowSas extends React.Component<IProps, IState> { export default class VerificationShowSas extends React.Component<IProps, IState> {
public constructor(props: IProps) { public constructor(props: IProps) {
super(props); super(props);
@@ -100,25 +54,14 @@ export default class VerificationShowSas extends React.Component<IProps, IState>
}; };
public render(): React.ReactNode { public render(): React.ReactNode {
const locale = getUserLanguage();
let sasDisplay; let sasDisplay;
let sasCaption; let sasCaption;
if (this.props.sas.emoji) { if (this.props.sas.emoji) {
const emojiBlocks = this.props.sas.emoji.map((emoji, i) => (
<div className="mx_VerificationShowSas_emojiSas_block" key={i}>
<div className="mx_VerificationShowSas_emojiSas_emoji" aria-hidden={true}>
{emoji[0]}
</div>
<div className="mx_VerificationShowSas_emojiSas_label">{tEmoji(emoji, locale)}</div>
</div>
));
sasDisplay = ( sasDisplay = (
<div className="mx_VerificationShowSas_emojiSas"> <SasEmoji
{emojiBlocks.slice(0, 4)} className="mx_VerificationShowSas_emojiSas"
<div className="mx_VerificationShowSas_emojiSas_break" /> emoji={this.props.sas.emoji.map((e) => e[0]) as ComponentProps<typeof SasEmoji>["emoji"]}
{emojiBlocks.slice(4)} />
</div>
); );
sasCaption = this.props.isSelf sasCaption = this.props.isSelf
? _t("encryption|verification|confirm_the_emojis") ? _t("encryption|verification|confirm_the_emojis")
@@ -1,23 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { type EmojiMapping } from "matrix-js-sdk/src/crypto-api";
import { tEmoji } from "../../../../src/components/views/verification/VerificationShowSas";
describe("tEmoji", () => {
it.each([
["en-GB", "Dog"],
["en", "Dog"],
["de-DE", "Hund"],
["pt", "Cachorro"],
])("should handle locale %s", (locale, expectation) => {
const emoji: EmojiMapping = ["🐶", "Dog"];
expect(tEmoji(emoji, locale)).toEqual(expectation);
});
});
@@ -34,7 +34,7 @@ describe("IncomingSasDialog", () => {
it("should show some emojis once keys are exchanged", () => { it("should show some emojis once keys are exchanged", () => {
const mockVerifier = makeMockVerifier(); const mockVerifier = makeMockVerifier();
const { container } = renderComponent(mockVerifier); const { getAllByText } = renderComponent(mockVerifier);
// fire the ShowSas event // fire the ShowSas event
const sasEvent = makeMockSasCallbacks(); const sasEvent = makeMockSasCallbacks();
@@ -42,11 +42,8 @@ describe("IncomingSasDialog", () => {
mockVerifier.emit(VerifierEvent.ShowSas, sasEvent); mockVerifier.emit(VerifierEvent.ShowSas, sasEvent);
}); });
const emojis = container.getElementsByClassName("mx_VerificationShowSas_emojiSas_block"); expect(getAllByText("🦄")).toHaveLength(7);
expect(emojis.length).toEqual(7); expect(getAllByText("Unicorn")).toHaveLength(7);
for (const emoji of emojis) {
expect(emoji).toHaveTextContent("🦄Unicorn");
}
}); });
}); });
@@ -456,41 +456,38 @@ exports[`VerificationRequestDialog When other device accepted emoji, displays em
Confirm that the emojis below match those shown on your other device. Confirm that the emojis below match those shown on your other device.
</p> </p>
<div <div
class="mx_VerificationShowSas_emojiSas" class="_container_1lqqy_8 mx_VerificationShowSas_emojiSas"
> >
<div <div
class="mx_VerificationShowSas_emojiSas_block" class="_segment_1lqqy_15"
> >
<div <div
aria-hidden="true" aria-hidden="true"
class="mx_VerificationShowSas_emojiSas_emoji" class="_emoji_1lqqy_23"
> >
🐶 🐶
</div> </div>
<div <div
class="mx_VerificationShowSas_emojiSas_label" class="_label_1lqqy_29"
> >
Dog Dog
</div> </div>
</div> </div>
<div <div
class="mx_VerificationShowSas_emojiSas_block" class="_segment_1lqqy_15"
> >
<div <div
aria-hidden="true" aria-hidden="true"
class="mx_VerificationShowSas_emojiSas_emoji" class="_emoji_1lqqy_23"
> >
🐱 🐱
</div> </div>
<div <div
class="mx_VerificationShowSas_emojiSas_label" class="_label_1lqqy_29"
> >
Cat Cat
</div> </div>
</div> </div>
<div
class="mx_VerificationShowSas_emojiSas_break"
/>
</div> </div>
<p /> <p />
<div <div
@@ -105,7 +105,7 @@ describe("<VerificationPanel />", () => {
}); });
it("should show some emojis once keys are exchanged", () => { it("should show some emojis once keys are exchanged", () => {
const { container } = renderComponent({ const { getAllByText } = renderComponent({
request: mockRequest, request: mockRequest,
phase: Phase.Started, phase: Phase.Started,
}); });
@@ -117,11 +117,8 @@ describe("<VerificationPanel />", () => {
mockVerifier.emit(VerifierEvent.ShowSas, sasEvent); mockVerifier.emit(VerifierEvent.ShowSas, sasEvent);
}); });
const emojis = container.getElementsByClassName("mx_VerificationShowSas_emojiSas_block"); expect(getAllByText("🦄")).toHaveLength(7);
expect(emojis.length).toEqual(7); expect(getAllByText("Unicorn")).toHaveLength(7);
for (const emoji of emojis) {
expect(emoji).toHaveTextContent("🦄Unicorn");
}
}); });
describe("'Verify own device' flow", () => { describe("'Verify own device' flow", () => {