From a06236a5b7462c137dd0b486052ee19c6f1f84fb Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Tue, 18 Aug 2026 14:20:33 -0300 Subject: [PATCH 1/7] feat: Remove screenshare gate for room view --- .../client/views/room/body/MediaCallRoom.tsx | 14 +----- .../MediaCallRoomSection.tsx | 43 ++++++++++++------- .../src/views/MediaCallWidget/OngoingCall.tsx | 32 +++++++------- .../MediaCallWidget/OngoingCallWithScreen.tsx | 42 +++++++++++------- 4 files changed, 70 insertions(+), 61 deletions(-) diff --git a/apps/meteor/client/views/room/body/MediaCallRoom.tsx b/apps/meteor/client/views/room/body/MediaCallRoom.tsx index d6c03f9a01547..d9f950a0fabb5 100644 --- a/apps/meteor/client/views/room/body/MediaCallRoom.tsx +++ b/apps/meteor/client/views/room/body/MediaCallRoom.tsx @@ -1,12 +1,7 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { isDirectMessageRoom } from '@rocket.chat/core-typings'; import type { PeerInfo } from '@rocket.chat/ui-voip'; -import { - MediaCallRoomActivity, - usePeekMediaSessionState, - usePeekMediaSessionPeerInfo, - usePeekMediaSessionFeatures, -} from '@rocket.chat/ui-voip'; +import { MediaCallRoomActivity, usePeekMediaSessionState, usePeekMediaSessionPeerInfo } from '@rocket.chat/ui-voip'; import type { ReactNode } from 'react'; import { memo } from 'react'; @@ -33,15 +28,8 @@ export type MediaCallRoomProps = { const MediaCallRoom = ({ children }: MediaCallRoomProps) => { const state = usePeekMediaSessionState(); const peerInfo = usePeekMediaSessionPeerInfo(); - const features = usePeekMediaSessionFeatures(); const room = useRoom(); - const screenShareEnabled = features.includes('screen-share'); - - if (!screenShareEnabled) { - return children; - } - if (state !== 'ongoing' || !isMediaCallRoom(room, peerInfo)) { return children; } diff --git a/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomSection.tsx b/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomSection.tsx index 71b04cb5d5e62..222b179af2b78 100644 --- a/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomSection.tsx +++ b/packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomSection.tsx @@ -14,6 +14,7 @@ import { } from '../../components'; import { useMediaCallInstance } from '../../context/MediaCallInstanceContext'; import { useMediaCallView } from '../../context/MediaCallViewContext'; +import { usePeekMediaSessionFeatures } from '../../context/usePeekMediaSessionFeatures'; import useRegisterView from '../../context/useRegisterView'; import MediaCallCardList from '../MediaCallCardList'; import PopoutDockPrompt from '../PopoutDockPrompt'; @@ -69,6 +70,12 @@ const MediaCallRoomSection = ({ showChat, onToggleChat, user, containerHeight }: useRegisterView('room'); + const features = usePeekMediaSessionFeatures(); + + const screenShareAvailable = features.includes('screen-share'); + const holdAvailable = features.includes('hold'); + const transferAvailable = features.includes('transfer'); + if (!peerInfo || 'number' in peerInfo) { return null; } @@ -108,21 +115,27 @@ const MediaCallRoomSection = ({ showChat, onToggleChat, user, containerHeight }: } > - - - + {holdAvailable && ( + + )} + {screenShareAvailable && ( + + )} + {transferAvailable && ( + + )} diff --git a/packages/ui-voip/src/views/MediaCallWidget/OngoingCall.tsx b/packages/ui-voip/src/views/MediaCallWidget/OngoingCall.tsx index cc335933fb8d4..5b0ac02a243b3 100644 --- a/packages/ui-voip/src/views/MediaCallWidget/OngoingCall.tsx +++ b/packages/ui-voip/src/views/MediaCallWidget/OngoingCall.tsx @@ -34,8 +34,8 @@ const OngoingCall = () => { const connecting = connectionState === 'CONNECTING'; const reconnecting = connectionState === 'RECONNECTING'; - const transferDisabled = !supportedFeatures.includes('transfer'); - const holdDisabled = !supportedFeatures.includes('hold'); + const holdAvailable = supportedFeatures.includes('hold'); + const transferAvailable = supportedFeatures.includes('transfer'); // TODO: Figure out how to ensure this always exist before rendering the component if (!peerInfo) { @@ -81,21 +81,19 @@ const OngoingCall = () => { onClick={() => setOpen((open) => !open)} /> - - + {holdAvailable && ( + + )} + {transferAvailable && ( + + )} { onToggleScreenSharing, onClosePopout, } = useMediaCallView(); - const { muted, held, remoteMuted, remoteHeld, peerInfo, connectionState, startedAt } = sessionState; + const { muted, held, remoteMuted, remoteHeld, peerInfo, connectionState, startedAt, supportedFeatures } = sessionState; const { currentViews } = useMediaCallInstance(); const isPopout = currentViews.has('popout'); + const screenShareAvailable = supportedFeatures.includes('screen-share'); + const holdAvailable = supportedFeatures.includes('hold'); + const transferAvailable = supportedFeatures.includes('transfer'); + const { localScreen, remoteScreen } = streams; const [remoteStreamRefCallback] = usePlayMediaStream(remoteScreen?.stream ?? null); @@ -120,21 +124,27 @@ const OngoingCall = () => { - - - + {holdAvailable && ( + + )} + {screenShareAvailable && ( + + )} + {transferAvailable && ( + + )} Date: Tue, 18 Aug 2026 15:58:10 -0300 Subject: [PATCH 2/7] feat: Remove beta tag --- apps/meteor/ee/server/settings/voip.ts | 1 - packages/i18n/src/locales/de.i18n.json | 2 -- packages/i18n/src/locales/en.i18n.json | 2 -- packages/i18n/src/locales/pt-BR.i18n.json | 2 -- packages/i18n/src/locales/sv.i18n.json | 2 -- 5 files changed, 9 deletions(-) diff --git a/apps/meteor/ee/server/settings/voip.ts b/apps/meteor/ee/server/settings/voip.ts index 42c424005679a..7c83bdf9e8599 100644 --- a/apps/meteor/ee/server/settings/voip.ts +++ b/apps/meteor/ee/server/settings/voip.ts @@ -13,7 +13,6 @@ export function addSettings(): Promise { type: 'boolean', public: true, invalidValue: false, - alert: 'VoIP_TeamCollab_Screen_Sharing_Enabled_Alert', i18nDescription: 'VoIP_TeamCollab_Screen_Sharing_Enabled_Description', }); diff --git a/packages/i18n/src/locales/de.i18n.json b/packages/i18n/src/locales/de.i18n.json index b6a6ff61c1522..a6c16dd3d24eb 100644 --- a/packages/i18n/src/locales/de.i18n.json +++ b/packages/i18n/src/locales/de.i18n.json @@ -1051,7 +1051,6 @@ "Call_ended_bold": "*Sprachanruf beendet*", "Call_not_answered_bold": "*Sprachanruf nicht angenommen*", "Call_failed_bold": "*Sprachanruf fehlgeschlagen*", - "Call_feature_unsupported": "Die Gegenseite unterstützt dies nicht", "Call_transferred_bold": "*Sprachanruf weitergeleitet*", "Call_history": "Anrufverlauf", "Call_history_provides_a_record_of_when_calls_took_place_and_who_joined": "In der Anrufliste wird festgehalten, wann Anrufe getätigt wurden und wer daran teilgenommen hat.", @@ -5930,7 +5929,6 @@ "VoIP_TeamCollab_Mobile_Ringing_Enabled_Description": "Ermöglicht Benutzern, Anrufe in der mobilen App zu tätigen und zu empfangen.", "VoIP_TeamCollab_Screen_Sharing_Enabled": "Bildschirmfreigabe", "VoIP_TeamCollab_Screen_Sharing_Enabled_Description": "Ermöglicht Benutzern, ihren Bildschirm während Sprachanrufen freizugeben.", - "VoIP_TeamCollab_Screen_Sharing_Enabled_Alert": "Die Bildschirmfreigabe befindet sich derzeit in der Beta-Phase.", "VoIP_TeamCollab_Internal_SIP_Beta_Alert": "Das Weiterleiten interner Anrufe über SIP befindet sich in der Beta-Phase und wird nicht für den Produktiveinsatz empfohlen.", "VoIP_TeamCollab_Drachtio_Host": "Drachtio-Host", "VoIP_TeamCollab_Drachtio_Port": "Drachtio-Port", diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 27cfc5173c289..7417f6fcde17c 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -1122,7 +1122,6 @@ "Call_ended_bold": "*Voice call ended*", "Call_not_answered_bold": "*Voice call not answered*", "Call_failed_bold": "*Voice call failed*", - "Call_feature_unsupported": "Other party doesn't support this", "Call_transferred_bold": "*Voice call transferred*", "Call_history": "Call history", "Call_history_provides_a_record_of_when_calls_took_place_and_who_joined": "Call history provides a record of when calls took place and who joined.", @@ -6060,7 +6059,6 @@ "VoIP_TeamCollab_Mobile_Ringing_Enabled_Description": "Allow users to make and receive calls on the mobile app.", "VoIP_TeamCollab_Screen_Sharing_Enabled": "Screen sharing", "VoIP_TeamCollab_Screen_Sharing_Enabled_Description": "Allow users to share their screen during voice calls.", - "VoIP_TeamCollab_Screen_Sharing_Enabled_Alert": "Screen sharing is currently in beta.", "VoIP_TeamCollab_Internal_SIP_Beta_Alert": "Routing of internal calls through SIP is in beta and is not recommended for production use.", "VoIP_TeamCollab_Drachtio_Host": "Drachtio Host", "VoIP_TeamCollab_Drachtio_Port": "Drachtio Port", diff --git a/packages/i18n/src/locales/pt-BR.i18n.json b/packages/i18n/src/locales/pt-BR.i18n.json index e904c242ccfda..d5d5958083f0d 100644 --- a/packages/i18n/src/locales/pt-BR.i18n.json +++ b/packages/i18n/src/locales/pt-BR.i18n.json @@ -1047,7 +1047,6 @@ "Call_ended_bold": "*Chamada de voz encerrada*", "Call_not_answered_bold": "*Chamada de voz não atendida*", "Call_failed_bold": "*Falha na chamada de voz*", - "Call_feature_unsupported": "A outra parte não suporta isso", "Call_transferred_bold": "*Chamada de voz transferida*", "Call_history": "Histórico de chamadas", "Call_history_provides_a_record_of_when_calls_took_place_and_who_joined": "O histórico de chamadas fornece um registro de quando as chamadas foram realizadas e quem participou.", @@ -5875,7 +5874,6 @@ "VoIP_TeamCollab_Mobile_Ringing_Enabled_Description": "Permitir que os usuários façam e recebam chamadas no aplicativo móvel.", "VoIP_TeamCollab_Screen_Sharing_Enabled": "Compartilhamento de tela", "VoIP_TeamCollab_Screen_Sharing_Enabled_Description": "Permitir que os usuários compartilhem a tela durante as chamadas de voz.", - "VoIP_TeamCollab_Screen_Sharing_Enabled_Alert": "O compartilhamento de tela está atualmente na versão beta.", "VoIP_TeamCollab_Internal_SIP_Beta_Alert": "O roteamento de chamadas internas por SIP está em fase beta e não é recomendado para uso em produção.", "VoIP_TeamCollab_Drachtio_Host": "Host do Drachtio", "VoIP_TeamCollab_Drachtio_Port": "Porta do Drachtio", diff --git a/packages/i18n/src/locales/sv.i18n.json b/packages/i18n/src/locales/sv.i18n.json index 0f80bf8bd2e4c..eb02bd8879bb8 100644 --- a/packages/i18n/src/locales/sv.i18n.json +++ b/packages/i18n/src/locales/sv.i18n.json @@ -1044,7 +1044,6 @@ "Call_ended_bold": "*Röstsamtal avslutad*", "Call_not_answered_bold": "*Röstsamtal besvarades inte*", "Call_failed_bold": "*Röstsamtal misslyckades*", - "Call_feature_unsupported": "Andra parten stöder inte detta", "Call_transferred_bold": "*Röstsamtal överfördes*", "Call_history": "Samtalhistorik", "Call_history_provides_a_record_of_when_calls_took_place_and_who_joined": "I samtalshistoriken samlas information om när samtal ägt rum och vilka som deltog.", @@ -5858,7 +5857,6 @@ "VoIP_TeamCollab_Mobile_Ringing_Enabled_Description": "Tillåt användare att göra och ta emot samtal i mobilappen.", "VoIP_TeamCollab_Screen_Sharing_Enabled": "Skärmdelning", "VoIP_TeamCollab_Screen_Sharing_Enabled_Description": "Tillåt användare att dela sin skärm under röstsamtal.", - "VoIP_TeamCollab_Screen_Sharing_Enabled_Alert": "Skärmdelning är för närvarande i beta.", "VoIP_TeamCollab_Internal_SIP_Beta_Alert": "Routning av interna samtal genom SIP är i beta och rekommenderas inte för produktionsanvändning.", "VoIP_TeamCollab_Drachtio_Host": "Drachtio Host", "VoIP_TeamCollab_Drachtio_Port": "Drachtio Port", From 28267a0254a8c2c97a99e9b0a6095ac1a92c1373 Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Tue, 18 Aug 2026 18:45:55 -0300 Subject: [PATCH 3/7] test: Update e2e tests --- .../e2e/page-objects/fragments/voice-calls.ts | 8 +++- apps/meteor/tests/e2e/voice-calls-ee.spec.ts | 38 +++++++------------ 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/apps/meteor/tests/e2e/page-objects/fragments/voice-calls.ts b/apps/meteor/tests/e2e/page-objects/fragments/voice-calls.ts index 1570c0c972e58..fd615f7e062b1 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/voice-calls.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/voice-calls.ts @@ -107,7 +107,7 @@ export class Widget { private readonly transferModal: TransferModal; - constructor(page: Page) { + constructor(private readonly page: Page) { this.transferModal = new TransferModal(page, page.getByRole('dialog', { name: 'Transfer call' })); this.root = page.getByRole('dialog', { name: 'Voice call', exact: false }); this.callControls = new VoiceCallControls(this.root.getByRole('group')); @@ -140,7 +140,11 @@ export class Widget { return timerToSeconds(text); } - async initiateCall(): Promise { + async initiateCall(username?: string): Promise { + if (username) { + await this.root.getByRole('textbox', { name: 'Enter username or number' }).fill(username); + await this.page.getByRole('listbox').getByRole('option', { name: username }).click(); + } await this.callControls.call.click(); await expect(this.callControls.cancel).toBeVisible(); } diff --git a/apps/meteor/tests/e2e/voice-calls-ee.spec.ts b/apps/meteor/tests/e2e/voice-calls-ee.spec.ts index ad93f25a37d42..9fab48397ae89 100644 --- a/apps/meteor/tests/e2e/voice-calls-ee.spec.ts +++ b/apps/meteor/tests/e2e/voice-calls-ee.spec.ts @@ -34,15 +34,10 @@ test.describe('Internal Voice Calls - Enterprise Edition', () => { test('should initiate voice call from direct message', async () => { const [user1, user2] = sessions; - await test.step('should open direct message with user2', async () => { - await user1.poHomeChannel.navbar.openChat('user2'); - await expect(user1.poHomeChannel.composer.inputMessage).toBeVisible(); - }); - await test.step('initiate a voice call from room toolbar', async () => { - await user1.poHomeChannel.content.btnVoiceCall.click(); + await user1.poHomeChannel.navbar.btnNewVoiceCall.click(); await expect(user1.poHomeChannel.voiceCalls.widget.content).toBeVisible(); - await user1.poHomeChannel.voiceCalls.widget.initiateCall(); + await user1.poHomeChannel.voiceCalls.widget.initiateCall(Users.user2.data.username); }); await test.step('user2 accepts the call', async () => { @@ -59,37 +54,34 @@ test.describe('Internal Voice Calls - Enterprise Edition', () => { test('should handle call controls during active call', async () => { const [user1, user2] = sessions; await test.step('establish call connection', async () => { - await user1.poHomeChannel.navbar.openChat('user2'); - await expect(user1.poHomeChannel.composer.inputMessage).toBeVisible(); - await user1.poHomeChannel.content.btnVoiceCall.click(); - await user1.poHomeChannel.voiceCalls.widget.initiateCall(); + await user1.poHomeChannel.navbar.btnNewVoiceCall.click(); + await user1.poHomeChannel.voiceCalls.widget.initiateCall(Users.user2.data.username); await user2.poHomeChannel.voiceCalls.widget.acceptCall(); }); await test.step('should mute/unmute microphone from user1', async () => { - // User1 mutes microphone await user1.poHomeChannel.voiceCalls.widget.muteSelf(); - // User1 unmutes microphone await user1.poHomeChannel.voiceCalls.widget.unmuteSelf(); }); await test.step('should put call on hold from user1', async () => { - // User1 puts call on hold await user1.poHomeChannel.voiceCalls.widget.holdSelf(); - // User1 resumes call await user1.poHomeChannel.voiceCalls.widget.resumeSelf(); }); await test.step('should access dialpad during call', async () => { - // User1 opens dial pad await user1.poHomeChannel.voiceCalls.widget.openDialpad(); - // User1 closes dial pad await user1.poHomeChannel.voiceCalls.widget.closeDialpad(); }); + await test.step('should not display screen share button', async () => { + await expect(user1.poHomeChannel.voiceCalls.widget.controls.shareScreen).not.toBeVisible(); + await expect(user2.poHomeChannel.voiceCalls.widget.controls.shareScreen).not.toBeVisible(); + }); + await test.step('should end the call from user1', async () => { await user1.poHomeChannel.voiceCalls.widget.endCall(); await expect(user2.poHomeChannel.voiceCalls.widget.content).not.toBeVisible(); @@ -106,10 +98,8 @@ test.describe('Internal Voice Calls - Enterprise Edition', () => { const user3 = { page: user3Context.page, poHomeChannel: new HomeChannel(user3Context.page) }; await test.step('establish call between user1 and user2', async () => { - await user1.poHomeChannel.navbar.openChat('user2'); - await expect(user1.poHomeChannel.composer.inputMessage).toBeVisible(); - await user1.poHomeChannel.content.btnVoiceCall.click(); - await user1.poHomeChannel.voiceCalls.widget.initiateCall(); + await user1.poHomeChannel.navbar.btnNewVoiceCall.click(); + await user1.poHomeChannel.voiceCalls.widget.initiateCall(Users.user2.data.username); await user2.poHomeChannel.voiceCalls.widget.acceptCall(); }); @@ -139,10 +129,8 @@ test.describe('Internal Voice Calls - Enterprise Edition', () => { const [user1, user2] = sessions; await test.step('user1 initiates call to user2', async () => { - await user1.poHomeChannel.navbar.openChat('user2'); - await expect(user1.poHomeChannel.composer.inputMessage).toBeVisible(); - await user1.poHomeChannel.content.btnVoiceCall.click(); - await user1.poHomeChannel.voiceCalls.widget.initiateCall(); + await user1.poHomeChannel.navbar.btnNewVoiceCall.click(); + await user1.poHomeChannel.voiceCalls.widget.initiateCall(Users.user2.data.username); }); await test.step('user2 declines the call', async () => { From 8a0409a483e419c59311ce179cec854f3b279192 Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Tue, 18 Aug 2026 19:00:00 -0300 Subject: [PATCH 4/7] fix: widget footer buttons alignment --- packages/ui-voip/src/views/MediaCallWidget/OngoingCall.tsx | 2 +- .../ui-voip/src/views/MediaCallWidget/OngoingCallWithScreen.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui-voip/src/views/MediaCallWidget/OngoingCall.tsx b/packages/ui-voip/src/views/MediaCallWidget/OngoingCall.tsx index 5b0ac02a243b3..934691bed18f7 100644 --- a/packages/ui-voip/src/views/MediaCallWidget/OngoingCall.tsx +++ b/packages/ui-voip/src/views/MediaCallWidget/OngoingCall.tsx @@ -72,7 +72,7 @@ const OngoingCall = () => { ) : null} - + { - + {holdAvailable && ( From 6fc81b979c4558ea9c1b9d7b5240108ed70af4b9 Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Tue, 18 Aug 2026 19:00:20 -0300 Subject: [PATCH 5/7] test: Add stories and update snapshots --- .../src/providers/MockedMediaCallProvider.tsx | 6 +- .../MediaCallWidget.stories.tsx | 14 + .../MediaCallWidget.spec.tsx.snap | 384 +++++++++++++++++- 3 files changed, 401 insertions(+), 3 deletions(-) diff --git a/packages/ui-voip/src/providers/MockedMediaCallProvider.tsx b/packages/ui-voip/src/providers/MockedMediaCallProvider.tsx index 88a9fe20951bf..d9b9eb20b0953 100644 --- a/packages/ui-voip/src/providers/MockedMediaCallProvider.tsx +++ b/packages/ui-voip/src/providers/MockedMediaCallProvider.tsx @@ -1,4 +1,6 @@ import { UserStatus } from '@rocket.chat/core-typings'; +import type { CallFeature } from '@rocket.chat/media-signaling'; +import { callFeatureList } from '@rocket.chat/media-signaling'; import type { ReactNode } from 'react'; import { useState } from 'react'; @@ -18,6 +20,7 @@ export type MockedMediaCallProviderProps = { held?: boolean; onClickDirectMessage?: () => void; instanceProps?: Partial; + supportedFeatures?: readonly CallFeature[]; }; const MockedMediaCallProvider = ({ @@ -31,6 +34,7 @@ const MockedMediaCallProvider = ({ muted = false, held = false, instanceProps, + supportedFeatures = callFeatureList, }: MockedMediaCallProviderProps) => { const [peerInfo, setPeerInfo] = useState({ displayName: 'John Doe', @@ -122,7 +126,7 @@ const MockedMediaCallProvider = ({ remoteMuted, remoteHeld, callId: undefined, - supportedFeatures: ['audio', 'screen-share', 'transfer', 'hold'], + supportedFeatures, } as SessionState; const contextValue = { diff --git a/packages/ui-voip/src/views/MediaCallWidget/MediaCallWidget.stories.tsx b/packages/ui-voip/src/views/MediaCallWidget/MediaCallWidget.stories.tsx index 1cdc4241ef714..0ee6e3755586c 100644 --- a/packages/ui-voip/src/views/MediaCallWidget/MediaCallWidget.stories.tsx +++ b/packages/ui-voip/src/views/MediaCallWidget/MediaCallWidget.stories.tsx @@ -118,3 +118,17 @@ export const OngoingCallConnecting: Story = { connectionState: 'CONNECTING', }, }; + +export const OngoingCallDisabledAllFeatures: Story = { + args: { + state: 'ongoing', + supportedFeatures: [], + }, +}; + +export const OngoingCallDisabledAllButScreenshare: Story = { + args: { + state: 'ongoing', + supportedFeatures: ['screen-share'], + }, +}; diff --git a/packages/ui-voip/src/views/MediaCallWidget/__snapshots__/MediaCallWidget.spec.tsx.snap b/packages/ui-voip/src/views/MediaCallWidget/__snapshots__/MediaCallWidget.spec.tsx.snap index 35c18109ea1a8..a4c5a72cd01b6 100644 --- a/packages/ui-voip/src/views/MediaCallWidget/__snapshots__/MediaCallWidget.spec.tsx.snap +++ b/packages/ui-voip/src/views/MediaCallWidget/__snapshots__/MediaCallWidget.spec.tsx.snap @@ -866,7 +866,7 @@ exports[`renders OngoingCall without crashing 1`] = ` class="rcx-box rcx-box--full rcx-css-1koqfyi" >
+ +
+ +
+
+
+
+
+ +
+
+
+
+ + +
+ John Doe +
+
+
+
+ 1234567890 +
+
+
+
+
+
+
+ + + +
+
+ +